]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Replace Protractor with Cypress
authorTiago Melo <tmelo@suse.com>
Fri, 13 Mar 2020 01:00:51 +0000 (00:00 -0100)
committerTiago Melo <tmelo@suse.com>
Tue, 28 Apr 2020 09:43:50 +0000 (09:43 +0000)
Fixes: https://tracker.ceph.com/issues/44812
Signed-off-by: Tiago Melo <tmelo@suse.com>
98 files changed:
src/pybind/mgr/dashboard/CMakeLists.txt
src/pybind/mgr/dashboard/HACKING.rst
src/pybind/mgr/dashboard/frontend/.gitignore
src/pybind/mgr/dashboard/frontend/angular.json
src/pybind/mgr/dashboard/frontend/cypress.json [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.e2e-spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.po.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/support/index.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/e2e/block/images.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/block/images.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/block/iscsi.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/block/iscsi.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/block/mirroring.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/block/mirroring.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/configuration.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/configuration.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/crush-map.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/crush-map.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/hosts.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/hosts.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/logs.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/logs.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/mgr-modules.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/mgr-modules.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/monitors.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/monitors.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/osds.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/cluster/osds.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/filesystems/filesystems.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/filesystems/filesystems.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/nfs/nfs.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/nfs/nfs.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/pools/pools.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/pools/pools.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/rgw/daemons.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/rgw/daemons.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/rgw/users.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/rgw/users.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/tsconfig.e2e.json [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/ui/dashboard.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/ui/dashboard.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/ui/notification.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/ui/notification.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/ui/role-mgmt.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/ui/role-mgmt.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/ui/user-mgmt.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/e2e/ui/user-mgmt.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/protractor.conf.js [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.ts
src/pybind/mgr/dashboard/frontend/tsconfig.json
src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh

index 7d871f986db3bf310ff64d27f802347e5c366364..104b2a1846f5e8847eb7a1a17ca5f0f3f7ae11c3 100644 (file)
@@ -49,7 +49,7 @@ endif()
 
 add_npm_command(
   OUTPUT "${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend/node_modules"
-  COMMAND NG_CLI_ANALYTICS=false npm ci
+  COMMAND NG_CLI_ANALYTICS=false CYPRESS_CACHE_FOLDER=${CMAKE_SOURCE_DIR}/build/src/pybind/mgr/dashboard/cypress npm ci
   DEPENDS frontend/package.json
   WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/pybind/mgr/dashboard/frontend
   COMMENT "dashboard frontend dependencies are being installed"
index e1963e93e105229b6c4edb20b968615ec47f52b5..6d0f47fab1379d1a07f784646e14273939ce20e7 100644 (file)
@@ -333,30 +333,47 @@ There are a few ways how you can try to resolve this:
 Running End-to-End (E2E) Tests
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-We use `Protractor <http://www.protractortest.org/>`__ to run our frontend E2E
-tests.
+We use `Cypress <https://www.cypress.io/>`__ to run our frontend E2E tests.
 
-Our ``run-frontend-e2e-tests.sh`` script will check if Chrome or Docker is
-installed and run the tests if either is found.
+E2E Prerequisites
+.................
+
+You need to previously build the frontend.
+
+In some environments, depending on your user permissions and the CYPRESS_CACHE_FOLDER,
+you might need to run ``npm ci`` with the ``--unsafe-perm`` flag.
+
+You might need to install additional packages to be able to run Cypress.
+Please run ``npx cypress verify`` to verify it.
+
+run-frontend-e2e-tests.sh
+.........................
+
+Our ``run-frontend-e2e-tests.sh`` script is the go to solution when you wish to
+do a full scale e2e run.
+It will verify if everything needed is installed, start a new vstart cluster
+and run the full test suite.
 
 Start all frontend E2E tests by running::
 
   $ ./run-frontend-e2e-tests.sh
 
 Report:
-  After running the tests you can find the corresponding report as well as screenshots
-  of failed test cases by opening the following file in your browser:
+  You can follow the e2e report on the terminal and you can find the screenshots
+  of failed test cases by opening the following directory::
 
-    src/pybind/mgr/dashboard/frontend/.protractor-report/index.html
+    src/pybind/mgr/dashboard/frontend/cypress/screenshots/
 
 Device:
   You can force the script to use a specific device with the ``-d`` flag::
 
-    $ ./run-frontend-e2e-tests.sh -d <chrome|docker>
+    $ ./run-frontend-e2e-tests.sh -d <chrome|chromium|electron|docker>
 
 Remote:
+  By default this script will stop and start a new vstart cluster.
   If you want to run the tests outside the ceph environment, you will need to
-  manually define the dashboard url using ``-r`` and, optionally, credentials (``-u``, ``-p``)::
+  manually define the dashboard url using ``-r`` and, optionally, credentials
+  (``-u``, ``-p``)::
 
     $ ./run-frontend-e2e-tests.sh -r <DASHBOARD_URL> -u <E2E_LOGIN_USER> -p <E2E_LOGIN_PWD>
 
@@ -364,29 +381,56 @@ Note:
   When using docker, as your device, you might need to run the script with sudo
   permissions.
 
-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:ci``.
+Other running options
+.....................
 
-Note::
+During active development, it is not recommended to run the previous script,
+as it is not prepared for constant file changes.
+Instead you should use one of the following commands:
+
+- ``npm run e2e`` - This will run ``ng serve`` and open the Cypress Test Runner.
+- ``npm run e2e:ci`` - This will run ``ng serve`` and run the Cypress Test Runner once.
+- ``npx cypress run`` - This calls cypress directly and will run the Cypress Test Runner.
+  You need to have a running frontend server.
+- ``npx cypress open`` - This calls cypress directly and will open the Cypress Test Runner.
+  You need to have a running frontend server.
+
+Calling Cypress directly has the advantage that you can use any of the available
+`flags <https://docs.cypress.io/guides/guides/command-line.html#cypress-run>`__
+to customize your test run and you don't need to start a frontend server each time.
+
+Using one of the ``open`` commands, will open a cypress application where you
+can see all the test files you have and run each individually.
+This is going to be run in watch mode, so if you make any changes to test files,
+it will retrigger the test run.
+This cannot be used inside docker, as it requires X11 environment to be able to open.
+
+By default Cypress will look for the web page at ``https://localhost:4200/``.
+If you are serving it in a different URL you will need to configure it by
+exporting the environment variable CYPRESS_BASE_URL with the new value.
+E.g.: ``CYPRESS_BASE_URL=https://localhost:41076/ npx cypress open``
+
+CYPRESS_CACHE_FOLDER
+.....................
 
-   In case you have a somewhat particular environment, you might need to adapt
-   `protractor.conf.js` to point to the appropriate destination.
+When installing cypress via npm, a binary of the cypress app will also be
+downloaded and stored in a cache folder.
+This removes the need to download it every time you run ``npm ci`` or even when
+using cypress in a separate project.
 
-Writing End-to-End Tests
-~~~~~~~~~~~~~~~~~~~~~~~~
+By default Cypress uses ~/.cache to store the binary.
+To prevent changes to the user home directory, we have changed this folder to
+``/ceph/build/src/pybind/mgr/dashboard/cypress``, so when you build ceph or run
+``run-frontend-e2e-tests.sh`` this is the directory Cypress will use.
 
-To be used methods
-..................
+When using any other command to install or run cypress,
+it will go back to the default directory. It is recommended that you export the
+CYPRESS_CACHE_FOLDER environment variable with a fixed directory, so you always
+use the same directory no matter which command you use.
 
-For clicking checkboxes, the ``clickCheckbox`` method is supposed to be used.
-Due an adaption of the ``<input type="checkbox">`` tag, the original checkbox
-is hidden and unclickable. Instead, a fancier replacement is shown. When the
-developer tries to use `ElementFinder::click()` on such a checkbox, it will
-raise an error. The ``clickCheckbox`` method prevents that by clicking the
-label of the checkbox, like a regular user would do.
+
+Writing End-to-End Tests
+~~~~~~~~~~~~~~~~~~~~~~~~
 
 The PagerHelper class
 .....................
@@ -396,9 +440,11 @@ can be used on various pages or suites.
 
 Examples are
 
-- ``getTableCellByContent()`` - returns a table cell by its content
+- ``navigateTo()`` - Navigates to a specific page and waits for it to load
+- ``getFirstTableCell()`` - returns the first table cell. You can also pass a
+  string with the desired content and it will return the first cell that
+  contains it.
 - ``getTabsCount()`` - returns the amount of tabs
-- ``clickCheckbox()`` - clicks a checkbox
 
 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
@@ -418,113 +464,102 @@ 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;
-
-     constructor() {
-        this.pools = new PoolPageHelper();
-     }
-
-     // ...
-  }
+Methods that return HTML elements 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.
 
 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.
+In any suite, an instance of the specific ``Helper`` class should be
+instantiated and called directly.
 
 .. code:: TypeScript
 
+  const pools = new PoolPageHelper();
+
   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);
-      });
-    });
+    pools.exist(poolName, false);
+    pools.navigateTo('create');
+    pools.create(poolName, 8);
+    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.
+Please refer to the official `Cypress Core Concepts
+<https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Cypress-Can-Be-Simple-Sometimes>`__
+for a better insight on how to write and structure tests.
 
 ``describe()`` vs ``it()``
 """"""""""""""""""""""""""
 
-Both ``describe()`` and ``it()`` are function blocks, meaning that any executable
-code necessary for the test can be contained in either block. However, Typescript
-scoping rules still apply, therefore any variables declared in a ``describe`` are available
-to the ``it()`` blocks inside of it.
+Both ``describe()`` and ``it()`` are function blocks, meaning that any
+executable code necessary for the test can be contained in either block.
+However, Typescript scoping rules still apply, therefore any variables declared
+in a ``describe`` are available to the ``it()`` blocks inside of it.
 
-``describe()`` typically are containers for tests, allowing you to break tests into
-multiple parts. Likewise, any setup that must be made before your tests are run can be
-initialized within the ``describe()`` block. Here is an example:
+``describe()`` typically are containers for tests, allowing you to break tests
+into multiple parts. Likewise, any setup that must be made before your tests are
+run can be initialized within the ``describe()`` block. Here is an example:
 
 .. code:: TypeScript
 
   describe('create, edit & delete image test', () => {
     const poolName = 'e2e_images_pool';
 
-    beforeAll(() => {
-      pools.navigateTo('create'); // Need pool for image testing
-      pools.create(poolName, 8, 'rbd').then(() => {
-        pools.navigateTo();
-        pools.exist(poolName, true);
-      });
+    before(() => {
+      cy.login();
+      pools.navigateTo('create');
+      pools.create(poolName, 8, 'rbd');
+      pools.exist(poolName, true);
+    });
+
+    beforeEach(() => {
+      cy.login();
       images.navigateTo();
     });
 
+    //...
+
+  });
+
 As shown, we can initiate the variable ``poolName`` as well as run commands
-before our test suite begins (creating a pool). ``describe()`` block messages should
-include what the test suite is.
+before our test suite begins (creating a pool). ``describe()`` block messages
+should include what the test suite is.
 
-``it()`` blocks typically are parts of an overarching test. They contain the functionality of
-the test suite, each performing individual roles. Here is an example:
+``it()`` blocks typically are parts of an overarching test. They contain the
+functionality of the test suite, each performing individual roles.
+Here is an example:
 
 .. code:: TypeScript
 
- describe('create, edit & delete image test', () => {
-  it('should create image', () => {
-    images.createImage(imageName, poolName, '1');
-    expect(images.getTableCell(imageName).isPresent()).toBe(true);
-  });
-  it('should edit image', () => {
-    images.editImage(imageName, poolName, newImageName, '2');
-    expect(images.getTableCell(newImageName).isPresent()).toBe(true);
+  describe('create, edit & delete image test', () => {
+    //...
+
+    it('should create image', () => {
+      images.createImage(imageName, poolName, '1');
+      images.getFirstTableCell(imageName).should('exist');
+    });
+
+    it('should edit image', () => {
+      images.editImage(imageName, poolName, newImageName, '2');
+      images.getFirstTableCell(newImageName).should('exist');
+    });
+
+    //...
   });
-  //...
- });
 
-As shown from the previous example, our ``describe()`` test suite is to create, edit
-and delete an image. Therefore, each ``it()`` completes one of these steps, one for creating,
-one for editing, and so on. Likewise, every ``it()`` blocks message should be in lowercase
-and written so long as "it" can be the prefix of the message. For example, ``it('edits the test image' () => ...)``
-vs. ``it('image edit test' () => ...)``. As shown, the first example makes grammatical sense with ``it()`` as the
-prefix whereas the second message does not.``it()`` should describe what the individual test is doing and
-what it expects to happen.
+As shown from the previous example, our ``describe()`` test suite is to create,
+edit and delete an image. Therefore, each ``it()`` completes one of these steps,
+one for creating, one for editing, and so on. Likewise, every ``it()`` blocks
+message should be in lowercase and written so long as "it" can be the prefix of
+the message. For example, ``it('edits the test image' () => ...)`` vs.
+``it('image edit test' () => ...)``. As shown, the first example makes
+grammatical sense with ``it()`` as the prefix whereas the second message does
+not. ``it()`` should describe what the individual test is doing and what it
+expects to happen.
 
 Differences between Frontend Unit Tests and End-to-End (E2E) Tests / FAQ
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -537,10 +572,6 @@ What are E2E/unit tests designed for?
 
 E2E test:
 
-"Protractor is an end-to-end test framework for Angular and AngularJS applications.
-Protractor runs tests against your application running in a real browser,
-interacting with it as a user would." `(src) <http://www.protractortest.org/#/>`__
-
 It requires a fully functional system and tests the interaction of all components
 of the application (Ceph, back-end, front-end).
 E2E tests are designed to mimic the behavior of the user when interacting with the application
index 08ded32538398ad8d20e55c3f84a89127fcab4e6..5bb225251f09f9660c224423b460d7bacb1ac3d6 100644 (file)
@@ -34,9 +34,8 @@ testem.log
 /src/unit-test-configuration.ts
 
 # e2e
-/e2e/*.js
-/e2e/*.map
-.protractor-fail-fast
+/cypress/screenshots
+/cypress/videos
 
 # System Files
 .DS_Store
index c54fb5f553dc8ea5d7d350a03a4c8138aa0acf75..da61a75de20cc098bedd14c7b93f1d4d125b7bfe 100644 (file)
         }
       },
       "cli": {}
-    },
-    "ceph-dashboard-e2e": {
-      "root": "",
-      "sourceRoot": "",
-      "projectType": "application",
-      "architect": {
-        "e2e": {
-          "builder": "@angular-devkit/build-angular:protractor",
-          "options": {
-            "protractorConfig": "./protractor.conf.js",
-            "devServerTarget": "ceph-dashboard:serve"
-          }
-        },
-        "lint": {
-          "builder": "@angular-devkit/build-angular:tslint",
-          "options": {
-            "tsConfig": [
-              "e2e/tsconfig.e2e.json"
-            ],
-            "exclude": [
-              "**/node_modules/**"
-            ]
-          }
-        }
-      },
-      "cli": {}
     }
   },
   "defaultProject": "ceph-dashboard",
diff --git a/src/pybind/mgr/dashboard/frontend/cypress.json b/src/pybind/mgr/dashboard/frontend/cypress.json
new file mode 100644 (file)
index 0000000..4f60424
--- /dev/null
@@ -0,0 +1,14 @@
+{
+  "baseUrl": "http://localhost:4200/",
+  "ignoreTestFiles": [
+    "*.po.ts"
+  ],
+  "supportFile": "cypress/support/index.ts",
+  "video": false,
+  "defaultCommandTimeout": 20000,
+  "viewportHeight": 1080,
+  "viewportWidth": 1920,
+  "pluginsFile": false,
+  "fixturesFolder": false,
+  "projectId": "k7ab29"
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.e2e-spec.ts
new file mode 100644 (file)
index 0000000..87900a0
--- /dev/null
@@ -0,0 +1,92 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { ImagesPageHelper } from './images.po';
+
+describe('Images page', () => {
+  const pools = new PoolPageHelper();
+  const images = new ImagesPageHelper();
+
+  const poolName = 'e2e_images_pool';
+
+  before(() => {
+    cy.login();
+    // Need pool for image testing
+    pools.navigateTo('create');
+    pools.create(poolName, 8, 'rbd');
+    pools.exist(poolName, true);
+  });
+
+  after(() => {
+    // Deletes images test pool
+    pools.navigateTo();
+    pools.delete(poolName);
+    pools.navigateTo();
+    pools.exist(poolName, false);
+  });
+
+  beforeEach(() => {
+    cy.login();
+    images.navigateTo();
+  });
+
+  it('should open and show breadcrumb', () => {
+    images.expectBreadcrumbText('Images');
+  });
+
+  it('should show four tabs', () => {
+    images.getTabsCount().should('eq', 4);
+  });
+
+  it('should show text for all tabs', () => {
+    images.getTabText(0).should('eq', 'Images');
+    images.getTabText(1).should('eq', 'Namespaces');
+    images.getTabText(2).should('eq', 'Trash');
+    images.getTabText(3).should('eq', 'Overall Performance');
+  });
+
+  describe('create, edit & delete image test', () => {
+    const imageName = 'e2e_images#image';
+    const newImageName = 'e2e_images#image_new';
+
+    it('should create image', () => {
+      images.createImage(imageName, poolName, '1');
+      images.getFirstTableCell(imageName).should('exist');
+    });
+
+    it('should edit image', () => {
+      images.editImage(imageName, poolName, newImageName, '2');
+      images.getFirstTableCell(newImageName).should('exist');
+    });
+
+    it('should delete image', () => {
+      images.delete(newImageName);
+    });
+  });
+
+  describe('move to trash, restore and purge image tests', () => {
+    const imageName = 'e2e_trash#image';
+    const newImageName = 'e2e_newtrash#image';
+
+    before(() => {
+      cy.login();
+      // Need image for trash testing
+      images.createImage(imageName, poolName, '1');
+      images.getFirstTableCell(imageName).should('exist');
+    });
+
+    it('should move the image to the trash', () => {
+      images.moveToTrash(imageName);
+      images.getFirstTableCell(imageName).should('exist');
+    });
+
+    it('should restore image to images table', () => {
+      images.restoreImage(imageName, newImageName);
+      images.getFirstTableCell(newImageName).should('exist');
+    });
+
+    it('should purge trash in images trash tab', () => {
+      images.getFirstTableCell(newImageName).should('exist');
+      images.moveToTrash(newImageName);
+      images.purgeTrash(newImageName, poolName);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.po.ts
new file mode 100644 (file)
index 0000000..f6cc1b9
--- /dev/null
@@ -0,0 +1,115 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ImagesPageHelper extends PageHelper {
+  pages = {
+    index: { url: '#/block/rbd', id: 'cd-rbd-list' },
+    create: { url: '#/block/rbd/create', id: 'cd-rbd-form' }
+  };
+
+  // Creates a block image and fills in the name, pool, and size fields.
+  // Then checks if the image is present in the Images table.
+  createImage(name: string, pool: string, size: string) {
+    this.navigateTo('create');
+
+    cy.get('#name').type(name); // Enter in image name
+
+    // Select image pool
+    cy.contains('Loading...').should('not.exist');
+    this.selectOption('pool', pool);
+    cy.get('#pool').should('have.class', 'ng-valid'); // check if selected
+
+    // Enter in the size of the image
+    cy.get('#size').type(size);
+
+    // Click the create button and wait for image to be made
+    cy.contains('button', 'Create RBD').click();
+    this.getFirstTableCell(name).should('exist');
+  }
+
+  editImage(name: string, pool: string, newName: string, newSize: string) {
+    const base_url = '#/block/rbd/edit/';
+    const editURL = base_url
+      .concat(encodeURIComponent(pool))
+      .concat('%2F')
+      .concat(encodeURIComponent(name));
+    cy.visit(editURL);
+
+    // Wait until data is loaded
+    cy.get('#pool').should('contain.value', pool);
+
+    cy.get('#name').clear().type(newName);
+    cy.get('#size').clear().type(newSize); // click the size box and send new size
+
+    cy.contains('button', 'Edit RBD').click();
+
+    this.getExpandCollapseElement(newName).click();
+    cy.get('.table.table-striped.table-bordered').contains('td', newSize);
+  }
+
+  // Selects RBD image and moves it to the trash,
+  // checks that it is present in the trash table
+  moveToTrash(name: string) {
+    // wait for image to be created
+    cy.get('.datatable-body').first().should('not.contain.text', '(Creating...)');
+
+    this.getFirstTableCell(name).click();
+
+    // click on the drop down and selects the move to trash option
+    cy.get('.table-actions button.dropdown-toggle').first().click();
+    cy.get('li.move-to-trash').click();
+
+    cy.contains('button', 'Move Image').should('be.visible').click();
+
+    // Clicks trash tab
+    cy.contains('.nav-link', 'Trash').click();
+    this.getFirstTableCell(name).should('exist');
+  }
+
+  // Checks trash tab table for image and then restores it to the RBD Images table
+  // (could change name if new name is given)
+  restoreImage(name: string, newName?: string) {
+    // clicks on trash tab
+    cy.contains('.nav-link', 'Trash').click();
+
+    // wait for table to load
+    this.getFirstTableCell(name).click();
+    cy.contains('button', 'Restore').click();
+
+    // wait for pop-up to be visible (checks for title of pop-up)
+    cy.get('#name').should('be.visible');
+
+    // If a new name for the image is passed, it changes the name of the image
+    if (newName !== undefined) {
+      // click name box and send new name
+      cy.get('#name').clear().type(newName);
+    }
+
+    cy.contains('button', 'Restore Image').click();
+
+    // clicks images tab
+    cy.contains('.nav-link', 'Images').click();
+
+    this.getFirstTableCell(newName).should('exist');
+  }
+
+  // Enters trash tab and purges trash, thus emptying the trash table.
+  // Checks if Image is still in the table.
+  purgeTrash(name: string, pool?: string) {
+    // clicks trash tab
+    cy.contains('.nav-link', 'Trash').click();
+    cy.contains('button', 'Purge Trash').click();
+
+    // Check for visibility of modal container
+    cy.get('.modal-header').should('be.visible');
+
+    // If purgeing a specific pool, selects that pool if given
+    if (pool !== undefined) {
+      this.selectOption('poolName', pool);
+      cy.get('#poolName').should('have.class', 'ng-valid'); // check if pool is selected
+    }
+    cy.get('#purgeFormButton').click();
+    // Wait for image to delete and check it is not present
+
+    this.getFirstTableCell(name).should('not.exist');
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.e2e-spec.ts
new file mode 100644 (file)
index 0000000..f7154fb
--- /dev/null
@@ -0,0 +1,24 @@
+import { IscsiPageHelper } from './iscsi.po';
+
+describe('Iscsi Page', () => {
+  const iscsi = new IscsiPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    iscsi.navigateTo();
+  });
+
+  it('should open and show breadcrumb', () => {
+    iscsi.expectBreadcrumbText('Overview');
+  });
+
+  it('should check that tables are displayed and legends are correct', () => {
+    // Check tables are displayed
+    iscsi.getDataTables().its(0).should('be.visible');
+    iscsi.getDataTables().its(1).should('visible');
+
+    // Check that legends are correct
+    iscsi.getLegends().its(0).should('contain.text', 'Gateways');
+    iscsi.getLegends().its(1).should('contain.text', 'Images');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.po.ts
new file mode 100644 (file)
index 0000000..08efa64
--- /dev/null
@@ -0,0 +1,7 @@
+import { PageHelper } from '../page-helper.po';
+
+export class IscsiPageHelper extends PageHelper {
+  pages = {
+    index: { url: '#/block/iscsi/overview', id: 'cd-iscsi' }
+  };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.e2e-spec.ts
new file mode 100644 (file)
index 0000000..c7bd427
--- /dev/null
@@ -0,0 +1,53 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { MirroringPageHelper } from './mirroring.po';
+
+describe('Mirroring page', () => {
+  const pools = new PoolPageHelper();
+  const mirroring = new MirroringPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    mirroring.navigateTo();
+  });
+
+  it('should open and show breadcrumb', () => {
+    mirroring.expectBreadcrumbText('Mirroring');
+  });
+
+  it('should show three tabs', () => {
+    mirroring.getTabsCount().should('eq', 3);
+  });
+
+  it('should show text for all tabs', () => {
+    mirroring.getTabText(0).should('eq', 'Issues');
+    mirroring.getTabText(1).should('eq', 'Syncing');
+    mirroring.getTabText(2).should('eq', 'Ready');
+  });
+
+  describe('checks that edit mode functionality shows in the pools table', () => {
+    const poolName = 'mirroring_test';
+
+    beforeEach(() => {
+      pools.navigateTo('create'); // Need pool for mirroring testing
+      pools.create(poolName, 8, 'rbd');
+      pools.navigateTo();
+      pools.exist(poolName, true);
+    });
+
+    it('tests editing mode for pools', () => {
+      mirroring.navigateTo();
+
+      mirroring.editMirror(poolName, 'Pool');
+      mirroring.getFirstTableCell('pool').should('be.visible');
+      mirroring.editMirror(poolName, 'Image');
+      mirroring.getFirstTableCell('image').should('be.visible');
+      mirroring.editMirror(poolName, 'Disabled');
+      mirroring.getFirstTableCell('disabled').should('be.visible');
+    });
+
+    afterEach(() => {
+      pools.navigateTo();
+      pools.delete(poolName);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.po.ts
new file mode 100644 (file)
index 0000000..8450763
--- /dev/null
@@ -0,0 +1,32 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+  index: { url: '#/block/mirroring', id: 'cd-mirroring' }
+};
+
+export class MirroringPageHelper extends PageHelper {
+  pages = pages;
+
+  /**
+   * Goes to the mirroring page and edits a pool in the Pool table. Clicks on the
+   * pool and chooses an option (either pool, image, or disabled)
+   */
+  @PageHelper.restrictTo(pages.index.url)
+  editMirror(name: string, option: string) {
+    // Clicks the pool in the table
+    this.getFirstTableCell(name).click();
+
+    // Clicks the Edit Mode button
+    cy.contains('button', 'Edit Mode').click();
+
+    // Clicks the drop down in the edit pop-up, then clicks the Update button
+    cy.get('.modal-content').should('be.visible');
+    this.selectOption('mirrorMode', option);
+
+    // Clicks update button and checks if the mode has been changed
+    cy.contains('button', 'Update').click();
+    cy.contains('.modal-dialog', 'Edit pool mirror mode').should('not.exist');
+    const val = option.toLowerCase(); // used since entries in table are lower case
+    this.getFirstTableCell(val).should('be.visible');
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts
new file mode 100644 (file)
index 0000000..fda5682
--- /dev/null
@@ -0,0 +1,66 @@
+import { ConfigurationPageHelper } from './configuration.po';
+
+describe('Configuration page', () => {
+  const configuration = new ConfigurationPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    configuration.navigateTo();
+  });
+
+  describe('breadcrumb test', () => {
+    it('should open and show breadcrumb', () => {
+      configuration.expectBreadcrumbText('Configuration');
+    });
+  });
+
+  describe('fields check', () => {
+    beforeEach(() => {
+      configuration.getExpandCollapseElement().click();
+    });
+
+    it('should verify that selected footer increases when an entry is clicked', () => {
+      configuration.getTableSelectedCount().should('eq', 1);
+    });
+
+    it('should check that details table opens and tab is correct', () => {
+      configuration.getStatusTables().should('be.visible');
+      configuration.getTabsCount().should('eq', 1);
+      configuration.getTabText(0).should('eq', 'Details');
+    });
+  });
+
+  describe('edit configuration test', () => {
+    const configName = 'client_cache_size';
+
+    beforeEach(() => {
+      configuration.clearTableSearchInput();
+    });
+
+    after(() => {
+      configuration.configClear(configName);
+    });
+
+    it('should click and edit a configuration and results should appear in the table', () => {
+      configuration.edit(
+        configName,
+        ['global', '1'],
+        ['mon', '2'],
+        ['mgr', '3'],
+        ['osd', '4'],
+        ['mds', '5'],
+        ['client', '6']
+      );
+    });
+
+    it('should show only modified configurations', () => {
+      configuration.filterTable('Modified', 'yes');
+      configuration.getTableFoundCount().should('eq', 1);
+    });
+
+    it('should hide all modified configurations', () => {
+      configuration.filterTable('Modified', 'no');
+      configuration.getTableFoundCount().should('gt', 1);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.po.ts
new file mode 100644 (file)
index 0000000..131ad27
--- /dev/null
@@ -0,0 +1,87 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ConfigurationPageHelper extends PageHelper {
+  pages = {
+    index: { url: '#/configuration', id: 'cd-configuration' }
+  };
+
+  /**
+   * Clears out all the values in a config to reset before and after testing
+   * Does not work for configs with checkbox only, possible future PR
+   */
+  configClear(name: string) {
+    this.navigateTo();
+    const valList = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']; // Editable values
+
+    // Enter config setting name into filter box
+    this.seachTable(name);
+
+    // Selects config that we want to clear
+    this.getFirstTableCell(name).click(); // waits for config to be clickable and click
+    cy.contains('button', 'Edit').click(); // clicks button to edit
+
+    // Wait for the data to load
+    cy.contains('.card-header', `Edit ${name}`);
+
+    for (const i of valList) {
+      cy.get(`#${i}`).clear();
+    }
+    // Clicks save button and checks that values are not present for the selected config
+    cy.contains('button', 'Save').click();
+
+    // Enter config setting name into filter box
+    this.seachTable(name);
+
+    // Expand row
+    this.getExpandCollapseElement(name).click();
+
+    // Checks for visibility of details tab
+    this.getStatusTables().should('be.visible');
+
+    for (const i of valList) {
+      // Waits until values are not present in the details table
+      this.getStatusTables().should('not.contain.text', i + ':');
+    }
+  }
+
+  /**
+   * Clicks the designated config, then inputs the values passed into the edit function.
+   * Then checks if the edit is reflected in the config table.
+   * Takes in name of config and a list of tuples of values the user wants edited,
+   * each tuple having the desired value along with the number tehey want for that value.
+   * Ex: [global, '2'] is the global value with an input of 2
+   */
+  edit(name: string, ...values: [string, string][]) {
+
+    // Enter config setting name into filter box
+    this.seachTable(name);
+
+    // Selects config that we want to edit
+    this.getFirstTableCell(name).click(); // waits for config to be clickable and click
+    cy.contains('button', 'Edit').click(); // clicks button to edit
+
+    cy.contains('.card-header', `Edit ${name}`);
+
+    values.forEach((valtuple) => {
+      // Finds desired value based off given list
+      cy.get(`#${valtuple[0]}`).type(valtuple[1]); // of values and inserts the given number for the value
+    });
+
+    // Clicks save button then waits until the desired config is visible, clicks it,
+    // then checks that each desired value appears with the desired number
+    cy.contains('button', 'Save').click();
+
+    // Enter config setting name into filter box
+    this.seachTable(name);
+
+    // Checks for visibility of config in table
+    this.getExpandCollapseElement(name).should('be.visible').click();
+
+    // Clicks config
+    values.forEach((value) => {
+      // iterates through list of values and
+      // checks if the value appears in details with the correct number attatched
+      cy.contains('.table.table-striped.table-bordered', `${value[0]}\: ${value[1]}`);
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.e2e-spec.ts
new file mode 100644 (file)
index 0000000..2c8d132
--- /dev/null
@@ -0,0 +1,36 @@
+import { CrushMapPageHelper } from './crush-map.po';
+
+describe('CRUSH map page', () => {
+  const crushmap = new CrushMapPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    crushmap.navigateTo();
+  });
+
+  describe('breadcrumb test', () => {
+    it('should open and show breadcrumb', () => {
+      crushmap.expectBreadcrumbText('CRUSH map');
+    });
+  });
+
+  describe('fields check', () => {
+    it('should check that title & table appears', () => {
+      // Check that title (CRUSH map viewer) appears
+      crushmap.getPageTitle().should('equal', 'CRUSH map viewer');
+
+      // Check that title appears once OSD is clicked
+      crushmap.getCrushNode(1).click();
+
+      crushmap
+        .getLegends()
+        .invoke('text')
+        .then((legend) => {
+          crushmap.getCrushNode(1).should('have.text', legend);
+        });
+
+      // Check that table appears once OSD is clicked
+      crushmap.getDataTables().should('be.visible');
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.po.ts
new file mode 100644 (file)
index 0000000..a5d2d59
--- /dev/null
@@ -0,0 +1,13 @@
+import { PageHelper } from '../page-helper.po';
+
+export class CrushMapPageHelper extends PageHelper {
+  pages = { index: { url: '#/crush-map', id: 'cd-crushmap' } };
+
+  getPageTitle() {
+    return cy.get('cd-crushmap .card-header').text();
+  }
+
+  getCrushNode(idx: number) {
+    return cy.get('.node-name.type-osd').eq(idx);
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.e2e-spec.ts
new file mode 100644 (file)
index 0000000..045b18f
--- /dev/null
@@ -0,0 +1,38 @@
+import { HostsPageHelper } from './hosts.po';
+
+describe('Hosts page', () => {
+  const hosts = new HostsPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    hosts.navigateTo();
+  });
+
+  describe('breadcrumb and tab tests', () => {
+    it('should open and show breadcrumb', () => {
+      hosts.expectBreadcrumbText('Hosts');
+    });
+
+    it('should show two tabs', () => {
+      hosts.getTabsCount().should('eq', 2);
+    });
+
+    it('should show hosts list tab at first', () => {
+      hosts.getTabText(0).should('eq', 'Hosts List');
+    });
+
+    it('should show overall performance as a second tab', () => {
+      hosts.getTabText(1).should('eq', 'Overall Performance');
+    });
+  });
+
+  describe('services link test', () => {
+    it('should check at least one host is present', () => {
+      hosts.check_for_host();
+    });
+
+    it('should check services link(s) work for first host', () => {
+      hosts.check_services_links();
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts
new file mode 100644 (file)
index 0000000..d3e7540
--- /dev/null
@@ -0,0 +1,31 @@
+import { PageHelper } from '../page-helper.po';
+
+export class HostsPageHelper extends PageHelper {
+  pages = { index: { url: '#/hosts', id: 'cd-hosts' } };
+
+  check_for_host() {
+    this.getTableTotalCount().should('not.be.eq', 0);
+  }
+
+  // function that checks all services links work for first
+  // host in table
+  check_services_links() {
+    // check that text (links) is present in services box
+    let links_tested = 0;
+
+    cy.get('cd-hosts a.service-link')
+      .should('have.length.greaterThan', 0)
+      .then(($elems) => {
+        $elems.each((_i, $el) => {
+          // click link, check it worked by looking for changed breadcrumb,
+          // navigate back to hosts page, repeat until all links checked
+          cy.contains('a', $el.innerText).should('exist').click();
+          this.expectBreadcrumbText('Performance Counters');
+          this.navigateTo();
+          links_tested++;
+        });
+        // check if any links were actually tested
+        expect(links_tested).gt(0);
+      });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts
new file mode 100644 (file)
index 0000000..0e918fb
--- /dev/null
@@ -0,0 +1,73 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { ConfigurationPageHelper } from './configuration.po';
+import { LogsPageHelper } from './logs.po';
+
+describe('Logs page', () => {
+  const logs = new LogsPageHelper();
+  const pools = new PoolPageHelper();
+  const configuration = new ConfigurationPageHelper();
+
+  const poolname = 'e2e_logs_test_pool';
+  const configname = 'log_graylog_port';
+  const today = new Date();
+  let hour = today.getHours();
+  if (hour > 12) {
+    hour = hour - 12;
+  }
+  const minute = today.getMinutes();
+
+  beforeEach(() => {
+    cy.login();
+  });
+
+  describe('breadcrumb and tab tests', () => {
+    beforeEach(() => {
+      logs.navigateTo();
+    });
+
+    it('should open and show breadcrumb', () => {
+      logs.expectBreadcrumbText('Logs');
+    });
+
+    it('should show two tabs', () => {
+      logs.getTabsCount().should('eq', 2);
+    });
+
+    it('should show cluster logs tab at first', () => {
+      logs.getTabText(0).should('eq', 'Cluster Logs');
+    });
+
+    it('should show audit logs as a second tab', () => {
+      logs.getTabText(1).should('eq', 'Audit Logs');
+    });
+  });
+
+  describe('audit logs respond to pool creation and deletion test', () => {
+    it('should create pool and check audit logs reacted', () => {
+      pools.navigateTo('create');
+      pools.create(poolname, 8);
+      pools.navigateTo();
+      pools.exist(poolname, true);
+      logs.checkAuditForPoolFunction(poolname, 'create', hour, minute);
+    });
+
+    it('should delete pool and check audit logs reacted', () => {
+      pools.navigateTo();
+      pools.delete(poolname);
+      logs.checkAuditForPoolFunction(poolname, 'delete', hour, minute);
+    });
+  });
+
+  describe('audit logs respond to editing configuration setting test', () => {
+    it('should change config settings and check audit logs reacted', () => {
+      configuration.navigateTo();
+      configuration.edit(configname, ['global', '5']);
+
+      logs.navigateTo();
+      logs.checkAuditForConfigChange(configname, 'global', hour, minute);
+
+      configuration.navigateTo();
+      configuration.configClear(configname);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.po.ts
new file mode 100644 (file)
index 0000000..bf5776c
--- /dev/null
@@ -0,0 +1,70 @@
+import { PageHelper } from '../page-helper.po';
+
+export class LogsPageHelper extends PageHelper {
+  pages = {
+    index: { url: '#/logs', id: 'cd-logs' }
+  };
+
+  checkAuditForPoolFunction(poolname: string, poolfunction: string, hour: number, minute: number) {
+    this.navigateTo();
+
+    // sometimes the modal from deleting pool is still present at this point.
+    // This wait makes sure it isn't
+    cy.contains('.modal-dialog', 'Delete Pool').should('not.exist');
+
+    // go to audit logs tab
+    cy.contains('.nav-link', 'Audit Logs').click();
+
+    // Enter an earliest time so that no old messages with the same pool name show up
+    cy.get('.bs-timepicker-field').its(0).clear();
+
+    if (hour < 10) {
+      cy.get('.bs-timepicker-field').its(0).type('0');
+    }
+    cy.get('.bs-timepicker-field').its(0).type(`${hour}`);
+
+    cy.get('.bs-timepicker-field').its(1).clear();
+    if (minute < 10) {
+      cy.get('.bs-timepicker-field').its(1).type('0');
+    }
+    cy.get('.bs-timepicker-field').its(1).type(`${minute}`);
+
+    // Enter the pool name into the filter box
+    cy.get('input.form-control.ng-valid').first().clear().type(poolname);
+
+    cy.get('.tab-pane.active')
+      .get('.card-body')
+      .get('.message')
+      .should('contain.text', poolname)
+      .and('contain.text', `pool ${poolfunction}`);
+  }
+
+  checkAuditForConfigChange(configname: string, setting: string, hour: number, minute: number) {
+    this.navigateTo();
+
+    // go to audit logs tab
+    cy.contains('.nav-link', 'Audit Logs').click();
+
+    // Enter an earliest time so that no old messages with the same config name show up
+    cy.get('.bs-timepicker-field').its(0).clear();
+    if (hour < 10) {
+      cy.get('.bs-timepicker-field').its(0).type('0');
+    }
+    cy.get('.bs-timepicker-field').its(0).type(`${hour}`);
+
+    cy.get('.bs-timepicker-field').its(1).clear();
+    if (minute < 10) {
+      cy.get('.bs-timepicker-field').its(1).type('0');
+    }
+    cy.get('.bs-timepicker-field').its(1).type(`${minute}`);
+
+    // Enter the config name into the filter box
+    cy.get('input.form-control.ng-valid').first().clear().type(configname);
+
+    cy.get('.tab-pane.active')
+      .get('.card-body')
+      .get('.message')
+      .should('contain.text', configname)
+      .and('contain.text', setting);
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.e2e-spec.ts
new file mode 100644 (file)
index 0000000..18a1c5d
--- /dev/null
@@ -0,0 +1,43 @@
+import { ManagerModulesPageHelper } from './mgr-modules.po';
+
+describe('Manager modules page', () => {
+  const mgrmodules = new ManagerModulesPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    mgrmodules.navigateTo();
+  });
+
+  describe('breadcrumb test', () => {
+    it('should open and show breadcrumb', () => {
+      mgrmodules.expectBreadcrumbText('Manager modules');
+    });
+  });
+
+  describe('verifies editing functionality for manager modules', () => {
+    it('should test editing on diskprediction_local module', () => {
+      const diskpredLocalArr = [
+        ['11', 'predict_interval'],
+        ['0122', 'sleep_interval']
+      ];
+      mgrmodules.editMgrModule('diskprediction_local', diskpredLocalArr);
+    });
+
+    it('should test editing on balancer module', () => {
+      const balancerArr = [['rq', 'pool_ids']];
+      mgrmodules.editMgrModule('balancer', balancerArr);
+    });
+
+    it('should test editing on dashboard module', () => {
+      const dashboardArr = [
+        ['rq', 'RGW_API_USER_ID'],
+        ['rafa', 'GRAFANA_API_PASSWORD']
+      ];
+      mgrmodules.editMgrModule('dashboard', dashboardArr);
+    });
+
+    it('should test editing on devicehealth module', () => {
+      mgrmodules.editDevicehealth('1987', 'sox', '1999', '2020', '456', '567');
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.po.ts
new file mode 100644 (file)
index 0000000..eaf93f9
--- /dev/null
@@ -0,0 +1,120 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ManagerModulesPageHelper extends PageHelper {
+  pages = { index: { url: '#/mgr-modules', id: 'cd-mgr-module-list' } };
+
+  /**
+   * Selects the Manager Module and then fills in the desired fields.
+   * Doesn't check/uncheck boxes because it is not reflected in the details table.
+   * DOES NOT WORK FOR ALL MGR MODULES, for example, Device health
+   */
+  editMgrModule(name: string, tuple: string[][]) {
+    this.getFirstTableCell(name).click();
+    cy.contains('button', 'Edit').click();
+
+    for (const entry of tuple) {
+      // Clears fields and adds edits
+      cy.get(`#${entry[1]}`).clear().type(entry[0]);
+    }
+
+    cy.contains('button', 'Update').click();
+    // Checks if edits appear
+    this.getExpandCollapseElement(name).should('be.visible').click();
+    for (const entry of tuple) {
+      cy.get('.datatable-body').last().contains(entry[0]);
+    }
+
+    // Clear mgr module of all edits made to it
+    this.getFirstTableCell(name).click();
+    cy.contains('button', 'Edit').click();
+
+    // Clears the editable fields
+    for (const entry of tuple) {
+      cy.get(`#${entry[1]}`).clear();
+    }
+
+    // Checks that clearing represents in details tab of module
+    cy.contains('button', 'Update').click();
+    this.getExpandCollapseElement(name).should('be.visible').click();
+    for (const entry of tuple) {
+      cy.get('.datatable-body').eq(1).should('contain', entry[1]).and('not.contain', entry[0]);
+    }
+  }
+
+  /**
+   * Selects the Devicehealth manager module, then fills in the desired fields,
+   * including all fields except checkboxes.
+   * Then checks if these edits appear in the details table.
+   */
+  editDevicehealth(
+    threshhold?: string,
+    pooln?: string,
+    retention?: string,
+    scrape?: string,
+    sleep?: string,
+    warn?: string
+  ) {
+    let devHealthArray: [string, string][];
+    devHealthArray = [
+      [threshhold, 'mark_out_threshold'],
+      [pooln, 'pool_name'],
+      [retention, 'retention_period'],
+      [scrape, 'scrape_frequency'],
+      [sleep, 'sleep_interval'],
+      [warn, 'warn_threshold']
+    ];
+
+    this.getFirstTableCell('devicehealth').click();
+    cy.contains('button', 'Edit').click();
+    for (let i = 0, devHealthTuple; (devHealthTuple = devHealthArray[i]); i++) {
+      if (devHealthTuple[0] !== undefined) {
+        // Clears and inputs edits
+        cy.get(`#${devHealthTuple[1]}`).type(devHealthTuple[0]);
+      }
+    }
+
+    cy.contains('button', 'Update').click();
+    this.getFirstTableCell('devicehealth').should('be.visible');
+    // Checks for visibility of devicehealth in table
+    this.getExpandCollapseElement('devicehealth').click();
+    for (let i = 0, devHealthTuple: [string, string]; (devHealthTuple = devHealthArray[i]); i++) {
+      if (devHealthTuple[0] !== undefined) {
+        // Repeatedly reclicks the module to check if edits has been done
+        cy.contains('.datatable-body-cell-label', 'devicehealth').click();
+        cy.get('.datatable-body').last().contains(devHealthTuple[0]).should('be.visible');
+      }
+    }
+
+    // Inputs old values into devicehealth fields. This manager module doesn't allow for updates
+    // to be made when the values are cleared. Therefore, I restored them to their original values
+    // (on my local run of ceph-dev, this is subject to change i would assume).
+    // I'd imagine there is a better way of doing this.
+    this.getFirstTableCell('devicehealth').click();
+    cy.contains('button', 'Edit').click();
+    cy.get('#mark_out_threshold').clear().type('2419200');
+
+    cy.get('#pool_name').clear().type('device_health_metrics');
+
+    cy.get('#retention_period').clear().type('15552000');
+
+    cy.get('#scrape_frequency').clear().type('86400');
+
+    cy.get('#sleep_interval').clear().type('600');
+
+    cy.get('#warn_threshold').clear().type('7257600');
+
+    // Checks that clearing represents in details tab
+    cy.contains('button', 'Update').click();
+    this.getExpandCollapseElement('devicehealth').should('be.visible').click();
+    for (let i = 0, devHealthTuple: [string, string]; (devHealthTuple = devHealthArray[i]); i++) {
+      if (devHealthTuple[0] !== undefined) {
+        // Repeatedly reclicks the module to check if clearing has been done
+        cy.contains('.datatable-body-cell-label', 'devicehealth').click();
+        cy.get('.datatable-body')
+          .eq(1)
+          .should('contain', devHealthTuple[1])
+          .and('not.contain', devHealthTuple[0]);
+      }
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.e2e-spec.ts
new file mode 100644 (file)
index 0000000..8324ff8
--- /dev/null
@@ -0,0 +1,61 @@
+import { MonitorsPageHelper } from './monitors.po';
+
+describe('Monitors page', () => {
+  const monitors = new MonitorsPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    monitors.navigateTo();
+  });
+
+  describe('breadcrumb test', () => {
+    it('should open and show breadcrumb', () => {
+      monitors.expectBreadcrumbText('Monitors');
+    });
+  });
+
+  describe('fields check', () => {
+    it('should check status table is present', () => {
+      // check for table header 'Status'
+      monitors.getLegends().its(0).should('have.text', 'Status');
+
+      // check for fields in table
+      monitors
+        .getStatusTables()
+        .should('contain.text', 'Cluster ID')
+        .and('contain.text', 'monmap modified')
+        .and('contain.text', 'monmap epoch')
+        .and('contain.text', 'quorum con')
+        .and('contain.text', 'quorum mon')
+        .and('contain.text', 'required con')
+        .and('contain.text', 'required mon');
+    });
+
+    it('should check In Quorum and Not In Quorum tables are present', () => {
+      // check for there to be two tables
+      monitors.getDataTables().should('have.length', 2);
+
+      // check for table header 'In Quorum'
+      monitors.getLegends().its(1).should('have.text', 'In Quorum');
+
+      // check for table header 'Not In Quorum'
+      monitors.getLegends().its(2).should('have.text', 'Not In Quorum');
+
+      // verify correct columns on In Quorum table
+      monitors.getDataTableHeaders(0).contains('Name');
+
+      monitors.getDataTableHeaders(0).contains('Rank');
+
+      monitors.getDataTableHeaders(0).contains('Public Address');
+
+      monitors.getDataTableHeaders(0).contains('Open Sessions');
+
+      // verify correct columns on Not In Quorum table
+      monitors.getDataTableHeaders(1).contains('Name');
+
+      monitors.getDataTableHeaders(1).contains('Rank');
+
+      monitors.getDataTableHeaders(1).contains('Public Address');
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.po.ts
new file mode 100644 (file)
index 0000000..4113b99
--- /dev/null
@@ -0,0 +1,7 @@
+import { PageHelper } from '../page-helper.po';
+
+export class MonitorsPageHelper extends PageHelper {
+  pages = {
+    index: { url: '#/monitor', id: 'cd-monitor' }
+  };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts
new file mode 100644 (file)
index 0000000..15bc30f
--- /dev/null
@@ -0,0 +1,61 @@
+import { OSDsPageHelper } from './osds.po';
+
+describe('OSDs page', () => {
+  const osds = new OSDsPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    osds.navigateTo();
+  });
+
+  describe('breadcrumb and tab tests', () => {
+    it('should open and show breadcrumb', () => {
+      osds.expectBreadcrumbText('OSDs');
+    });
+
+    it('should show two tabs', () => {
+      osds.getTabsCount().should('eq', 2);
+      osds.getTabText(0).should('eq', 'OSDs List');
+      osds.getTabText(1).should('eq', 'Overall Performance');
+    });
+  });
+
+  describe('check existence of fields on OSD page', () => {
+    it('should check that number of rows and count in footer match', () => {
+      osds.getTableTotalCount().then((text) => {
+        osds.getTableRows().its('length').should('equal', text);
+      });
+    });
+
+    it('should verify that buttons exist', () => {
+      cy.contains('button', 'Create');
+      cy.contains('button', 'Cluster-wide configuration');
+    });
+
+    describe('by selecting one row in OSDs List', () => {
+      beforeEach(() => {
+        osds.getExpandCollapseElement().click();
+      });
+
+      it('should verify that selected footer increases', () => {
+        osds.getTableSelectedCount().should('equal', 1);
+      });
+
+      it('should show the correct text for the tab labels', () => {
+        cy.get('#tabset-osd-details > div > tab').then(($tabs) => {
+          const tabHeadings = $tabs.map((_i, e) => e.getAttribute('heading')).get();
+
+          expect(tabHeadings).to.eql([
+            'Devices',
+            'Attributes (OSD map)',
+            'Metadata',
+            'Device health',
+            'Performance counter',
+            'Histogram',
+            'Performance Details'
+          ]);
+        });
+      });
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts
new file mode 100644 (file)
index 0000000..36e0b4f
--- /dev/null
@@ -0,0 +1,5 @@
+import { PageHelper } from '../page-helper.po';
+
+export class OSDsPageHelper extends PageHelper {
+  pages = { index: { url: '#/osd', id: 'cd-osd-list' } };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.e2e-spec.ts
new file mode 100644 (file)
index 0000000..63f5991
--- /dev/null
@@ -0,0 +1,16 @@
+import { FilesystemsPageHelper } from './filesystems.po';
+
+describe('Filesystems page', () => {
+  const filesystems = new FilesystemsPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    filesystems.navigateTo();
+  });
+
+  describe('breadcrumb test', () => {
+    it('should open and show breadcrumb', () => {
+      filesystems.expectBreadcrumbText('Filesystems');
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.po.ts
new file mode 100644 (file)
index 0000000..bd6e5b8
--- /dev/null
@@ -0,0 +1,5 @@
+import { PageHelper } from '../page-helper.po';
+
+export class FilesystemsPageHelper extends PageHelper {
+  pages = { index: { url: '#/cephfs', id: 'cd-cephfs-list' } };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.e2e-spec.ts
new file mode 100644 (file)
index 0000000..0864a5e
--- /dev/null
@@ -0,0 +1,16 @@
+import { NfsPageHelper } from './nfs.po';
+
+describe('Nfs page', () => {
+  const nfs = new NfsPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    nfs.navigateTo();
+  });
+
+  describe('breadcrumb test', () => {
+    it('should open and show breadcrumb', () => {
+      nfs.expectBreadcrumbText('NFS');
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.po.ts
new file mode 100644 (file)
index 0000000..7dd482a
--- /dev/null
@@ -0,0 +1,5 @@
+import { PageHelper } from '../page-helper.po';
+
+export class NfsPageHelper extends PageHelper {
+  pages = { index: { url: '#/nfs', id: 'cd-nfs-501' } };
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts
new file mode 100644 (file)
index 0000000..cff33a7
--- /dev/null
@@ -0,0 +1,248 @@
+interface Page {
+  url: string;
+  id: string;
+}
+
+export abstract class PageHelper {
+  pages: Record<string, Page>;
+
+  /**
+   * 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: string): Function {
+    return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
+      const fn: Function = descriptor.value;
+      descriptor.value = function (...args: any) {
+        cy.location('hash').should((url) => {
+          expect(url).to.eq(
+            page,
+            `Method ${target.constructor.name}::${propertyKey} is supposed to be ` +
+              `run on path "${page}", but was run on URL "${url}"`
+          );
+        });
+        fn.apply(this, args);
+      };
+    };
+  }
+
+  /**
+   * Navigates to the given page or to index.
+   * Waits until the page component is loaded
+   */
+  navigateTo(name: string = null) {
+    name = name || 'index';
+    const page = this.pages[name];
+
+    cy.visit(page.url);
+    cy.get(page.id);
+  }
+
+  /**
+   * Navigates back and waits for the hash to change
+   */
+  navigateBack() {
+    cy.location('hash').then((hash) => {
+      cy.go('back');
+      cy.location('hash').should('not.be', hash);
+    });
+  }
+
+  /**
+   * Checks the active breadcrumb value.
+   */
+  expectBreadcrumbText(text: string) {
+    cy.get('.breadcrumb-item.active').should('have.text', text);
+  }
+
+  getTabText(index: number) {
+    return cy.get('.nav.nav-tabs li').its(index).text();
+  }
+
+  getTabsCount(): any {
+    return cy.get('.nav.nav-tabs li').its('length');
+  }
+
+  /**
+   * Helper method to select an option inside a select element.
+   * This method will also expect that the option was set.
+   * @param option The option text (not value) to be selected.
+   */
+  selectOption(selectionName: string, option: string) {
+    cy.get(`select[name=${selectionName}]`).select(option);
+    return this.expectSelectOption(selectionName, option);
+  }
+
+  /**
+   * Helper method to expect a set option inside a select element.
+   * @param option The selected option text (not value) that is to
+   *   be expected.
+   */
+  expectSelectOption(selectionName: string, option: string) {
+    return cy.get(`select[name=${selectionName}] option:checked`).contains(option);
+  }
+
+  getLegends() {
+    return cy.get('legend');
+  }
+
+  getToast() {
+    return cy.get('.ngx-toastr');
+  }
+
+  /**
+   * Waits for the table to load its data
+   * Should be used in all methods that access the datatable
+   */
+  private waitDataTableToLoad() {
+    cy.get('cd-table').should('exist');
+    cy.get('datatable-scroller, .empty-row');
+  }
+
+  getDataTables() {
+    this.waitDataTableToLoad();
+
+    return cy.get('cd-table .dataTables_wrapper');
+  }
+
+  getTableTotalCount() {
+    this.waitDataTableToLoad();
+
+    return cy.get('.datatable-footer-inner .page-count span').then(($elem) => {
+      const text = $elem
+        .filter((_i, e) => e.innerText.includes('total'))
+        .first()
+        .text();
+
+      return Number(text.match(/(\d+)\s+total/)[1]);
+    });
+  }
+
+  getTableSelectedCount() {
+    this.waitDataTableToLoad();
+
+    return cy.get('.datatable-footer-inner .page-count span').then(($elem) => {
+      const text = $elem
+        .filter((_i, e) => e.innerText.includes('selected'))
+        .first()
+        .text();
+
+      return Number(text.match(/(\d+)\s+selected/)[1]);
+    });
+  }
+
+  getTableFoundCount() {
+    this.waitDataTableToLoad();
+
+    return cy.get('.datatable-footer-inner .page-count span').then(($elem) => {
+      const text = $elem
+        .filter((_i, e) => e.innerText.includes('found'))
+        .first()
+        .text();
+
+      return Number(text.match(/(\d+)\s+found/)[1]);
+    });
+  }
+
+  getTableRow(content: string) {
+    this.waitDataTableToLoad();
+
+    this.seachTable(content);
+    return cy.contains('.datatable-body-row', content);
+  }
+
+  getTableRows() {
+    this.waitDataTableToLoad();
+
+    return cy.get('datatable-row-wrapper');
+  }
+
+  /**
+   * Returns the first table cell.
+   * Optionally, you can specify the content of the cell.
+   */
+  getFirstTableCell(content?: string) {
+    this.waitDataTableToLoad();
+
+    if (content) {
+      this.seachTable(content);
+      return cy.contains('.datatable-body-cell-label', content);
+    } else {
+      return cy.get('.datatable-body-cell-label').first();
+    }
+  }
+
+  getExpandCollapseElement(content?: string) {
+    this.waitDataTableToLoad();
+
+    if (content) {
+      return cy.contains('.datatable-body-row', content).find('.tc_expand-collapse');
+    } else {
+      return cy.get('.tc_expand-collapse').first();
+    }
+  }
+
+  /**
+   * Gets column headers of table
+   */
+  getDataTableHeaders(index = 0) {
+    this.waitDataTableToLoad();
+
+    return cy.get('.datatable-header').its(index).find('.datatable-header-cell-label');
+  }
+
+  /**
+   * Grabs striped tables
+   */
+  getStatusTables() {
+    return cy.get('.table.table-striped');
+  }
+
+  filterTable(name: string, option: string) {
+    this.waitDataTableToLoad();
+
+    cy.get('.tc_filter_name > a').click();
+    cy.contains(`.tc_filter_name .dropdown-item`, name).click();
+
+    cy.get('.tc_filter_option > a').click();
+    cy.contains(`.tc_filter_option .dropdown-item`, option).click();
+  }
+
+  seachTable(text: string) {
+    this.waitDataTableToLoad();
+
+    cy.get('cd-table .dataTables_paginate input').first().clear().type('10');
+    cy.get('cd-table .search input').first().clear().type(text);
+  }
+
+  clearTableSearchInput() {
+    this.waitDataTableToLoad();
+
+    return cy.get('cd-table .search button').click();
+  }
+
+  /**
+   * This is a generic method to delete table rows.
+   * It will select the first row that contains the provided name and delete it.
+   * After that it will wait until the row is no longer displayed.
+   */
+  delete(name: string) {
+    // Selects row
+    this.getFirstTableCell(name).click();
+
+    // Clicks on table Delete button
+    cy.get('.table-actions button.dropdown-toggle').first().click(); // open submenu
+    cy.get('li.delete a').click(); // click on "delete" menu item
+
+    // Confirms deletion
+    cy.get('.custom-control-label').click();
+    cy.contains('button', 'Delete').click();
+
+    // Wait for modal to close
+    cy.get('cd-modal').should('not.exist');
+
+    // Waits for item to be removed from table
+    this.getFirstTableCell(name).should('not.exist');
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.e2e-spec.ts
new file mode 100644 (file)
index 0000000..fd22175
--- /dev/null
@@ -0,0 +1,47 @@
+import { PoolPageHelper } from './pools.po';
+
+describe('Pools page', () => {
+  const pools = new PoolPageHelper();
+  const poolName = 'pool_e2e_pool/test';
+
+  beforeEach(() => {
+    cy.login();
+    pools.navigateTo();
+  });
+
+  describe('breadcrumb and tab tests', () => {
+    it('should open and show breadcrumb', () => {
+      pools.expectBreadcrumbText('Pools');
+    });
+
+    it('should show two tabs', () => {
+      pools.getTabsCount().should('equal', 2);
+    });
+
+    it('should show pools list tab at first', () => {
+      pools.getTabText(0).should('eq', 'Pools List');
+    });
+
+    it('should show overall performance as a second tab', () => {
+      pools.getTabText(1).should('eq', 'Overall Performance');
+    });
+  });
+
+  describe('Create, update and destroy', () => {
+    it('should create a pool', () => {
+      pools.exist(poolName, false);
+      pools.navigateTo('create');
+      pools.create(poolName, 8);
+      pools.exist(poolName, true);
+    });
+
+    it('should edit a pools placement group', () => {
+      pools.exist(poolName, true);
+      pools.edit_pool_pg(poolName, 32);
+    });
+
+    it('should delete a pool', () => {
+      pools.delete(poolName);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.po.ts
new file mode 100644 (file)
index 0000000..24f4d42
--- /dev/null
@@ -0,0 +1,59 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+  index: { url: '#/pool', id: 'cd-pool-list' },
+  create: { url: '#/pool/create', id: 'cd-pool-form' }
+};
+
+export class PoolPageHelper extends PageHelper {
+  pages = pages;
+
+  private isPowerOf2(n: number) {
+    // tslint:disable-next-line: no-bitwise
+    return expect((n & (n - 1)) === 0, `Placement groups ${n} are not a power of 2`).to.be.true;
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  exist(name: string, oughtToBePresent = true) {
+    const waitRule = oughtToBePresent ? 'be.visible' : 'not.exist';
+    this.getFirstTableCell(name).should(waitRule);
+  }
+
+  @PageHelper.restrictTo(pages.create.url)
+  create(name: string, placement_groups: number, ...apps: string[]) {
+    cy.get('input[name=name]').clear().type(name);
+
+    this.isPowerOf2(placement_groups);
+
+    this.selectOption('poolType', 'replicated');
+
+    this.expectSelectOption('pgAutoscaleMode', 'on');
+    this.selectOption('pgAutoscaleMode', 'off'); // To show pgNum field
+    cy.get('input[name=pgNum]').clear().type(`${placement_groups}`);
+    this.setApplications(apps);
+    cy.get('cd-submit-button').click();
+  }
+
+  edit_pool_pg(name: string, new_pg: number, wait = true) {
+    this.isPowerOf2(new_pg);
+    this.getFirstTableCell(name).click(); // select pool from the table
+    cy.contains('button', 'Edit').click(); // click edit button
+    this.expectBreadcrumbText('Edit'); // verify we are now on edit page
+    cy.get('input[name=pgNum]').clear().type(`${new_pg}`);
+    cy.get('cd-submit-button').click();
+    const str = `${new_pg} active+clean`;
+    this.getTableRow(name);
+    if (wait) {
+      this.getTableRow(name).contains(str);
+    }
+  }
+
+  private setApplications(apps: string[]) {
+    if (!apps || apps.length === 0) {
+      return;
+    }
+    cy.get('.float-left.mr-2.select-menu-edit').click();
+    cy.get('.popover-content.popover-body').should('be.visible');
+    apps.forEach((app) => cy.get('.select-menu-item-content').contains(app).click());
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.e2e-spec.ts
new file mode 100644 (file)
index 0000000..e5e0daa
--- /dev/null
@@ -0,0 +1,56 @@
+import { BucketsPageHelper } from './buckets.po';
+
+describe('RGW buckets page', () => {
+  const buckets = new BucketsPageHelper();
+  const bucket_name = 'e2ebucket';
+
+  beforeEach(() => {
+    cy.login();
+    buckets.navigateTo();
+  });
+
+  describe('breadcrumb tests', () => {
+    it('should open and show breadcrumb', () => {
+      buckets.expectBreadcrumbText('Buckets');
+    });
+  });
+
+  describe('create, edit & delete bucket tests', () => {
+    it('should create bucket', () => {
+      buckets.navigateTo('create');
+      buckets.create(
+        bucket_name,
+        '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
+        'default-placement'
+      );
+      buckets.getFirstTableCell(bucket_name).should('exist');
+    });
+
+    it('should edit bucket', () => {
+      buckets.edit(bucket_name, 'dev');
+      buckets.getDataTables().should('contain.text', 'dev');
+    });
+
+    it('should delete bucket', () => {
+      buckets.delete(bucket_name);
+    });
+  });
+
+  describe('Invalid Input in Create and Edit tests', () => {
+    it('should test invalid inputs in create fields', () => {
+      buckets.testInvalidCreate();
+    });
+
+    it('should test invalid input in edit owner field', () => {
+      buckets.navigateTo('create');
+      buckets.create(
+        bucket_name,
+        '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
+        'default-placement'
+      );
+      buckets.testInvalidEdit(bucket_name);
+      buckets.navigateTo();
+      buckets.delete(bucket_name);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.po.ts
new file mode 100644 (file)
index 0000000..7d1f6e5
--- /dev/null
@@ -0,0 +1,162 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+  index: { url: '#/rgw/bucket', id: 'cd-rgw-bucket-list' },
+  create: { url: '#/rgw/bucket/create', id: 'cd-rgw-bucket-form' }
+};
+
+export class BucketsPageHelper extends PageHelper {
+  pages = pages;
+
+  versioningStateEnabled = 'Enabled';
+  versioningStateSuspended = 'Suspended';
+
+  private selectOwner(owner: string) {
+    return this.selectOption('owner', owner);
+  }
+
+  private selectPlacementTarget(placementTarget: string) {
+    return this.selectOption('placement-target', placementTarget);
+  }
+
+  @PageHelper.restrictTo(pages.create.url)
+  create(name: string, owner: string, placementTarget: string) {
+    // Enter in bucket name
+    cy.get('#bid').type(name);
+
+    // Select bucket owner
+    this.selectOwner(owner);
+    cy.get('#owner').should('have.class', 'ng-valid');
+
+    // Select bucket placement target:
+    this.selectPlacementTarget(placementTarget);
+    cy.get('#placement-target').should('have.class', 'ng-valid');
+
+    // Click the create button and wait for bucket to be made
+    cy.contains('button', 'Create Bucket').click();
+
+    this.getFirstTableCell(name).should('exist');
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  edit(name: string, new_owner: string) {
+    this.getFirstTableCell(name).click(); // wait for table to load and click
+    cy.contains('button', 'Edit').click(); // click button to move to edit page
+    this.expectBreadcrumbText('Edit');
+    cy.get('input[name=placement-target]').should('have.value', 'default-placement');
+    this.selectOwner(new_owner);
+
+    // Enable versioning
+    cy.get('input[id=versioning]').should('not.be.checked');
+    cy.get('label[for=versioning]').click();
+    cy.get('input[id=versioning]').should('be.checked');
+
+    cy.contains('button', 'Edit Bucket').click();
+
+    // wait to be back on buckets page with table visible and click
+    this.getExpandCollapseElement(name).click();
+
+    // check its details table for edited owner field
+    cy.get('.table.table-striped.table-bordered')
+      .first()
+      .should('contains.text', new_owner)
+      .as('bucketDataTable');
+
+    // Check versioning enabled:
+    cy.get('@bucketDataTable').find('tr').its(2).find('td').last().should('have.text', new_owner);
+    cy.get('@bucketDataTable').find('tr').its(11).find('td').last().as('versioningValueCell');
+
+    cy.get('@versioningValueCell').should('have.text', this.versioningStateEnabled);
+
+    // Disable versioning:
+    cy.contains('button', 'Edit').click(); // click button to move to edit page
+    this.expectBreadcrumbText('Edit');
+    cy.get('label[for=versioning]').click();
+    cy.get('input[id=versioning]').should('not.be.checked');
+    cy.contains('button', 'Edit Bucket').click();
+
+    // Check versioning suspended:
+    this.getExpandCollapseElement(name).click();
+
+    return cy.get('@versioningValueCell').should('have.text', this.versioningStateSuspended);
+  }
+
+  testInvalidCreate() {
+    this.navigateTo('create');
+    cy.get('#bid').as('nameInputField'); // Grabs name box field
+
+    // Gives an invalid name (too short), then waits for dashboard to determine validity
+    cy.get('@nameInputField').type('rq');
+
+    cy.contains('button', 'Create Bucket').click(); // To trigger a validation
+
+    // Waiting for website to decide if name is valid or not
+    // Check that name input field was marked invalid in the css
+    cy.get('@nameInputField')
+      .should('not.have.class', 'ng-pending')
+      .and('have.class', 'ng-invalid');
+
+    // Check that error message was printed under name input field
+    cy.get('#bid + .invalid-feedback').should('have.text', 'The value is not valid.');
+
+    // Test invalid owner input
+    // select some valid option. The owner drop down error message will not appear unless a valid user was selected at
+    // one point before the invalid placeholder user is selected.
+    this.selectOwner('dev');
+
+    // select the first option, which is invalid because it is a placeholder
+    this.selectOwner('-- Select a user --');
+
+    cy.get('@nameInputField').click();
+
+    // Check that owner drop down field was marked invalid in the css
+    cy.get('#owner').should('have.class', 'ng-invalid');
+
+    // Check that error message was printed under owner drop down field
+    cy.get('#owner + .invalid-feedback').should('have.text', 'This field is required.');
+
+    // Check invalid placement target input
+    this.selectOwner('dev');
+    // The drop down error message will not appear unless a valid option is previsously selected.
+    this.selectPlacementTarget('default-placement');
+    this.selectPlacementTarget('-- Select a placement target --');
+    cy.get('@nameInputField').click(); // Trigger validation
+    cy.get('#placement-target').should('have.class', 'ng-invalid');
+    cy.get('#placement-target + .invalid-feedback').should('have.text', 'This field is required.');
+
+    // Clicks the Create Bucket button but the page doesn't move.
+    // Done by testing for the breadcrumb
+    cy.contains('button', 'Create Bucket').click(); // Clicks Create Bucket button
+    this.expectBreadcrumbText('Create');
+    // content in fields seems to subsist through tests if not cleared, so it is cleared
+    cy.get('@nameInputField').clear();
+    return cy.contains('button', 'Cancel').click();
+  }
+
+  testInvalidEdit(name: string) {
+    this.navigateTo();
+
+    this.getFirstTableCell(name).click(); // wait for table to load and click
+    cy.contains('button', 'Edit').click(); // click button to move to edit page
+
+    this.expectBreadcrumbText('Edit');
+
+    cy.get('input[id=versioning]').should('exist').and('not.be.checked');
+
+    // Chooses 'Select a user' rather than a valid owner on Edit Bucket page
+    // and checks if it's an invalid input
+
+    // select the first option, which is invalid because it is a placeholder
+    this.selectOwner('-- Select a user --');
+
+    cy.contains('button', 'Edit Bucket').click();
+
+    // Check that owner drop down field was marked invalid in the css
+    cy.get('#owner').should('have.class', 'ng-invalid');
+
+    // Check that error message was printed under owner drop down field
+    cy.get('#owner + .invalid-feedback').should('have.text', 'This field is required.');
+
+    this.expectBreadcrumbText('Edit');
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.e2e-spec.ts
new file mode 100644 (file)
index 0000000..03b2ca8
--- /dev/null
@@ -0,0 +1,34 @@
+import { DaemonsPageHelper } from './daemons.po';
+
+describe('RGW daemons page', () => {
+  const daemons = new DaemonsPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    daemons.navigateTo();
+  });
+
+  describe('breadcrumb and tab tests', () => {
+    it('should open and show breadcrumb', () => {
+      daemons.expectBreadcrumbText('Daemons');
+    });
+
+    it('should show two tabs', () => {
+      daemons.getTabsCount().should('eq', 2);
+    });
+
+    it('should show daemons list tab at first', () => {
+      daemons.getTabText(0).should('eq', 'Daemons List');
+    });
+
+    it('should show overall performance as a second tab', () => {
+      daemons.getTabText(1).should('eq', 'Overall Performance');
+    });
+  });
+
+  describe('details and performance counters table tests', () => {
+    it('should check that details/performance tables are visible when daemon is selected', () => {
+      daemons.checkTables();
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.po.ts
new file mode 100644 (file)
index 0000000..0ca6066
--- /dev/null
@@ -0,0 +1,42 @@
+import { PageHelper } from '../page-helper.po';
+
+export class DaemonsPageHelper extends PageHelper {
+  pages = {
+    index: { url: '#/rgw/daemon', id: 'cd-rgw-daemon-list' }
+  };
+
+  getTableCell(tableIndex: number) {
+    return cy
+      .get('.tab-container')
+      .its(1)
+      .find('cd-table')
+      .its(tableIndex)
+      .find('datatable-body-cell');
+  }
+
+  checkTables() {
+    // click on a daemon so details table appears
+    cy.get('.datatable-body-cell-label').first().click();
+
+    // check details table is visible
+    // check at least one field is present
+    this.getTableCell(0).should('visible').should('contain.text', 'ceph_version');
+    // check performance counters table is not currently visible
+    this.getTableCell(1).should('not.be.visible');
+
+    // click on performance counters tab and check table is loaded
+    cy.contains('.nav-link', 'Performance Counters').click();
+
+    // check at least one field is present
+    this.getTableCell(1).should('be.visible').should('contain.text', 'objecter.op_r');
+    // check details table is not currently visible
+    this.getTableCell(0).should('not.be.visible');
+
+    // click on performance details tab
+    cy.contains('.nav-link', 'Performance Details').click();
+
+    // checks the other tabs' content isn't visible
+    this.getTableCell(0).should('not.be.visible');
+    this.getTableCell(1).should('not.be.visible');
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.e2e-spec.ts
new file mode 100644 (file)
index 0000000..a8d7d45
--- /dev/null
@@ -0,0 +1,43 @@
+import { UsersPageHelper } from './users.po';
+
+describe('RGW users page', () => {
+  const users = new UsersPageHelper();
+  const user_name = 'e2e_000user_create_edit_delete';
+
+  beforeEach(() => {
+    cy.login();
+    users.navigateTo();
+  });
+
+  describe('breadcrumb tests', () => {
+    it('should open and show breadcrumb', () => {
+      users.expectBreadcrumbText('Users');
+    });
+  });
+
+  describe('create, edit & delete user tests', () => {
+    it('should create user', () => {
+      users.navigateTo('create');
+      users.create(user_name, 'Some Name', 'original@website.com', '1200');
+      users.getFirstTableCell(user_name).should('exist');
+    });
+
+    it('should edit users full name, email and max buckets', () => {
+      users.edit(user_name, 'Another Identity', 'changed@othersite.com', '1969');
+    });
+
+    it('should delete user', () => {
+      users.delete(user_name);
+    });
+  });
+
+  describe('Invalid input tests', () => {
+    it('should put invalid input into user creation form and check fields are marked invalid', () => {
+      users.invalidCreate();
+    });
+
+    it('should put invalid input into user edit form and check fields are marked invalid', () => {
+      users.invalidEdit();
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.po.ts
new file mode 100644 (file)
index 0000000..66cff75
--- /dev/null
@@ -0,0 +1,139 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+  index: { url: '#/rgw/user', id: 'cd-rgw-user-list' },
+  create: { url: '#/rgw/user/create', id: 'cd-rgw-user-form' }
+};
+
+export class UsersPageHelper extends PageHelper {
+  pages = pages;
+
+  @PageHelper.restrictTo(pages.create.url)
+  create(username: string, fullname: string, email: string, maxbuckets: string) {
+    // Enter in  username
+    cy.get('#uid').type(username);
+
+    // Enter in full name
+    cy.get('#display_name').click().type(fullname);
+
+    // Enter in email
+    cy.get('#email').click().type(email);
+
+    // Enter max buckets
+    this.selectOption('max_buckets_mode', 'Custom');
+    cy.get('#max_buckets').click().clear().type(maxbuckets);
+
+    // Click the create button and wait for user to be made
+    cy.contains('button', 'Create User').click();
+    this.getFirstTableCell(username).should('exist');
+  }
+
+  @PageHelper.restrictTo(pages.index.url)
+  edit(name: string, new_fullname: string, new_email: string, new_maxbuckets: string) {
+    this.getFirstTableCell(name).click(); // wait for table to load and click
+    cy.contains('button', 'Edit').click(); // click button to move to edit page
+
+    this.expectBreadcrumbText('Edit');
+
+    // Change the full name field
+    cy.get('#display_name').click().clear().type(new_fullname);
+
+    // Change the email field
+    cy.get('#email').click().clear().type(new_email);
+
+    // Change the max buckets field
+    this.selectOption('max_buckets_mode', 'Custom');
+    cy.get('#max_buckets').click().clear().type(new_maxbuckets);
+
+    cy.contains('button', 'Edit User').click();
+
+    // Click the user and check its details table for updated content
+    this.getExpandCollapseElement(name).click();
+    cy.get('.active.tab-pane')
+      .should('contain.text', new_fullname)
+      .and('contain.text', new_email)
+      .and('contain.text', new_maxbuckets);
+  }
+
+  invalidCreate() {
+    const uname = '000invalid_create_user';
+    // creating this user in order to check that you can't give two users the same name
+    this.navigateTo('create');
+    this.create(uname, 'xxx', 'xxx@xxx', '1');
+
+    this.navigateTo('create');
+
+    // Username
+    cy.get('#uid')
+      // No username had been entered. Field should be invalid
+      .should('have.class', 'ng-invalid')
+      // Try to give user already taken name. Should make field invalid.
+      .type(uname)
+      .blur()
+      .should('have.class', 'ng-invalid');
+    cy.contains('#uid + .invalid-feedback', 'The chosen user ID is already in use.');
+
+    // check that username field is marked invalid if username has been cleared off
+    cy.get('#uid').clear().blur().should('have.class', 'ng-invalid');
+    cy.contains('#uid + .invalid-feedback', 'This field is required.');
+
+    // Full name
+    cy.get('#display_name')
+      // No display name has been given so field should be invalid
+      .should('have.class', 'ng-invalid')
+      // display name field should also be marked invalid if given input then emptied
+      .type('a')
+      .clear()
+      .blur()
+      .should('have.class', 'ng-invalid');
+    cy.contains('#display_name + .invalid-feedback', 'This field is required.');
+
+    // put invalid email to make field invalid
+    cy.get('#email').type('a').blur().should('have.class', 'ng-invalid');
+    cy.contains('#email + .invalid-feedback', 'This is not a valid email address.');
+
+    // put negative max buckets to make field invalid
+    this.expectSelectOption('max_buckets_mode', 'Custom');
+    cy.get('#max_buckets').clear().type('-5').blur().should('have.class', 'ng-invalid');
+    cy.contains('#max_buckets + .invalid-feedback', 'The entered value must be >= 1.');
+
+    this.navigateTo();
+    this.delete(uname);
+  }
+
+  invalidEdit() {
+    const uname = '000invalid_edit_user';
+    // creating this user to edit for the test
+    this.navigateTo('create');
+    this.create(uname, 'xxx', 'xxx@xxx', '1');
+
+    this.navigateTo();
+
+    // wait for table to load and click on the bucket you want to edit in the table
+    this.getFirstTableCell(uname).click();
+    cy.contains('button', 'Edit').click(); // click button to move to edit page
+
+    this.expectBreadcrumbText('Edit');
+
+    // put invalid email to make field invalid
+    cy.get('#email')
+      .clear()
+      .type('a')
+      .blur()
+      .should('not.have.class', 'ng-pending')
+      .should('have.class', 'ng-invalid');
+    cy.contains('#email + .invalid-feedback', 'This is not a valid email address.');
+
+    // empty the display name field making it invalid
+    cy.get('#display_name').clear().blur().should('have.class', 'ng-invalid');
+    cy.contains('#display_name + .invalid-feedback', 'This field is required.');
+
+    // put negative max buckets to make field invalid
+    this.expectSelectOption('max_buckets_mode', 'Custom');
+    cy.get('#max_buckets').clear().type('-5').blur().should('have.class', 'ng-invalid');
+    cy.contains('#max_buckets + .invalid-feedback', 'The entered value must be >= 1.');
+
+    this.navigateTo();
+    this.delete(uname);
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.e2e-spec.ts
new file mode 100644 (file)
index 0000000..f149a4b
--- /dev/null
@@ -0,0 +1,124 @@
+import { IscsiPageHelper } from '../block/iscsi.po';
+import { HostsPageHelper } from '../cluster/hosts.po';
+import { MonitorsPageHelper } from '../cluster/monitors.po';
+import { OSDsPageHelper } from '../cluster/osds.po';
+import { PageHelper } from '../page-helper.po';
+import { PoolPageHelper } from '../pools/pools.po';
+import { DaemonsPageHelper } from '../rgw/daemons.po';
+import { DashboardPageHelper } from './dashboard.po';
+
+describe('Dashboard Main Page', () => {
+  const dashboard = new DashboardPageHelper();
+  const daemons = new DaemonsPageHelper();
+  const hosts = new HostsPageHelper();
+  const osds = new OSDsPageHelper();
+  const pools = new PoolPageHelper();
+  const monitors = new MonitorsPageHelper();
+  const iscsi = new IscsiPageHelper();
+
+  beforeEach(() => {
+    cy.login();
+    dashboard.navigateTo();
+  });
+
+  describe('Check that all hyperlinks on info cards lead to the correct page and fields exist', () => {
+    it('should ensure that all linked info cards lead to correct page', () => {
+      const expectationMap = {
+        Monitors: 'Monitors',
+        OSDs: 'OSDs',
+        Hosts: 'Hosts',
+        'Object Gateways': 'Daemons',
+        'iSCSI Gateways': 'Overview',
+        Pools: 'Pools'
+      };
+
+      for (const [linkText, breadcrumbText] of Object.entries(expectationMap)) {
+        cy.location('hash').should('eq', '#/dashboard');
+        dashboard.clickInfoCardLink(linkText);
+        dashboard.expectBreadcrumbText(breadcrumbText);
+        dashboard.navigateBack();
+      }
+    });
+
+    it('should verify that info cards exist on dashboard in proper order', () => {
+      // Ensures that info cards are all displayed on the dashboard tab while being in the proper
+      // order, checks for card title and position via indexing into a list of all info cards.
+      const order = [
+        'Cluster Status',
+        'Monitors',
+        'OSDs',
+        'Manager Daemons',
+        'Hosts',
+        'Object Gateways',
+        'Metadata Servers',
+        'iSCSI Gateways',
+        'Client IOPS',
+        'Client Throughput',
+        'Client Read/Write',
+        'Recovery Throughput',
+        'Scrub',
+        'Pools',
+        'Raw Capacity',
+        'Objects',
+        'PGs per OSD',
+        'PG Status'
+      ];
+
+      for (let i = 0; i < order.length; i++) {
+        dashboard.infoCard(i).should('contain.text', order[i]);
+      }
+    });
+
+    it('should verify that info card group titles are present and in the right order', () => {
+      cy.location('hash').should('eq', '#/dashboard');
+      dashboard.infoGroupTitle(0).should('eq', 'Status');
+      dashboard.infoGroupTitle(1).should('eq', 'Performance');
+      dashboard.infoGroupTitle(2).should('eq', 'Capacity');
+    });
+  });
+
+  it('Should check that dashboard cards have correct information', () => {
+    interface TestSpec {
+      cardName: string;
+      regexMatcher?: RegExp;
+      pageObject: PageHelper;
+    }
+    const testSpecs: TestSpec[] = [
+      { cardName: 'Object Gateways', regexMatcher: /(\d+)\s+total/, pageObject: daemons },
+      { cardName: 'Monitors', regexMatcher: /(\d+)\s+\(quorum/, pageObject: monitors },
+      { cardName: 'Hosts', regexMatcher: /(\d+)\s+total/, pageObject: hosts },
+      { cardName: 'OSDs', regexMatcher: /(\d+)\s+total/, pageObject: osds },
+      { cardName: 'Pools', pageObject: pools },
+      { cardName: 'iSCSI Gateways', regexMatcher: /(\d+)\s+total/, pageObject: iscsi }
+    ];
+    for (let i = 0; i < testSpecs.length; i++) {
+      const spec = testSpecs[i];
+      dashboard.navigateTo();
+
+      dashboard.infoCardBodyText(spec.cardName).then((infoCardBodyText: string) => {
+        let dashCount = 0;
+
+        if (spec.regexMatcher) {
+          const match = infoCardBodyText.match(new RegExp(spec.regexMatcher));
+          expect(match).to.length.gt(
+            1,
+            `Regex ${spec.regexMatcher} did not find a match for card with name ` +
+              `${spec.cardName}`
+          );
+          dashCount = Number(match[1]);
+        } else {
+          dashCount = Number(infoCardBodyText);
+        }
+
+        spec.pageObject.navigateTo();
+        spec.pageObject.getTableTotalCount().then((tableCount) => {
+          expect(tableCount).to.eq(
+            dashCount,
+            `Text of card "${spec.cardName}" and regex "${spec.regexMatcher}" resulted in ${dashCount} ` +
+              `but did not match table count ${tableCount}`
+          );
+        });
+      });
+    }
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.po.ts
new file mode 100644 (file)
index 0000000..0212573
--- /dev/null
@@ -0,0 +1,27 @@
+import { PageHelper } from '../page-helper.po';
+
+export class DashboardPageHelper extends PageHelper {
+  pages = { index: { url: '#/dashboard', id: 'cd-dashboard' } };
+
+  infoGroupTitle(index: number) {
+    return cy.get('.info-group-title').its(index).text();
+  }
+
+  clickInfoCardLink(cardName: string) {
+    cy.get(`cd-info-card[cardtitle="${cardName}"]`).contains('a', cardName).click();
+  }
+
+  infoCard(indexOrTitle: number | string) {
+    cy.get('cd-info-card').as('infoCards');
+
+    if (typeof indexOrTitle === 'number') {
+      return cy.get('@infoCards').its(indexOrTitle);
+    } else {
+      return cy.contains('cd-info-card a', indexOrTitle).parent().parent().parent().parent();
+    }
+  }
+
+  infoCardBodyText(infoCard: string) {
+    return this.infoCard(infoCard).find('.card-text').text();
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.e2e-spec.ts
new file mode 100644 (file)
index 0000000..b69f26f
--- /dev/null
@@ -0,0 +1,56 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { NotificationSidebarPageHelper } from './notification.po';
+
+describe('Notification page', () => {
+  const notification = new NotificationSidebarPageHelper();
+  const pools = new PoolPageHelper();
+  const poolName = 'e2e_notification_pool';
+
+  before(() => {
+    cy.login();
+    pools.navigateTo('create');
+    pools.create(poolName, 8);
+    pools.edit_pool_pg(poolName, 4, false);
+  });
+
+  after(() => {
+    cy.login();
+    pools.navigateTo();
+    pools.delete(poolName);
+  });
+
+  beforeEach(() => {
+    cy.login();
+    pools.navigateTo();
+  });
+
+  it('should open notification sidebar', () => {
+    notification.getSidebar().should('not.be.visible');
+    notification.open();
+    notification.getSidebar().should('be.visible');
+  });
+
+  it('should display a running task', () => {
+    notification.getToast().should('not.exist');
+
+    // Check that running task is shown.
+    notification.open();
+    notification.getTasks().contains(poolName).should('exist');
+
+    // Delete pool after task is complete (otherwise we get an error).
+    notification.getTasks().contains(poolName, { timeout: 300000 }).should('not.exist');
+  });
+
+  it('should have notifications', () => {
+    notification.open();
+    notification.getNotifications().should('have.length.gt', 0);
+  });
+
+  it('should clear notifications', () => {
+    notification.getToast().should('not.exist');
+    notification.open();
+    notification.getNotifications().should('have.length.gt', 0);
+    notification.getClearNotficationsBtn().should('be.visible');
+    notification.clearNotifications();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.po.ts
new file mode 100644 (file)
index 0000000..12c424e
--- /dev/null
@@ -0,0 +1,45 @@
+import { PageHelper } from '../page-helper.po';
+
+export class NotificationSidebarPageHelper extends PageHelper {
+  getNotificatinoIcon() {
+    return cy.get('cd-notifications a');
+  }
+
+  getSidebar() {
+    return cy.get('cd-notifications-sidebar');
+  }
+
+  getTasks() {
+    return this.getSidebar().find('.card.tc_task');
+  }
+
+  getNotifications() {
+    return this.getSidebar().find('.card.tc_notification');
+  }
+
+  getClearNotficationsBtn() {
+    return this.getSidebar().find('button.btn-block');
+  }
+
+  getCloseBtn() {
+    return this.getSidebar().find('button.close');
+  }
+
+  open() {
+    this.getNotificatinoIcon().click();
+    this.getSidebar().should('be.visible');
+  }
+
+  clearNotifications() {
+    // It can happen that although notifications are cleared, by the time we check the notifications
+    // amount, another notification can appear, so we check it more than once (if needed).
+    this.getClearNotficationsBtn().click();
+    this.getNotifications()
+      .should('have.length.gte', 0)
+      .then(($elems) => {
+        if ($elems.length > 0) {
+          this.clearNotifications();
+        }
+      });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.e2e-spec.ts
new file mode 100644 (file)
index 0000000..7e76f16
--- /dev/null
@@ -0,0 +1,36 @@
+import { RoleMgmtPageHelper } from './role-mgmt.po';
+
+describe('Role Management page', () => {
+  const roleMgmt = new RoleMgmtPageHelper();
+  const role_name = 'e2e_role_mgmt_role';
+
+  beforeEach(() => {
+    cy.login();
+    roleMgmt.navigateTo();
+  });
+
+  describe('breadcrumb tests', () => {
+    it('should check breadcrumb on roles tab on user management page', () => {
+      roleMgmt.expectBreadcrumbText('Roles');
+    });
+
+    it('should check breadcrumb on role creation page', () => {
+      roleMgmt.navigateTo('create');
+      roleMgmt.expectBreadcrumbText('Create');
+    });
+  });
+
+  describe('role create, edit & delete test', () => {
+    it('should create a role', () => {
+      roleMgmt.create(role_name, 'An interesting description');
+    });
+
+    it('should edit a role', () => {
+      roleMgmt.edit(role_name, 'A far more interesting description');
+    });
+
+    it('should delete a role', () => {
+      roleMgmt.delete(role_name);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.po.ts
new file mode 100644 (file)
index 0000000..b90da23
--- /dev/null
@@ -0,0 +1,35 @@
+import { PageHelper } from '../page-helper.po';
+
+export class RoleMgmtPageHelper extends PageHelper {
+  pages = {
+    index: { url: '#/user-management/roles', id: 'cd-role-list' },
+    create: { url: '#/user-management/roles/create', id: 'cd-role-form' }
+  };
+
+  create(name: string, description: string) {
+    this.navigateTo('create');
+
+    // fill in fields
+    cy.get('#name').type(name);
+    cy.get('#description').type(description);
+
+    // Click the create button and wait for role to be made
+    cy.contains('button', 'Create Role').click();
+
+    this.getFirstTableCell(name).should('exist');
+  }
+
+  edit(name: string, description: string) {
+    this.getFirstTableCell(name).click(); // select role from table
+    cy.contains('button', 'Edit').click(); // click button to move to edit page
+
+    // fill in fields with new values
+    cy.get('#description').clear().type(description);
+
+    // Click the edit button and check new values are present in table
+    cy.contains('button', 'Edit Role').click();
+
+    this.getFirstTableCell(name).should('exist');
+    this.getFirstTableCell(description).should('exist');
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.e2e-spec.ts
new file mode 100644 (file)
index 0000000..57818db
--- /dev/null
@@ -0,0 +1,36 @@
+import { UserMgmtPageHelper } from './user-mgmt.po';
+
+describe('User Management page', () => {
+  const userMgmt = new UserMgmtPageHelper();
+  const user_name = 'e2e_user_mgmt_user';
+
+  beforeEach(() => {
+    cy.login();
+    userMgmt.navigateTo();
+  });
+
+  describe('breadcrumb tests', () => {
+    it('should check breadcrumb on users tab of user management page', () => {
+      userMgmt.expectBreadcrumbText('Users');
+    });
+
+    it('should check breadcrumb on user creation page', () => {
+      userMgmt.navigateTo('create');
+      userMgmt.expectBreadcrumbText('Create');
+    });
+  });
+
+  describe('user create, edit & delete test', () => {
+    it('should create a user', () => {
+      userMgmt.create(user_name, 'cool_password', 'Jeff', 'realemail@realwebsite.com');
+    });
+
+    it('should edit a user', () => {
+      userMgmt.edit(user_name, 'cool_password_number_2', 'Geoff', 'w@m');
+    });
+
+    it('should delete a user', () => {
+      userMgmt.delete(user_name);
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.po.ts
new file mode 100644 (file)
index 0000000..904cc6e
--- /dev/null
@@ -0,0 +1,40 @@
+import { PageHelper } from '../page-helper.po';
+
+export class UserMgmtPageHelper extends PageHelper {
+  pages = {
+    index: { url: '#/user-management/users', id: 'cd-user-list' },
+    create: { url: '#/user-management/users/create', id: 'cd-user-form' }
+  };
+
+  create(username: string, password: string, name: string, email: string) {
+    this.navigateTo('create');
+
+    // fill in fields
+    cy.get('#username').type(username);
+    cy.get('#password').type(password);
+    cy.get('#confirmpassword').type(password);
+    cy.get('#name').type(name);
+    cy.get('#email').type(email);
+
+    // Click the create button and wait for user to be made
+    cy.contains('button', 'Create User').click();
+    this.getFirstTableCell(username).should('exist');
+  }
+
+  edit(username: string, password: string, name: string, email: string) {
+    this.getFirstTableCell(username).click(); // select user from table
+    cy.contains('button', 'Edit').click(); // click button to move to edit page
+
+    // fill in fields with new values
+    cy.get('#password').clear().type(password);
+    cy.get('#confirmpassword').clear().type(password);
+    cy.get('#name').clear().type(name);
+    cy.get('#email').clear().type(email);
+
+    // Click the edit button and check new values are present in table
+    const editButton = cy.contains('button', 'Edit User');
+    editButton.click();
+    this.getFirstTableCell(email).should('exist');
+    this.getFirstTableCell(name).should('exist');
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts b/src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts
new file mode 100644 (file)
index 0000000..0bcfe76
--- /dev/null
@@ -0,0 +1,47 @@
+declare global {
+  namespace Cypress {
+    interface Chainable<Subject> {
+      login(): void;
+      text(): Chainable<string>;
+    }
+  }
+}
+
+import { Permissions } from '../../src/app/shared/models/permissions';
+
+let auth: any;
+
+const fillAuth = () => {
+  window.localStorage.setItem('dashboard_username', auth.username);
+  window.localStorage.setItem('access_token', auth.token);
+  window.localStorage.setItem('dashboard_permissions', auth.permissions);
+  window.localStorage.setItem('user_pwd_expiration_date', auth.pwdExpirationDate);
+  window.localStorage.setItem('user_pwd_update_required', auth.pwdUpdateRequired);
+  window.localStorage.setItem('sso', auth.sso);
+};
+
+Cypress.Commands.add('login', () => {
+  const username = Cypress.env('LOGIN_USER') || 'admin';
+  const password = Cypress.env('LOGIN_PWD') || 'admin';
+
+  if (auth === undefined) {
+    cy.request({
+      method: 'POST',
+      url: 'api/auth',
+      body: { username: username, password: password }
+    }).then((resp) => {
+      auth = resp.body;
+      auth.permissions = JSON.stringify(new Permissions(auth.permissions));
+      auth.pwdExpirationDate = String(auth.pwdExpirationDate);
+      auth.pwdUpdateRequired = String(auth.pwdUpdateRequired);
+      auth.sso = String(auth.sso);
+      fillAuth();
+    });
+  } else {
+    fillAuth();
+  }
+});
+
+Cypress.Commands.add('text', { prevSubject: true }, (subject) => {
+  return subject.text();
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/support/index.ts b/src/pybind/mgr/dashboard/frontend/cypress/support/index.ts
new file mode 100644 (file)
index 0000000..750acb7
--- /dev/null
@@ -0,0 +1,5 @@
+import './commands';
+
+afterEach(() => {
+  cy.visit('#/403');
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json b/src/pybind/mgr/dashboard/frontend/cypress/tsconfig.json
new file mode 100644 (file)
index 0000000..681a8b3
--- /dev/null
@@ -0,0 +1,13 @@
+{
+  "extends": "../tsconfig.json",
+  "exclude": [],
+  "include": [
+    "**/*.ts"
+  ],
+  "compilerOptions": {
+    "types": [
+      "cypress"
+    ],
+    "target": "es6"
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/block/images.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/block/images.e2e-spec.ts
deleted file mode 100644 (file)
index 1d807e7..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-import { PoolPageHelper } from '../pools/pools.po';
-import { ImagesPageHelper } from './images.po';
-
-describe('Images page', () => {
-  let pools: PoolPageHelper;
-  let images: ImagesPageHelper;
-
-  beforeAll(() => {
-    images = new ImagesPageHelper();
-    pools = new PoolPageHelper();
-  });
-
-  afterEach(async () => {
-    await ImagesPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb and tab tests', () => {
-    beforeAll(async () => {
-      await images.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await images.waitTextToBePresent(images.getBreadcrumb(), 'Images');
-    });
-
-    it('should show four tabs', async () => {
-      await expect(images.getTabsCount()).toEqual(4);
-    });
-
-    it('should show text for all tabs', async () => {
-      await expect(images.getTabText(0)).toEqual('Images');
-      await expect(images.getTabText(1)).toEqual('Namespaces');
-      await expect(images.getTabText(2)).toEqual('Trash');
-      await expect(images.getTabText(3)).toEqual('Overall Performance');
-    });
-  });
-
-  describe('create, edit & delete image test', () => {
-    const poolName = 'e2e_images_pool';
-    const imageName = 'e2e_images#image';
-    const newImageName = 'e2e_images#image_new';
-
-    beforeAll(async () => {
-      await pools.navigateTo('create'); // Need pool for image testing
-      await pools.create(poolName, 8, 'rbd');
-      await pools.navigateTo();
-      await pools.exist(poolName, true);
-      await images.navigateTo();
-    });
-
-    it('should create image', async () => {
-      await images.createImage(imageName, poolName, '1');
-      await expect(images.getFirstTableCellWithText(imageName).isPresent()).toBe(true);
-    });
-
-    it('should edit image', async () => {
-      await images.editImage(imageName, poolName, newImageName, '2');
-      await expect(images.getFirstTableCellWithText(newImageName).isPresent()).toBe(true);
-    });
-
-    it('should delete image', async () => {
-      await images.navigateTo();
-      await images.delete(newImageName);
-    });
-
-    afterAll(async () => {
-      await pools.navigateTo();
-      await pools.delete(poolName);
-    });
-  });
-
-  describe('move to trash, restore and purge image tests', () => {
-    const poolName = 'trash_pool';
-    const imageName = 'trash#image';
-    const newImageName = 'newtrash#image';
-
-    beforeAll(async () => {
-      await pools.navigateTo('create'); // Need pool for image testing
-      await pools.create(poolName, 8, 'rbd');
-      await pools.navigateTo();
-      await pools.exist(poolName, true);
-
-      await images.navigateTo(); // Need image for trash testing
-      await images.createImage(imageName, poolName, '1');
-      await expect(images.getFirstTableCellWithText(imageName).isPresent()).toBe(true);
-    });
-
-    it('should move the image to the trash', async () => {
-      await images.moveToTrash(imageName);
-      await expect(images.getFirstTableCellWithText(imageName).isPresent()).toBe(true);
-    });
-
-    it('should restore image to images table', async () => {
-      await images.restoreImage(imageName, newImageName);
-      await expect(images.getFirstTableCellWithText(newImageName).isPresent()).toBe(true);
-    });
-
-    it('should purge trash in images trash tab', async () => {
-      await images.navigateTo();
-      // Have had issues with image not restoring fast enough, thus these tests/waits are here
-      await images.waitPresence(
-        images.getFirstTableCellWithText(newImageName),
-        'Timed out waiting for image to restore'
-      );
-      await images.moveToTrash(newImageName);
-      await images.purgeTrash(newImageName, poolName);
-    });
-
-    afterAll(async () => {
-      await pools.navigateTo();
-      await pools.delete(poolName); // Deletes images test pool
-      await pools.navigateTo();
-      await pools.exist(poolName, false);
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/block/images.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/block/images.po.ts
deleted file mode 100644 (file)
index 11e5ab1..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-import { $, $$, browser, by, element } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-export class ImagesPageHelper extends PageHelper {
-  pages = {
-    index: '/#/block/rbd',
-    create: '/#/block/rbd/create'
-  };
-
-  // Creates a block image and fills in the name, pool, and size fields. Then checks
-  // if the image is present in the Images table.
-  async createImage(name: string, pool: string, size: string) {
-    await this.navigateTo('create');
-
-    // Need the string '[value="<pool>"]' to find the pool in the dropdown menu
-    const getPoolName = `[value="${pool}"]`;
-
-    await element(by.id('name')).sendKeys(name); // Enter in image name
-
-    // Select image pool
-    await this.selectOption('pool', pool);
-    await $(getPoolName).click();
-    await expect(element(by.id('pool')).getAttribute('class')).toContain('ng-valid'); // check if selected
-
-    // Enter in the size of the image
-    await element(by.id('size')).click();
-    await element(by.id('size')).sendKeys(size);
-
-    // Click the create button and wait for image to be made
-    await element(by.cssContainingText('button', 'Create RBD')).click();
-    return this.waitPresence(this.getFirstTableCellWithText(name));
-  }
-
-  async editImage(name: string, pool: string, newName: string, newSize: string) {
-    const base_url = '/#/block/rbd/edit/';
-    const editURL = base_url
-      .concat(encodeURIComponent(pool))
-      .concat('%2F')
-      .concat(encodeURIComponent(name));
-    await browser.get(editURL);
-
-    await element(by.id('name')).click(); // click name box and send new name
-    await element(by.id('name')).clear();
-    await element(by.id('name')).sendKeys(newName);
-    await element(by.id('size')).click();
-    await element(by.id('size')).clear();
-    await element(by.id('size')).sendKeys(newSize); // click the size box and send new size
-
-    await element(by.cssContainingText('button', 'Edit RBD')).click();
-    await this.navigateTo();
-    await this.waitClickableAndClick(this.getExpandCollapseElement(newName));
-    await expect(
-      element.all(by.css('.table.table-striped.table-bordered')).first().getText()
-    ).toMatch(newSize);
-  }
-
-  // Selects RBD image and moves it to the trash, checks that it is present in the
-  // trash table
-  async moveToTrash(name: string) {
-    await this.navigateTo();
-    // wait for image to be created
-    await this.waitTextNotPresent($$('.datatable-body').first(), '(Creating...)');
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name));
-    // click on the drop down and selects the move to trash option
-    await $$('.table-actions button.dropdown-toggle').first().click();
-    await $('li.move-to-trash').click();
-    await this.waitVisibility(element(by.cssContainingText('button', 'Move Image')));
-    await element(by.cssContainingText('button', 'Move Image')).click();
-    await this.navigateTo();
-    // Clicks trash tab
-    await this.waitClickableAndClick(element(by.cssContainingText('.nav-link', 'Trash')));
-    await this.waitPresence(this.getFirstTableCellWithText(name));
-  }
-
-  // Checks trash tab table for image and then restores it to the RBD Images table
-  // (could change name if new name is given)
-  async restoreImage(name: string, newName?: string) {
-    await this.navigateTo();
-    // clicks on trash tab
-    await element(by.cssContainingText('.nav-link', 'Trash')).click();
-    // wait for table to load
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name));
-    await element(by.cssContainingText('button', 'Restore')).click();
-    // wait for pop-up to be visible (checks for title of pop-up)
-    await this.waitVisibility(element(by.id('name')));
-    // If a new name for the image is passed, it changes the name of the image
-    if (newName !== undefined) {
-      await element(by.id('name')).click(); // click name box and send new name
-      await element(by.id('name')).clear();
-      await element(by.id('name')).sendKeys(newName);
-    }
-    await element(by.cssContainingText('button', 'Restore Image')).click();
-    await this.navigateTo();
-    // clicks images tab
-    await element(by.cssContainingText('.nav-link', 'Images')).click();
-    await this.navigateTo();
-    await this.waitPresence(this.getFirstTableCellWithText(newName));
-  }
-
-  // Enters trash tab and purges trash, thus emptying the trash table. Checks if
-  // Image is still in the table.
-  async purgeTrash(name: string, pool?: string) {
-    await this.navigateTo();
-    // clicks trash tab
-    await element(by.cssContainingText('.nav-link', 'Trash')).click();
-    await element(by.cssContainingText('button', 'Purge Trash')).click();
-    // Check for visibility of modal container
-    await this.waitVisibility(element(by.id('poolName')));
-    // If purgeing a specific pool, selects that pool if given
-    if (pool !== undefined) {
-      const getPoolName = `[value="${pool}"]`;
-      await element(by.id('poolName')).click();
-      await element(by.cssContainingText('select[name=poolName] option', pool)).click();
-      await $(getPoolName).click();
-      await expect(element(by.id('poolName')).getAttribute('class')).toContain('ng-valid'); // check if pool is selected
-    }
-    await this.waitClickableAndClick(element(by.id('purgeFormButton')));
-    // Wait for image to delete and check it is not present
-    await this.waitStaleness(
-      this.getFirstTableCellWithText(name),
-      'Timed out waiting for image to be purged'
-    );
-    await expect(this.getFirstTableCellWithText(name).isPresent()).toBe(false);
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/block/iscsi.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/block/iscsi.e2e-spec.ts
deleted file mode 100644 (file)
index 79ea41d..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-import { IscsiPageHelper } from './iscsi.po';
-
-describe('Iscsi Page', () => {
-  let iscsi: IscsiPageHelper;
-
-  beforeAll(() => {
-    iscsi = new IscsiPageHelper();
-  });
-
-  afterEach(async () => {
-    await IscsiPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb test', () => {
-    beforeAll(async () => {
-      await iscsi.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await iscsi.waitTextToBePresent(iscsi.getBreadcrumb(), 'Overview');
-    });
-  });
-
-  describe('fields check', () => {
-    beforeAll(async () => {
-      await iscsi.navigateTo();
-    });
-
-    it('should check that tables are displayed and legends are correct', async () => {
-      // Check tables are displayed
-      const dataTables = iscsi.getDataTables();
-      await expect(dataTables.get(0).isDisplayed());
-      await expect(dataTables.get(1).isDisplayed());
-
-      // Check that legends are correct
-      const legends = iscsi.getLegends();
-      await expect(legends.get(0).getText()).toMatch('Gateways');
-      await expect(legends.get(1).getText()).toMatch('Images');
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/block/iscsi.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/block/iscsi.po.ts
deleted file mode 100644 (file)
index e2621f0..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PageHelper } from '../page-helper.po';
-
-export class IscsiPageHelper extends PageHelper {
-  pages = { index: '/#/block/iscsi/overview' };
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/block/mirroring.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/block/mirroring.e2e-spec.ts
deleted file mode 100644 (file)
index 3327cf1..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-import { PoolPageHelper } from '../pools/pools.po';
-import { MirroringPageHelper } from './mirroring.po';
-
-describe('Mirroring page', () => {
-  let pools: PoolPageHelper;
-  let mirroring: MirroringPageHelper;
-
-  beforeAll(() => {
-    mirroring = new MirroringPageHelper();
-    pools = new PoolPageHelper();
-  });
-
-  afterEach(async () => {
-    await MirroringPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb and tab tests', () => {
-    beforeAll(async () => {
-      await mirroring.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await mirroring.waitTextToBePresent(mirroring.getBreadcrumb(), 'Mirroring');
-    });
-
-    it('should show three tabs', async () => {
-      await expect(mirroring.getTabsCount()).toEqual(3);
-    });
-
-    it('should show text for all tabs', async () => {
-      await expect(mirroring.getTabText(0)).toEqual('Issues');
-      await expect(mirroring.getTabText(1)).toEqual('Syncing');
-      await expect(mirroring.getTabText(2)).toEqual('Ready');
-    });
-  });
-
-  describe('checks that edit mode functionality shows in the pools table', () => {
-    const poolName = 'mirroring_test';
-
-    beforeAll(async () => {
-      await pools.navigateTo('create'); // Need pool for mirroring testing
-      await pools.create(poolName, 8, 'rbd');
-      await pools.navigateTo();
-      await pools.exist(poolName, true);
-    });
-
-    it('tests editing mode for pools', async () => {
-      await mirroring.navigateTo();
-
-      await mirroring.editMirror(poolName, 'Pool');
-      await expect(mirroring.getFirstTableCellWithText('pool').isPresent()).toBe(true);
-      await mirroring.editMirror(poolName, 'Image');
-      await expect(mirroring.getFirstTableCellWithText('image').isPresent()).toBe(true);
-      await mirroring.editMirror(poolName, 'Disabled');
-      await expect(mirroring.getFirstTableCellWithText('disabled').isPresent()).toBe(true);
-    });
-
-    afterAll(async () => {
-      await pools.navigateTo();
-      await pools.delete(poolName);
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/block/mirroring.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/block/mirroring.po.ts
deleted file mode 100644 (file)
index d27a1dc..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-import { $, by, element } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-const pages = { index: '/#/block/mirroring' };
-
-export class MirroringPageHelper extends PageHelper {
-  pages = pages;
-
-  /**
-   * Goes to the mirroring page and edits a pool in the Pool table. Clicks on the
-   * pool and chooses an option (either pool, image, or disabled)
-   */
-  @PageHelper.restrictTo(pages.index)
-  async editMirror(name: string, option: string) {
-    // Clicks the pool in the table
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name));
-
-    // Clicks the Edit Mode button
-    const editModeButton = element(by.cssContainingText('button', 'Edit Mode'));
-    await this.waitClickableAndClick(editModeButton);
-    // Clicks the drop down in the edit pop-up, then clicks the Update button
-    await this.waitVisibility($('.modal-content'));
-    await this.selectOption('mirrorMode', option);
-
-    // Clicks update button and checks if the mode has been changed
-    await element(by.cssContainingText('button', 'Update')).click();
-    await this.waitStaleness(
-      element(by.cssContainingText('.modal-dialog', 'Edit pool mirror mode'))
-    );
-    const val = option.toLowerCase(); // used since entries in table are lower case
-    await this.waitVisibility(this.getFirstTableCellWithText(val));
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/configuration.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/configuration.e2e-spec.ts
deleted file mode 100644 (file)
index 57b4d92..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-import { $ } from 'protractor';
-import { ConfigurationPageHelper } from './configuration.po';
-
-describe('Configuration page', () => {
-  let configuration: ConfigurationPageHelper;
-
-  beforeAll(() => {
-    configuration = new ConfigurationPageHelper();
-  });
-
-  afterEach(async () => {
-    await ConfigurationPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb test', () => {
-    beforeAll(async () => {
-      await configuration.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await configuration.waitTextToBePresent(configuration.getBreadcrumb(), 'Configuration');
-    });
-  });
-
-  describe('fields check', () => {
-    beforeAll(async () => {
-      await configuration.navigateTo();
-      await configuration.waitClickableAndClick(configuration.getFirstExpandCollapseElement());
-    });
-
-    it('should verify that selected footer increases when an entry is clicked', async () => {
-      const selectedCount = await configuration.getTableSelectedCount();
-      await expect(selectedCount).toBe(1);
-    });
-
-    it('should check that details table opens and tab is correct', async () => {
-      await expect($('.table.table-striped.table-bordered').isDisplayed());
-      await expect(configuration.getTabsCount()).toEqual(1);
-      await expect(configuration.getTabText(0)).toEqual('Details');
-    });
-  });
-
-  describe('edit configuration test', () => {
-    const configName = 'client_cache_size';
-
-    beforeAll(async () => {
-      await configuration.navigateTo();
-    });
-
-    beforeEach(async () => {
-      await configuration.clearTableSearchInput();
-    });
-
-    afterAll(async () => {
-      await configuration.configClear(configName);
-    });
-
-    it('should click and edit a configuration and results should appear in the table', async () => {
-      await configuration.edit(
-        configName,
-        ['global', '1'],
-        ['mon', '2'],
-        ['mgr', '3'],
-        ['osd', '4'],
-        ['mds', '5'],
-        ['client', '6']
-      );
-    });
-
-    it('should show only modified configurations', async () => {
-      await configuration.filterTable('Modified', 'yes');
-      expect(await configuration.getTableFoundCount()).toBe(1);
-    });
-
-    it('should hide all modified configurations', async () => {
-      await configuration.filterTable('Modified', 'no');
-      expect(await configuration.getTableFoundCount()).toBeGreaterThan(1);
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/configuration.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/configuration.po.ts
deleted file mode 100644 (file)
index 6355226..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-import { $, by, element, protractor } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-export class ConfigurationPageHelper extends PageHelper {
-  pages = {
-    index: '/#/configuration'
-  };
-
-  async configClear(name: string) {
-    // Clears out all the values in a config to reset before and after testing
-    // Does not work for configs with checkbox only, possible future PR
-
-    await this.navigateTo();
-    const valList = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']; // Editable values
-
-    // Enter config setting name into filter box
-    await $('input.form-control.ng-valid').clear();
-    await $('input.form-control.ng-valid').sendKeys(name);
-
-    // Selects config that we want to clear
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // waits for config to be clickable and click
-    await element(by.cssContainingText('button', 'Edit')).click(); // clicks button to edit
-
-    for (const i of valList) {
-      // Sends two backspaces to all values, clear() did not work in this instance, could be optimized more
-      await element(by.id(i)).sendKeys(protractor.Key.chord(protractor.Key.CONTROL, 'a'));
-      await element(by.id(i)).sendKeys(protractor.Key.BACK_SPACE);
-    }
-    // Clicks save button and checks that values are not present for the selected config
-    await element(by.cssContainingText('button', 'Save')).click();
-
-    // Enter config setting name into filter box
-    await $('input.form-control.ng-valid').clear();
-    await $('input.form-control.ng-valid').sendKeys(name);
-
-    // Expand row
-    await this.waitClickableAndClick(this.getExpandCollapseElement(name));
-    // Clicks desired config
-    await this.waitVisibility(
-      $('.table.table-striped.table-bordered'), // Checks for visibility of details tab
-      'config details did not appear'
-    );
-    for (const i of valList) {
-      // Waits until values are not present in the details table
-      await this.waitTextNotPresent($('.table.table-striped.table-bordered'), i + ':');
-    }
-  }
-
-  async edit(name: string, ...values: [string, string][]) {
-    // Clicks the designated config, then inputs the values passed into the edit function.
-    // Then checks if the edit is reflected in the config table. Takes in name of config and
-    // a list of tuples of values the user wants edited, each tuple having the desired value along
-    // with the number tehey want for that value. Ex: [global, '2'] is the global value with an input of 2
-    await this.navigateTo();
-
-    // Enter config setting name into filter box
-    await $('input.form-control.ng-valid').clear();
-    await $('input.form-control.ng-valid').sendKeys(name);
-
-    // Selects config that we want to edit
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // waits for config to be clickable and click
-    await element(by.cssContainingText('button', 'Edit')).click(); // clicks button to edit
-
-    await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
-
-    for (let i = 0, valtuple; (valtuple = values[i]); i++) {
-      // Finds desired value based off given list
-      await element(by.id(valtuple[0])).sendKeys(valtuple[1]); // of values and inserts the given number for the value
-    }
-
-    // Clicks save button then waits until the desired config is visible, clicks it, then checks
-    // that each desired value appears with the desired number
-    await element(by.cssContainingText('button', 'Save')).click();
-    await this.navigateTo();
-
-    // Enter config setting name into filter box
-    await $('input.form-control.ng-valid').clear();
-    await $('input.form-control.ng-valid').sendKeys(name);
-
-    await this.waitVisibility(this.getFirstTableCellWithText(name));
-    // Checks for visibility of config in table
-    await this.getExpandCollapseElement(name).click();
-    // Clicks config
-    for (let i = 0, valtuple; (valtuple = values[i]); i++) {
-      // iterates through list of values and
-      await this.waitTextToBePresent(
-        // checks if the value appears in details with the correct number attatched
-        $('.table.table-striped.table-bordered'),
-        valtuple[0] + ': ' + valtuple[1]
-      );
-    }
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/crush-map.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/crush-map.e2e-spec.ts
deleted file mode 100644 (file)
index 07687bd..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-import { $ } from 'protractor';
-import { CrushMapPageHelper } from './crush-map.po';
-
-describe('CRUSH map page', () => {
-  let crushmap: CrushMapPageHelper;
-
-  beforeAll(() => {
-    crushmap = new CrushMapPageHelper();
-  });
-
-  afterEach(async () => {
-    await CrushMapPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb test', () => {
-    beforeAll(async () => {
-      await crushmap.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await crushmap.waitTextToBePresent(crushmap.getBreadcrumb(), 'CRUSH map');
-    });
-  });
-  describe('fields check', () => {
-    beforeAll(async () => {
-      await crushmap.navigateTo();
-    });
-
-    it('should check that title & table appears', async () => {
-      // Check that title (CRUSH map viewer) appears
-      await expect(crushmap.getPageTitle()).toMatch('CRUSH map viewer');
-
-      // Check that title appears once OSD is clicked
-      await crushmap.getCrushNode(1).click();
-
-      const label = await $('legend').getText(); // Get table label
-      await expect(crushmap.getCrushNode(1).getText()).toEqual(label);
-
-      // Check that table appears once OSD is clicked
-      await crushmap.waitVisibility($('.datatable-body'));
-      await expect($('.datatable-body').isDisplayed()).toBe(true);
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/crush-map.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/crush-map.po.ts
deleted file mode 100644 (file)
index 20790fa..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-import { $, $$ } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-export class CrushMapPageHelper extends PageHelper {
-  pages = { index: '/#/crush-map' };
-
-  getPageTitle() {
-    return $('cd-crushmap .card-header').getText();
-  }
-
-  getCrushNode(idx: number) {
-    return $$('.node-name.type-osd').get(idx);
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/hosts.e2e-spec.ts
deleted file mode 100644 (file)
index 1b64cdb..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-import { HostsPageHelper } from './hosts.po';
-
-describe('Hosts page', () => {
-  let hosts: HostsPageHelper;
-
-  beforeAll(() => {
-    hosts = new HostsPageHelper();
-  });
-
-  afterEach(async () => {
-    await HostsPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb and tab tests', () => {
-    beforeAll(async () => {
-      await hosts.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await hosts.waitTextToBePresent(hosts.getBreadcrumb(), 'Hosts');
-    });
-
-    it('should show two tabs', async () => {
-      await expect(hosts.getTabsCount()).toEqual(2);
-    });
-
-    it('should show hosts list tab at first', async () => {
-      await expect(hosts.getTabText(0)).toEqual('Hosts List');
-    });
-
-    it('should show overall performance as a second tab', async () => {
-      await expect(hosts.getTabText(1)).toEqual('Overall Performance');
-    });
-  });
-
-  describe('services link test', () => {
-    it('should check at least one host is present', async () => {
-      await hosts.check_for_host();
-    });
-
-    it('should check services link(s) work for first host', async () => {
-      await hosts.check_services_links();
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/hosts.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/hosts.po.ts
deleted file mode 100644 (file)
index df935c6..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-import { by, element } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-export class HostsPageHelper extends PageHelper {
-  pages = { index: '/#/hosts' };
-
-  async check_for_host() {
-    await this.navigateTo();
-
-    await expect(this.getTableTotalCount()).not.toBe(0);
-  }
-
-  // function that checks all services links work for first
-  // host in table
-  async check_services_links() {
-    await this.navigateTo();
-    let links_tested = 0;
-
-    const services = element.all(by.css('cd-hosts a.service-link'));
-    // check that text (links) is present in services box
-    await expect(services.count()).toBeGreaterThan(0, 'No services links exist on first host');
-
-    /**
-     * Currently there is an issue [1] in ceph that it's causing
-     * a random appearance of a mds service in the hosts service listing.
-     * Decreasing the number of service by 1 temporarily fixes the e2e failure.
-     *
-     * TODO: Revert this change when the issue has been fixed.
-     *
-     * [1] https://tracker.ceph.com/issues/41538
-     */
-    const num_links = (await services.count()) - 1;
-
-    for (let i = 0; i < num_links; i++) {
-      // click link, check it worked by looking for changed breadcrumb,
-      // navigate back to hosts page, repeat until all links checked
-      await services.get(i).click();
-      await this.waitTextToBePresent(this.getBreadcrumb(), 'Performance Counters');
-      await this.navigateBack();
-      await this.waitTextToBePresent(this.getBreadcrumb(), 'Hosts');
-      links_tested++;
-    }
-    // check if any links were actually tested
-    await expect(links_tested > 0).toBe(true, 'No links were tested. Test failed');
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/logs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/logs.e2e-spec.ts
deleted file mode 100644 (file)
index 47c95a3..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-import { PoolPageHelper } from '../pools/pools.po';
-import { ConfigurationPageHelper } from './configuration.po';
-import { LogsPageHelper } from './logs.po';
-
-describe('Logs page', () => {
-  let logs: LogsPageHelper;
-  let pools: PoolPageHelper;
-  let configuration: ConfigurationPageHelper;
-
-  const poolname = 'logs_e2e_test_pool';
-  const configname = 'log_graylog_port';
-  const today = new Date();
-  let hour = today.getHours();
-  if (hour > 12) {
-    hour = hour - 12;
-  }
-  const minute = today.getMinutes();
-
-  beforeAll(() => {
-    logs = new LogsPageHelper();
-    pools = new PoolPageHelper();
-    configuration = new ConfigurationPageHelper();
-  });
-
-  afterEach(async () => {
-    await LogsPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb and tab tests', () => {
-    beforeAll(async () => {
-      await logs.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await logs.waitTextToBePresent(logs.getBreadcrumb(), 'Logs');
-    });
-
-    it('should show two tabs', async () => {
-      await expect(logs.getTabsCount()).toEqual(2);
-    });
-
-    it('should show cluster logs tab at first', async () => {
-      await expect(logs.getTabText(0)).toEqual('Cluster Logs');
-    });
-
-    it('should show audit logs as a second tab', async () => {
-      await expect(logs.getTabText(1)).toEqual('Audit Logs');
-    });
-  });
-
-  describe('audit logs respond to pool creation and deletion test', () => {
-    it('should create pool and check audit logs reacted', async () => {
-      await pools.navigateTo('create');
-      await pools.create(poolname, 8);
-
-      await pools.navigateTo();
-      await pools.exist(poolname, true);
-
-      await logs.checkAuditForPoolFunction(poolname, 'create', hour, minute);
-    });
-
-    it('should delete pool and check audit logs reacted', async () => {
-      await pools.navigateTo();
-      await pools.delete(poolname);
-
-      await logs.navigateTo();
-      await logs.checkAuditForPoolFunction(poolname, 'delete', hour, minute);
-    });
-  });
-
-  describe('audit logs respond to editing configuration setting test', () => {
-    it('should change config settings and check audit logs reacted', async () => {
-      await configuration.navigateTo();
-      await configuration.edit(configname, ['global', '5']);
-
-      await logs.navigateTo();
-      await logs.checkAuditForConfigChange(configname, 'global', hour, minute);
-
-      await configuration.navigateTo();
-      await configuration.configClear(configname);
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/logs.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/logs.po.ts
deleted file mode 100644 (file)
index 7cc87f8..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-import { $, $$, by, element, protractor } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-export class LogsPageHelper extends PageHelper {
-  pages = { index: '/#/logs' };
-
-  async checkAuditForPoolFunction(
-    poolname: string,
-    poolfunction: string,
-    hour: number,
-    minute: number
-  ) {
-    await this.navigateTo();
-
-    // sometimes the modal from deleting pool is still present at this point.
-    // This wait makes sure it isn't
-    await this.waitStaleness(element(by.cssContainingText('.modal-dialog', 'Delete Pool')));
-
-    // go to audit logs tab
-    await element(by.cssContainingText('.nav-link', 'Audit Logs')).click();
-
-    // Enter an earliest time so that no old messages with the same pool name show up
-    await $$('.bs-timepicker-field')
-      .get(0)
-      .sendKeys(protractor.Key.chord(protractor.Key.CONTROL, 'a'));
-    await $$('.bs-timepicker-field').get(0).sendKeys(protractor.Key.BACK_SPACE);
-    if (hour < 10) {
-      await $$('.bs-timepicker-field').get(0).sendKeys('0');
-    }
-    await $$('.bs-timepicker-field').get(0).sendKeys(hour);
-
-    await $$('.bs-timepicker-field')
-      .get(1)
-      .sendKeys(protractor.Key.chord(protractor.Key.CONTROL, 'a'));
-    await $$('.bs-timepicker-field').get(1).sendKeys(protractor.Key.BACK_SPACE);
-    if (minute < 10) {
-      await $$('.bs-timepicker-field').get(1).sendKeys('0');
-    }
-    await $$('.bs-timepicker-field').get(1).sendKeys(minute);
-
-    // Enter the pool name into the filter box
-    await $$('input.form-control.ng-valid').first().click();
-    await $$('input.form-control.ng-valid').first().clear();
-    await $$('input.form-control.ng-valid').first().sendKeys(poolname);
-
-    const audit_logs_tab = $('.tab-pane.active');
-    const audit_logs_body = audit_logs_tab.element(by.css('.card-body'));
-    const logs = audit_logs_body.all(by.cssContainingText('.message', poolname));
-
-    await expect(logs.getText()).toMatch(poolname);
-    await expect(logs.getText()).toMatch(`pool ${poolfunction}`);
-  }
-
-  async checkAuditForConfigChange(
-    configname: string,
-    setting: string,
-    hour: number,
-    minute: number
-  ) {
-    await this.navigateTo();
-
-    // go to audit logs tab
-    await element(by.cssContainingText('.nav-link', 'Audit Logs')).click();
-
-    // Enter an earliest time so that no old messages with the same config name show up
-    await $$('.bs-timepicker-field')
-      .get(0)
-      .sendKeys(protractor.Key.chord(protractor.Key.CONTROL, 'a'));
-    await $$('.bs-timepicker-field').get(0).sendKeys(protractor.Key.BACK_SPACE);
-    if (hour < 10) {
-      await $$('.bs-timepicker-field').get(0).sendKeys('0');
-    }
-    await $$('.bs-timepicker-field').get(0).sendKeys(hour);
-
-    await $$('.bs-timepicker-field')
-      .get(1)
-      .sendKeys(protractor.Key.chord(protractor.Key.CONTROL, 'a'));
-    await $$('.bs-timepicker-field').get(1).sendKeys(protractor.Key.BACK_SPACE);
-    if (minute < 10) {
-      await $$('.bs-timepicker-field').get(1).sendKeys('0');
-    }
-    await $$('.bs-timepicker-field').get(1).sendKeys(minute);
-
-    // Enter the config name into the filter box
-    await $$('input.form-control.ng-valid').first().click();
-    await $$('input.form-control.ng-valid').first().clear();
-    await $$('input.form-control.ng-valid').first().sendKeys(configname);
-
-    const audit_logs_tab = $('.tab-pane.active');
-    const audit_logs_body = audit_logs_tab.element(by.css('.card-body'));
-    const logs = audit_logs_body.all(by.cssContainingText('.message', configname));
-
-    await this.waitPresence(logs.first());
-
-    await expect(logs.getText()).toMatch(configname);
-    await expect(logs.getText()).toMatch(setting);
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/mgr-modules.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/mgr-modules.e2e-spec.ts
deleted file mode 100644 (file)
index d14f35a..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-import { ManagerModulesPageHelper } from './mgr-modules.po';
-
-describe('Manager modules page', () => {
-  let mgrmodules: ManagerModulesPageHelper;
-
-  beforeAll(() => {
-    mgrmodules = new ManagerModulesPageHelper();
-  });
-
-  afterEach(async () => {
-    await ManagerModulesPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb test', () => {
-    beforeAll(async () => {
-      await mgrmodules.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await mgrmodules.waitTextToBePresent(mgrmodules.getBreadcrumb(), 'Manager modules');
-    });
-  });
-
-  describe('verifies editing functionality for manager modules', () => {
-    beforeAll(async () => {
-      await mgrmodules.navigateTo();
-    });
-
-    it('should test editing on diskprediction_local module', async () => {
-      const diskpredLocalArr = [
-        ['11', 'predict_interval'],
-        ['0122', 'sleep_interval']
-      ];
-      await mgrmodules.editMgrModule('diskprediction_local', diskpredLocalArr);
-    });
-
-    it('should test editing on balancer module', async () => {
-      const balancerArr = [['rq', 'pool_ids']];
-      await mgrmodules.editMgrModule('balancer', balancerArr);
-    });
-
-    it('should test editing on dashboard module', async () => {
-      const dashboardArr = [
-        ['rq', 'RGW_API_USER_ID'],
-        ['rafa', 'GRAFANA_API_PASSWORD']
-      ];
-      await mgrmodules.editMgrModule('dashboard', dashboardArr);
-    });
-
-    it('should test editing on devicehealth module', async () => {
-      await mgrmodules.editDevicehealth('1987', 'sox', '1999', '2020', '456', '567');
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/mgr-modules.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/mgr-modules.po.ts
deleted file mode 100644 (file)
index 1b928c4..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-import { $$, by, element } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-export class ManagerModulesPageHelper extends PageHelper {
-  pages = {
-    index: '/#/mgr-modules'
-  };
-
-  // NOTABLE ISSUES: .clear() does not work on most text boxes, therefore using sendKeys
-  // a Ctrl + 'a' BACK_SPACE is used.
-  // The need to click the module repeatedly in the table is to ensure
-  // that the values in the details tab updated. This fixed a bug I experienced.
-
-  async editMgrModule(name: string, tuple: string[][]) {
-    // Selects the Manager Module and then fills in the desired fields.
-    // Doesn't check/uncheck boxes because it is not reflected in the details table.
-    // DOES NOT WORK FOR ALL MGR MODULES, for example, Device health
-    await this.navigateTo();
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name));
-    await element(by.cssContainingText('button', 'Edit')).click();
-
-    for (const entry of tuple) {
-      // Clears fields and adds edits
-      await this.clearInput(element(by.id(entry[1])));
-      await element(by.id(entry[1])).sendKeys(entry[0]);
-    }
-
-    await element(by.cssContainingText('button', 'Update')).click();
-    // Checks if edits appear
-    await this.navigateTo();
-    await this.waitVisibility(this.getFirstTableCellWithText(name));
-    await this.getExpandCollapseElement(name).click();
-    for (const entry of tuple) {
-      await this.waitTextToBePresent($$('.datatable-body').last(), entry[0]);
-    }
-
-    // Clear mgr module of all edits made to it
-    await this.navigateTo();
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name));
-    await element(by.cssContainingText('button', 'Edit')).click();
-
-    // Clears the editable fields
-    for (const entry of tuple) {
-      await this.clearInput(element(by.id(entry[1])));
-    }
-
-    // Checks that clearing represents in details tab of module
-    await element(by.cssContainingText('button', 'Update')).click();
-    await this.navigateTo();
-    await this.waitVisibility(this.getFirstTableCellWithText(name));
-    await this.getExpandCollapseElement(name).click();
-    for (const entry of tuple) {
-      await this.waitTextNotPresent($$('.datatable-body').last(), entry[0]);
-    }
-  }
-
-  async editDevicehealth(
-    threshhold?: string,
-    pooln?: string,
-    retention?: string,
-    scrape?: string,
-    sleep?: string,
-    warn?: string
-  ) {
-    // Isn't called by editMgrModule since clearing doesn't work.
-    // Selects the Devicehealth manager module, then fills in the desired fields, including all fields except
-    // checkboxes. Clicking checkboxes has been a notable issue in Protractor, therefore they were omitted in this
-    // version of the tests. Could be added in a future PR. Then checks if these edits appear in the details table.
-    await this.navigateTo();
-    let devHealthArray: [string, string][];
-    devHealthArray = [
-      [threshhold, 'mark_out_threshold'],
-      [pooln, 'pool_name'],
-      [retention, 'retention_period'],
-      [scrape, 'scrape_frequency'],
-      [sleep, 'sleep_interval'],
-      [warn, 'warn_threshold']
-    ];
-
-    await this.waitClickableAndClick(this.getFirstTableCellWithText('devicehealth'));
-    await element(by.cssContainingText('button', 'Edit')).click();
-    for (let i = 0, devHealthTuple; (devHealthTuple = devHealthArray[i]); i++) {
-      if (devHealthTuple[0] !== undefined) {
-        // Clears and inputs edits
-        await this.clearInput(element(by.id(devHealthTuple[1])));
-        await element(by.id(devHealthTuple[1])).sendKeys(devHealthTuple[0]);
-      }
-    }
-
-    await element(by.cssContainingText('button', 'Update')).click();
-    await this.navigateTo();
-    await this.waitVisibility(this.getFirstTableCellWithText('devicehealth'));
-    // Checks for visibility of devicehealth in table
-    await this.getExpandCollapseElement('devicehealth').click();
-    for (let i = 0, devHealthTuple: [string, string]; (devHealthTuple = devHealthArray[i]); i++) {
-      if (devHealthTuple[0] !== undefined) {
-        await this.waitFn(async () => {
-          // Repeatedly reclicks the module to check if edits has been done
-          await element(by.cssContainingText('.datatable-body-cell-label', 'devicehealth')).click();
-          return this.waitTextToBePresent($$('.datatable-body').last(), devHealthTuple[0]);
-        });
-      }
-    }
-
-    // Inputs old values into devicehealth fields. This manager module doesnt allow for updates
-    // to be made when the values are cleared. Therefore, I restored them to their original values
-    // (on my local run of ceph-dev, this is subject to change i would assume). I'd imagine there is a
-    // better way of doing this.
-    await this.navigateTo();
-    await this.waitClickableAndClick(this.getFirstTableCellWithText('devicehealth'));
-    await element(by.cssContainingText('button', 'Edit')).click();
-    await this.clearInput(element(by.id('mark_out_threshold')));
-    await element(by.id('mark_out_threshold')).sendKeys('2419200');
-
-    await this.clearInput(element(by.id('pool_name')));
-    await element(by.id('pool_name')).sendKeys('device_health_metrics');
-
-    await this.clearInput(element(by.id('retention_period')));
-    await element(by.id('retention_period')).sendKeys('15552000');
-
-    await this.clearInput(element(by.id('scrape_frequency')));
-    await element(by.id('scrape_frequency')).sendKeys('86400');
-
-    await this.clearInput(element(by.id('sleep_interval')));
-    await element(by.id('sleep_interval')).sendKeys('600');
-
-    await this.clearInput(element(by.id('warn_threshold')));
-    await element(by.id('warn_threshold')).sendKeys('7257600');
-
-    // Checks that clearing represents in details tab
-    await this.waitClickableAndClick(element(by.cssContainingText('button', 'Update')));
-    await this.navigateTo();
-    await this.waitVisibility(this.getFirstTableCellWithText('devicehealth'));
-    await this.getExpandCollapseElement('devicehealth').click();
-    for (let i = 0, devHealthTuple: [string, string]; (devHealthTuple = devHealthArray[i]); i++) {
-      if (devHealthTuple[0] !== undefined) {
-        await this.waitFn(async () => {
-          // Repeatedly reclicks the module to check if clearing has been done
-          await element(by.cssContainingText('.datatable-body-cell-label', 'devicehealth')).click();
-          return this.waitTextNotPresent($$('.datatable-body').last(), devHealthTuple[0]);
-        });
-      }
-    }
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/monitors.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/monitors.e2e-spec.ts
deleted file mode 100644 (file)
index f5524b6..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-import { MonitorsPageHelper } from './monitors.po';
-
-describe('Monitors page', () => {
-  let monitors: MonitorsPageHelper;
-
-  beforeAll(() => {
-    monitors = new MonitorsPageHelper();
-  });
-
-  afterEach(async () => {
-    await MonitorsPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb test', () => {
-    beforeAll(async () => {
-      await monitors.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await monitors.waitTextToBePresent(monitors.getBreadcrumb(), 'Monitors');
-    });
-  });
-
-  describe('fields check', () => {
-    beforeAll(async () => {
-      await monitors.navigateTo();
-    });
-
-    it('should check status table is present', async () => {
-      // check for table header 'Status'
-      await expect(monitors.getLegends().get(0).getText()).toMatch('Status');
-
-      // check for fields in table
-      await expect(monitors.getStatusTables().getText()).toMatch('Cluster ID');
-      await expect(monitors.getStatusTables().getText()).toMatch('monmap modified');
-      await expect(monitors.getStatusTables().getText()).toMatch('monmap epoch');
-      await expect(monitors.getStatusTables().getText()).toMatch('quorum con');
-      await expect(monitors.getStatusTables().getText()).toMatch('quorum mon');
-      await expect(monitors.getStatusTables().getText()).toMatch('required con');
-      await expect(monitors.getStatusTables().getText()).toMatch('required mon');
-    });
-
-    it('should check In Quorum and Not In Quorum tables are present', async () => {
-      // check for there to be two tables
-      await expect(monitors.getDataTables().count()).toEqual(2);
-
-      // check for table header 'In Quorum'
-      await expect(monitors.getLegends().get(1).getText()).toMatch('In Quorum');
-
-      // check for table header 'Not In Quorum'
-      await expect(monitors.getLegends().get(2).getText()).toMatch('Not In Quorum');
-
-      // verify correct columns on In Quorum table
-      await expect(monitors.getDataTableHeaders().get(0).getText()).toMatch('Name');
-      await expect(monitors.getDataTableHeaders().get(0).getText()).toMatch('Rank');
-      await expect(monitors.getDataTableHeaders().get(0).getText()).toMatch('Public Address');
-      await expect(monitors.getDataTableHeaders().get(0).getText()).toMatch('Open Sessions');
-
-      // verify correct columns on Not In Quorum table
-      await expect(monitors.getDataTableHeaders().get(1).getText()).toMatch('Name');
-      await expect(monitors.getDataTableHeaders().get(1).getText()).toMatch('Rank');
-      await expect(monitors.getDataTableHeaders().get(1).getText()).toMatch('Public Address');
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/monitors.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/monitors.po.ts
deleted file mode 100644 (file)
index c935eba..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PageHelper } from '../page-helper.po';
-
-export class MonitorsPageHelper extends PageHelper {
-  pages = { index: '/#/monitor' };
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/osds.e2e-spec.ts
deleted file mode 100644 (file)
index f8a0939..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-import { $$, by, element } from 'protractor';
-import { OSDsPageHelper } from './osds.po';
-
-describe('OSDs page', () => {
-  let osds: OSDsPageHelper;
-
-  beforeAll(async () => {
-    osds = new OSDsPageHelper();
-    await osds.navigateTo();
-  });
-
-  afterEach(async () => {
-    await OSDsPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb and tab tests', () => {
-    it('should open and show breadcrumb', async () => {
-      await osds.waitTextToBePresent(osds.getBreadcrumb(), 'OSDs');
-    });
-
-    it('should show two tabs', async () => {
-      await expect(osds.getTabsCount()).toEqual(2);
-    });
-
-    it('should show OSDs list tab at first', async () => {
-      await expect(osds.getTabText(0)).toEqual('OSDs List');
-    });
-
-    it('should show overall performance as a second tab', async () => {
-      await expect(osds.getTabText(1)).toEqual('Overall Performance');
-    });
-  });
-
-  describe('check existence of fields on OSD page', () => {
-    it('should check that number of rows and count in footer match', async () => {
-      await expect(osds.getTableTotalCount()).toEqual(osds.getTableRows().count());
-    });
-
-    it('should verify that buttons exist', async () => {
-      await expect(element(by.cssContainingText('button', 'Create')).isPresent()).toBe(true);
-      await expect(
-        element(by.cssContainingText('button', 'Cluster-wide configuration')).isPresent()
-      ).toBe(true);
-    });
-
-    describe('by selecting one row in OSDs List', () => {
-      beforeAll(async () => {
-        await osds.waitClickableAndClick(osds.getFirstExpandCollapseElement());
-      });
-
-      it('should verify that selected footer increases', async () => {
-        await expect(osds.getTableSelectedCount()).toEqual(1);
-      });
-
-      it('should show the correct text for the tab labels', async () => {
-        const tabHeadings = $$('#tabset-osd-details > div > tab').map((e) =>
-          e.getAttribute('heading')
-        );
-        await expect(tabHeadings).toEqual([
-          'Devices',
-          'Attributes (OSD map)',
-          'Metadata',
-          'Device health',
-          'Performance counter',
-          'Histogram',
-          'Performance Details'
-        ]);
-      });
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/cluster/osds.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/cluster/osds.po.ts
deleted file mode 100644 (file)
index a968989..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PageHelper } from '../page-helper.po';
-
-export class OSDsPageHelper extends PageHelper {
-  pages = { index: '/#/osd' };
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/filesystems/filesystems.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/filesystems/filesystems.e2e-spec.ts
deleted file mode 100644 (file)
index 2d860c4..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import { FilesystemsPageHelper } from './filesystems.po';
-
-describe('Filesystems page', () => {
-  let filesystems: FilesystemsPageHelper;
-
-  beforeAll(() => {
-    filesystems = new FilesystemsPageHelper();
-  });
-
-  afterEach(async () => {
-    await FilesystemsPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb test', () => {
-    beforeAll(async () => {
-      await filesystems.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await filesystems.waitTextToBePresent(filesystems.getBreadcrumb(), 'Filesystems');
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/filesystems/filesystems.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/filesystems/filesystems.po.ts
deleted file mode 100644 (file)
index eedbd85..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PageHelper } from '../page-helper.po';
-
-export class FilesystemsPageHelper extends PageHelper {
-  pages = { index: '/#/cephfs' };
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/nfs/nfs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/nfs/nfs.e2e-spec.ts
deleted file mode 100644 (file)
index dc69745..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import { NfsPageHelper } from './nfs.po';
-
-describe('Nfs page', () => {
-  let nfs: NfsPageHelper;
-
-  beforeAll(() => {
-    nfs = new NfsPageHelper();
-  });
-
-  afterEach(async () => {
-    await NfsPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb test', () => {
-    beforeAll(async () => {
-      await nfs.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await nfs.waitTextToBePresent(nfs.getBreadcrumb(), 'NFS');
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/nfs/nfs.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/nfs/nfs.po.ts
deleted file mode 100644 (file)
index d3db8ec..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PageHelper } from '../page-helper.po';
-
-export class NfsPageHelper extends PageHelper {
-  pages = { index: '/#/nfs' };
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts
deleted file mode 100644 (file)
index b5d4f5f..0000000
+++ /dev/null
@@ -1,348 +0,0 @@
-import {
-  $,
-  $$,
-  browser,
-  by,
-  element,
-  ElementArrayFinder,
-  ElementFinder,
-  protractor
-} from 'protractor';
-
-const EC = browser.ExpectedConditions;
-const TIMEOUT = 20000;
-
-interface Pages {
-  index: string;
-}
-
-export abstract class PageHelper {
-  pages: Pages;
-
-  /**
-   * Checks if there are any errors on the browser
-   *
-   * @static
-   * @memberof Helper
-   */
-  static async checkConsole() {
-    let browserLog = await browser.manage().logs().get('browser');
-
-    browserLog = browserLog.filter((log) => log.level.value > 900);
-
-    if (browserLog.length > 0) {
-      console.log('\n log: ' + require('util').inspect(browserLog));
-    }
-
-    await expect(browserLog.length).toEqual(0);
-  }
-
-  /**
-   * 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: string): Function {
-    return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
-      const fn: Function = descriptor.value;
-      descriptor.value = function (...args: any) {
-        return browser
-          .getCurrentUrl()
-          .then((url) =>
-            url.endsWith(page)
-              ? fn.apply(this, args)
-              : Promise.reject(
-                  `Method ${target.constructor.name}::${propertyKey} is supposed to be ` +
-                    `run on path "${page}", but was run on URL "${url}"`
-                )
-          );
-      };
-    };
-  }
-
-  /**
-   * Get the active breadcrumb item.
-   */
-  getBreadcrumb(): ElementFinder {
-    return $('.breadcrumb-item.active');
-  }
-
-  async getTabText(index: number): Promise<string> {
-    return $$('.nav.nav-tabs li').get(index).getText();
-  }
-
-  async getTableTotalCount(): Promise<number> {
-    const text = await $$('.datatable-footer-inner .page-count span')
-      .filter(async (e) => (await e.getText()).includes('total'))
-      .first()
-      .getText();
-    return Number(text.match(/(\d+)\s+total/)[1]);
-  }
-
-  async getTableSelectedCount(): Promise<number> {
-    const text = await $$('.datatable-footer-inner .page-count span')
-      .filter(async (e) => (await e.getText()).includes('selected'))
-      .first()
-      .getText();
-    return Number(text.match(/(\d+)\s+selected/)[1]);
-  }
-
-  async getTableFoundCount(): Promise<number> {
-    const text = await $$('.datatable-footer-inner .page-count span')
-      .filter(async (e) => (await e.getText()).includes('found'))
-      .first()
-      .getText();
-    return Number(text.match(/(\d+)\s+found/)[1]);
-  }
-
-  getFirstTableCellWithText(content: string): ElementFinder {
-    return element.all(by.cssContainingText('.datatable-body-cell-label', content)).first();
-  }
-
-  getFirstExpandCollapseElement(): ElementFinder {
-    return element.all(by.className('tc_expand-collapse')).first();
-  }
-
-  getExpandCollapseElement(content: string): ElementFinder {
-    const tableRow = element(by.cssContainingText('.datatable-body-row', content));
-    return tableRow.element(by.className('tc_expand-collapse'));
-  }
-
-  getTableRow(content: string) {
-    return element(by.cssContainingText('.datatable-body-row', content));
-  }
-
-  getTable(): ElementFinder {
-    return $('.datatable-body');
-  }
-
-  async getTabsCount(): Promise<number> {
-    return $$('.nav.nav-tabs li').count();
-  }
-
-  /**
-   * Ceph Dashboards' <input type="checkbox"> tag is not visible. Instead of the real checkbox, a
-   * replacement is shown which is supposed to have an adapted style. The replacement checkbox shown
-   * is part of the label and is rendered in the "::before" pseudo element of the label, hence the
-   * label is always clicked when the user clicks the replacement checkbox.
-   *
-   * This method finds corresponding label to the given checkbox and clicks it instead of the (fake)
-   * checkbox, like it is the case with real users.
-   *
-   * Alternatively, the checkbox' label can be passed.
-   *
-   * @param elem The checkbox or corresponding label
-   */
-  async clickCheckbox(elem: ElementFinder): Promise<void> {
-    const tagName = await elem.getTagName();
-    let label: ElementFinder = null; // Both types are clickable
-
-    await this.waitPresence(elem);
-    if (tagName === 'input') {
-      if ((await elem.getAttribute('type')) === 'checkbox') {
-        label = elem.element(by.xpath('..')).$(`label[for="${await elem.getAttribute('id')}"]`);
-      } else {
-        return Promise.reject('element <input> must be of type checkbox');
-      }
-    } else if (tagName === 'label') {
-      label = elem;
-    } else {
-      return Promise.reject(
-        `element <${tagName}> is not of the correct type. You need to pass a checkbox or label`
-      );
-    }
-
-    return this.waitClickableAndClick(label);
-  }
-
-  /**
-   * Helper method to select an option inside a select element.
-   * This method will also expect that the option was set.
-   * @param option The option text (not value) to be selected.
-   */
-  async selectOption(selectionName: string, option: string) {
-    await element(by.cssContainingText(`select[name=${selectionName}] option`, option)).click();
-    return this.expectSelectOption(selectionName, option);
-  }
-
-  /**
-   * Helper method to expect a set option inside a select element.
-   * @param option The selected option text (not value) that is to
-   *   be expected.
-   */
-  async expectSelectOption(selectionName: string, option: string) {
-    return expect(
-      element(by.css(`select[name=${selectionName}] option:checked`)).getText()
-    ).toContain(option);
-  }
-
-  /**
-   * 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
-   * visibility/invisibility/presence of the returned element.
-   *
-   * It will return a rejected Promise if the result is ambiguous, though. That means if the search
-   * for content has been completed, but more than a single row is shown in the data table.
-   */
-  async getTableCellByContent(content: string): 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');
-
-    await rowAmountInput.clear();
-    await rowAmountInput.sendKeys('10');
-    await searchInput.clear();
-    await searchInput.sendKeys(content);
-
-    const count = Number(await footer.getAttribute('ng-reflect-row-count'));
-    if (count !== 0 && count > 1) {
-      return Promise.reject('getTableCellByContent: Result is ambiguous');
-    } else {
-      return Promise.resolve(
-        element(
-          by.cssContainingText('.datatable-body-cell-label', new RegExp(`^\\s${content}\\s$`))
-        )
-      );
-    }
-  }
-
-  /**
-   * Used when .clear() does not work on a text box, sends a Ctrl + a, BACKSPACE
-   */
-  async clearInput(elem: ElementFinder) {
-    const types = ['text', 'number'];
-    if ((await elem.getTagName()) === 'input' && types.includes(await elem.getAttribute('type'))) {
-      return await elem.sendKeys(
-        protractor.Key.chord(protractor.Key.CONTROL, 'a'),
-        protractor.Key.BACK_SPACE
-      );
-    } else {
-      return Promise.reject(`Element ${elem} does not match the expected criteria.`);
-    }
-  }
-
-  async navigateTo(page: string = null) {
-    page = page || 'index';
-    const url = this.pages[page];
-    await browser.get(url);
-  }
-
-  async navigateBack() {
-    await browser.navigate().back();
-  }
-
-  getDataTables(): ElementArrayFinder {
-    return $$('cd-table');
-  }
-
-  /**
-   * Gets column headers of table
-   */
-  getDataTableHeaders(): ElementArrayFinder {
-    return $$('.datatable-header');
-  }
-
-  /**
-   * Grabs striped tables
-   */
-  getStatusTables(): ElementArrayFinder {
-    return $$('.table.table-striped');
-  }
-
-  /**
-   * Grabs legends above tables
-   */
-  getLegends(): ElementArrayFinder {
-    return $$('legend');
-  }
-
-  getToast() {
-    return $('.ngx-toastr');
-  }
-
-  async waitPresence(elem: ElementFinder, message?: string) {
-    return browser.wait(EC.presenceOf(elem), TIMEOUT, message);
-  }
-
-  async waitStaleness(elem: ElementFinder, message?: string) {
-    return browser.wait(EC.stalenessOf(elem), TIMEOUT, message);
-  }
-
-  /**
-   * This method will wait for the element to be clickable and then click it.
-   */
-  async waitClickableAndClick(elem: ElementFinder, message?: string) {
-    await browser.wait(EC.elementToBeClickable(elem), TIMEOUT, message);
-    return elem.click();
-  }
-
-  async waitVisibility(elem: ElementFinder, message?: string) {
-    return browser.wait(EC.visibilityOf(elem), TIMEOUT, message);
-  }
-
-  async waitInvisibility(elem: ElementFinder, message?: string) {
-    return browser.wait(EC.invisibilityOf(elem), TIMEOUT, message);
-  }
-
-  async waitTextToBePresent(elem: ElementFinder, text: string, message?: string) {
-    return browser.wait(EC.textToBePresentInElement(elem, text), TIMEOUT, message);
-  }
-
-  async waitTextNotPresent(elem: ElementFinder, text: string, message?: string) {
-    return browser.wait(EC.not(EC.textToBePresentInElement(elem, text)), TIMEOUT, message);
-  }
-
-  async waitFn(func: Function, message?: string, timeout: number = TIMEOUT) {
-    return browser.wait(func, timeout, message);
-  }
-
-  getFirstCell(): ElementFinder {
-    return $$('.datatable-body-cell-label').first();
-  }
-
-  /**
-   * This is a generic method to delete table rows.
-   * It will select the first row that contains the provided name and delete it.
-   * After that it will wait until the row is no longer displayed.
-   */
-  async delete(name: string): Promise<any> {
-    // Selects row
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name));
-
-    // Clicks on table Delete button
-    await $$('.table-actions button.dropdown-toggle').first().click(); // open submenu
-    await $('li.delete a').click(); // click on "delete" menu item
-
-    // Confirms deletion
-    await this.clickCheckbox($('.custom-control-label'));
-    await element(by.cssContainingText('button', 'Delete')).click();
-
-    // Waits for item to be removed from table
-    return this.waitStaleness(this.getFirstTableCellWithText(name));
-  }
-
-  getTableRows() {
-    return $$('datatable-row-wrapper');
-  }
-
-  /**
-   * Uncheck all checked table rows.
-   */
-  async uncheckAllTableRows() {
-    await $$(
-      '.datatable-body-cell-label .datatable-checkbox input[type=checkbox]:checked'
-    ).each((e: ElementFinder) => e.click());
-  }
-
-  async filterTable(name: string, option: string) {
-    await this.waitClickableAndClick($('.tc_filter_name > a'));
-    await element(by.cssContainingText(`.tc_filter_name .dropdown-item`, name)).click();
-
-    await this.waitClickableAndClick($('.tc_filter_option > a'));
-    await element(by.cssContainingText(`.tc_filter_option .dropdown-item`, option)).click();
-  }
-
-  async clearTableSearchInput() {
-    return this.waitClickableAndClick($('cd-table .search button'));
-  }
-}
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
deleted file mode 100644 (file)
index 7668dc2..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-import { PoolPageHelper } from './pools.po';
-
-describe('Pools page', () => {
-  let pools: PoolPageHelper;
-  const poolName = 'pool_e2e_pool/test';
-
-  beforeAll(async () => {
-    pools = new PoolPageHelper();
-    await pools.navigateTo();
-  });
-
-  afterEach(async () => {
-    await PoolPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb and tab tests', () => {
-    it('should open and show breadcrumb', async () => {
-      await pools.waitTextToBePresent(pools.getBreadcrumb(), 'Pools');
-    });
-
-    it('should show two tabs', async () => {
-      await expect(pools.getTabsCount()).toEqual(2);
-    });
-
-    it('should show pools list tab at first', async () => {
-      await expect(pools.getTabText(0)).toEqual('Pools List');
-    });
-
-    it('should show overall performance as a second tab', async () => {
-      await expect(pools.getTabText(1)).toEqual('Overall Performance');
-    });
-  });
-
-  it('should create a pool', async () => {
-    await pools.exist(poolName, false);
-    await pools.navigateTo('create');
-    await pools.create(poolName, 8);
-    await pools.navigateTo();
-    await pools.exist(poolName, true);
-  });
-
-  it('should edit a pools placement group', async () => {
-    await pools.exist(poolName, true);
-    await pools.navigateTo();
-    await pools.edit_pool_pg(poolName, 32);
-  });
-
-  it('should delete a pool', async () => {
-    await pools.navigateTo();
-    await pools.delete(poolName);
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/pools/pools.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/pools/pools.po.ts
deleted file mode 100644 (file)
index bda20d6..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-import { $, by, element, protractor } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-const pages = {
-  index: '/#/pool',
-  create: '/#/pool/create'
-};
-
-export class PoolPageHelper extends PageHelper {
-  pages = pages;
-
-  private isPowerOf2(n: number): boolean {
-    // tslint:disable-next-line: no-bitwise
-    return (n & (n - 1)) === 0;
-  }
-
-  @PageHelper.restrictTo(pages.index)
-  async exist(name: string, oughtToBePresent = true) {
-    const tableCell = await this.getTableCellByContent(name);
-    const waitFn = oughtToBePresent ? this.waitVisibility : this.waitInvisibility;
-    try {
-      await waitFn(tableCell);
-    } catch (e) {
-      const visibility = oughtToBePresent ? 'invisible' : 'visible';
-      const msg = `Pool "${name}" is ${visibility}, but should not be. Waiting for a change timed out`;
-      return Promise.reject(msg);
-    }
-    return Promise.resolve();
-  }
-
-  @PageHelper.restrictTo(pages.create)
-  async create(name: string, placement_groups: number, ...apps: string[]): Promise<any> {
-    const nameInput = $('input[name=name]');
-    await nameInput.clear();
-    if (!this.isPowerOf2(placement_groups)) {
-      return Promise.reject(`Placement groups ${placement_groups} are not a power of 2`);
-    }
-    await nameInput.sendKeys(name);
-    await this.selectOption('poolType', 'replicated');
-
-    await this.expectSelectOption('pgAutoscaleMode', 'on');
-    await this.selectOption('pgAutoscaleMode', 'off'); // To show pgNum field
-    await $('input[name=pgNum]').sendKeys(
-      protractor.Key.CONTROL,
-      'a',
-      protractor.Key.NULL,
-      placement_groups
-    );
-    await this.setApplications(apps);
-    await element(by.css('cd-submit-button')).click();
-
-    return Promise.resolve();
-  }
-
-  async edit_pool_pg(name: string, new_pg: number, wait = true): Promise<void> {
-    if (!this.isPowerOf2(new_pg)) {
-      return Promise.reject(`Placement groups ${new_pg} are not a power of 2`);
-    }
-    const elem = await this.getTableCellByContent(name);
-    await this.waitClickableAndClick(elem); // select pool from the table
-    await element(by.cssContainingText('button', 'Edit')).click(); // click edit button
-    await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit'); // verify we are now on edit page
-    await $('input[name=pgNum]').sendKeys(protractor.Key.CONTROL, 'a', protractor.Key.NULL, new_pg);
-    await element(by.css('cd-submit-button')).click();
-    const str = `${new_pg} active+clean`;
-    await this.waitVisibility(this.getTableRow(name), 'Timed out waiting for table row to load');
-    if (wait) {
-      await this.waitTextToBePresent(
-        this.getTableRow(name),
-        str,
-        'Timed out waiting for placement group to be updated'
-      );
-    }
-  }
-
-  private async setApplications(apps: string[]) {
-    if (!apps || apps.length === 0) {
-      return;
-    }
-    await element(by.css('.float-left.mr-2.select-menu-edit')).click();
-    await this.waitVisibility(element(by.css('.popover-content.popover-body')));
-    apps.forEach(
-      async (app) => await element(by.cssContainingText('.select-menu-item-content', app)).click()
-    );
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.e2e-spec.ts
deleted file mode 100644 (file)
index 8dbb80e..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-import { $ } from 'protractor';
-import { BucketsPageHelper } from './buckets.po';
-
-describe('RGW buckets page', () => {
-  let buckets: BucketsPageHelper;
-  const bucket_name = '000test';
-
-  beforeAll(async () => {
-    buckets = new BucketsPageHelper();
-  });
-
-  afterEach(async () => {
-    await BucketsPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb tests', () => {
-    beforeEach(async () => {
-      await buckets.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await expect($('.breadcrumb-item.active').getText()).toBe('Buckets');
-    });
-  });
-
-  describe('create, edit & delete bucket tests', () => {
-    beforeEach(async () => {
-      await buckets.navigateTo();
-      await buckets.uncheckAllTableRows();
-    });
-
-    it('should create bucket', async () => {
-      await buckets.navigateTo('create');
-      await buckets.create(
-        bucket_name,
-        '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
-        'default-placement'
-      );
-      await expect(buckets.getFirstTableCellWithText(bucket_name).isPresent()).toBe(true);
-    });
-
-    it('should edit bucket', async () => {
-      await buckets.edit(bucket_name, 'dev');
-      await expect(buckets.getTable().getText()).toMatch('dev');
-    });
-
-    it('should delete bucket', async () => {
-      await buckets.delete(bucket_name);
-    });
-  });
-
-  describe('Invalid Input in Create and Edit tests', () => {
-    it('should test invalid inputs in create fields', async () => {
-      await buckets.testInvalidCreate();
-    });
-
-    it('should test invalid input in edit owner field', async () => {
-      await buckets.navigateTo('create');
-      await buckets.create(
-        '000rq',
-        '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
-        'default-placement'
-      );
-      await buckets.testInvalidEdit('000rq');
-      await buckets.navigateTo();
-      await buckets.uncheckAllTableRows();
-      await buckets.delete('000rq');
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/buckets.po.ts
deleted file mode 100644 (file)
index ab14627..0000000
+++ /dev/null
@@ -1,192 +0,0 @@
-import { by, element } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-const pages = {
-  index: '/#/rgw/bucket',
-  create: '/#/rgw/bucket/create'
-};
-
-export class BucketsPageHelper extends PageHelper {
-  pages = pages;
-  versioningStateEnabled = 'Enabled';
-  versioningStateSuspended = 'Suspended';
-
-  private async selectOwner(owner: string) {
-    return this.selectOption('owner', owner);
-  }
-
-  private async selectPlacementTarget(placementTarget: string) {
-    return this.selectOption('placement-target', placementTarget);
-  }
-
-  /**
-   * TODO add check to verify the existance of the bucket!
-   * TODO let it print a meaningful error message (for devs) if it does not exist!
-   */
-  @PageHelper.restrictTo(pages.create)
-  async create(name: string, owner: string, placementTarget: string) {
-    // Enter in bucket name
-    await element(by.id('bid')).sendKeys(name);
-
-    // Select bucket owner
-    await this.selectOwner(owner);
-    await expect(element(by.id('owner')).getAttribute('class')).toContain('ng-valid');
-
-    // Select bucket placement target:
-    await this.selectPlacementTarget(placementTarget);
-    await expect(element(by.id('placement-target')).getAttribute('class')).toContain('ng-valid');
-
-    // Click the create button and wait for bucket to be made
-    const createButton = element(by.cssContainingText('button', 'Create Bucket'));
-    await createButton.click();
-
-    return this.waitPresence(
-      this.getFirstTableCellWithText(name),
-      'Timed out waiting for bucket creation'
-    );
-  }
-
-  @PageHelper.restrictTo(pages.index)
-  async edit(name: string, new_owner: string) {
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // wait for table to load and click
-    await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page
-    await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
-    await expect(element(by.css('input[name=placement-target]')).getAttribute('value')).toBe(
-      'default-placement'
-    );
-    await this.selectOwner(new_owner);
-
-    // Enable versioning
-    await expect(element(by.css('input[id=versioning]')).getAttribute('checked')).toBeFalsy();
-    await element(by.css('label[for=versioning]')).click();
-    await expect(element(by.css('input[id=versioning]')).getAttribute('checked')).toBeTruthy();
-
-    await element(by.cssContainingText('button', 'Edit Bucket')).click();
-
-    // wait to be back on buckets page with table visible and click
-    await this.waitClickableAndClick(
-      this.getExpandCollapseElement(name),
-      'Could not return to buckets page and load table after editing bucket'
-    );
-
-    // check its details table for edited owner field
-    let bucketDataTable = element.all(by.css('.table.table-striped.table-bordered')).first();
-    await expect(bucketDataTable.getText()).toMatch(new_owner);
-
-    // Check versioning enabled:
-    const ownerValueCell = bucketDataTable.all(by.css('tr')).get(2).all(by.css('td')).last();
-    await expect(ownerValueCell.getText()).toEqual(new_owner);
-    let versioningValueCell = bucketDataTable.all(by.css('tr')).get(11).all(by.css('td')).last();
-    await expect(versioningValueCell.getText()).toEqual(this.versioningStateEnabled);
-
-    // Disable versioning:
-    await this.uncheckAllTableRows();
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // wait for table to load and click
-    await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page
-    await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
-    await element(by.css('label[for=versioning]')).click();
-    await expect(element(by.css('input[id=versioning]')).getAttribute('checked')).toBeFalsy();
-    await element(by.cssContainingText('button', 'Edit Bucket')).click();
-
-    // Check versioning suspended:
-    await this.waitClickableAndClick(
-      this.getExpandCollapseElement(name),
-      'Could not return to buckets page and load table after editing bucket'
-    );
-    bucketDataTable = element.all(by.css('.table.table-striped.table-bordered')).first();
-    versioningValueCell = bucketDataTable.all(by.css('tr')).get(11).all(by.css('td')).last();
-    return expect(versioningValueCell.getText()).toEqual(this.versioningStateSuspended);
-  }
-
-  async testInvalidCreate() {
-    await this.navigateTo('create');
-    const nameInputField = element(by.id('bid')); // Grabs name box field
-
-    // Gives an invalid name (too short), then waits for dashboard to determine validity
-    await nameInputField.sendKeys('rq');
-
-    await element(by.id('owner')).click(); // To trigger a validation
-
-    await this.waitFn(async () => {
-      // Waiting for website to decide if name is valid or not
-      const klass = await nameInputField.getAttribute('class');
-      return !klass.includes('ng-pending');
-    }, 'Timed out waiting for dashboard to decide bucket name validity');
-
-    // Check that name input field was marked invalid in the css
-    await expect(nameInputField.getAttribute('class')).toContain('ng-invalid');
-
-    // Check that error message was printed under name input field
-    await expect(element(by.css('#bid + .invalid-feedback')).getText()).toMatch(
-      'The value is not valid.'
-    );
-
-    // Test invalid owner input
-    // select some valid option. The owner drop down error message will not appear unless a valid user was selected at
-    // one point before the invalid placeholder user is selected.
-    await this.selectOwner('dev');
-
-    // select the first option, which is invalid because it is a placeholder
-    await this.selectOwner('Select a user');
-
-    await nameInputField.click();
-
-    // Check that owner drop down field was marked invalid in the css
-    await expect(element(by.id('owner')).getAttribute('class')).toContain('ng-invalid');
-
-    // Check that error message was printed under owner drop down field
-    await expect(element(by.css('#owner + .invalid-feedback')).getText()).toMatch(
-      'This field is required.'
-    );
-
-    // Check invalid placement target input
-    await this.selectOwner('dev');
-    // The drop down error message will not appear unless a valid option is previsously selected.
-    await this.selectPlacementTarget('default-placement');
-    await this.selectPlacementTarget('Select a placement target');
-    await nameInputField.click(); // Trigger validation
-    await expect(element(by.id('placement-target')).getAttribute('class')).toContain('ng-invalid');
-    await expect(element(by.css('#placement-target + .invalid-feedback')).getText()).toMatch(
-      'This field is required.'
-    );
-
-    // Clicks the Create Bucket button but the page doesn't move. Done by testing
-    // for the breadcrumb
-    await element(by.cssContainingText('button', 'Create Bucket')).click(); // Clicks Create Bucket button
-    await this.waitTextToBePresent(this.getBreadcrumb(), 'Create');
-    // content in fields seems to subsist through tests if not cleared, so it is cleared
-    await nameInputField.clear();
-    return element(by.cssContainingText('button', 'Cancel')).click();
-  }
-
-  async testInvalidEdit(name: string) {
-    await this.navigateTo();
-
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // wait for table to load and click
-    await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page
-
-    await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
-
-    await expect(element(by.css('input[id=versioning]')).getAttribute('checked')).toBeFalsy();
-
-    // Chooses 'Select a user' rather than a valid owner on Edit Bucket page
-    // and checks if it's an invalid input
-
-    // select the first option, which is invalid because it is a placeholder
-    await this.selectOwner('Select a user');
-
-    // Changes when updated to bootstrap 4 -> Error message takes a long time to appear unless another field
-    // is clicked on. For that reason, I'm having the test click on the edit button before checking for errors
-    await element(by.cssContainingText('button', 'Edit Bucket')).click();
-
-    // Check that owner drop down field was marked invalid in the css
-    await expect(element(by.id('owner')).getAttribute('class')).toContain('ng-invalid');
-
-    // Check that error message was printed under owner drop down field
-    await expect(element(by.css('#owner + .invalid-feedback')).getText()).toMatch(
-      'This field is required.'
-    );
-
-    await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/daemons.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/daemons.e2e-spec.ts
deleted file mode 100644 (file)
index 0a9e551..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-import { DaemonsPageHelper } from './daemons.po';
-
-describe('RGW daemons page', () => {
-  let daemons: DaemonsPageHelper;
-
-  beforeAll(() => {
-    daemons = new DaemonsPageHelper();
-  });
-
-  afterEach(async () => {
-    await DaemonsPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb and tab tests', () => {
-    beforeAll(async () => {
-      await daemons.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await daemons.waitTextToBePresent(daemons.getBreadcrumb(), 'Daemons');
-    });
-
-    it('should show two tabs', async () => {
-      await expect(daemons.getTabsCount()).toEqual(2);
-    });
-
-    it('should show daemons list tab at first', async () => {
-      await expect(daemons.getTabText(0)).toEqual('Daemons List');
-    });
-
-    it('should show overall performance as a second tab', async () => {
-      await expect(daemons.getTabText(1)).toEqual('Overall Performance');
-    });
-  });
-
-  describe('details and performance counters table tests', () => {
-    beforeAll(async () => {
-      await daemons.navigateTo();
-    });
-
-    it('should check that details/performance tables are visible when daemon is selected', async () => {
-      await daemons.checkTables();
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/daemons.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/daemons.po.ts
deleted file mode 100644 (file)
index cd06380..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-import { $$, by, element } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-export class DaemonsPageHelper extends PageHelper {
-  pages = { index: '/#/rgw/daemon' };
-
-  async checkTables() {
-    await this.navigateTo();
-
-    // click on a daemon so details table appears
-    await $$('.datatable-body-cell-label').first().click();
-
-    const tab_container = $$('.tab-container').last();
-    const details_table = tab_container.all(by.css('cd-table')).get(0);
-    const performance_counters_table = tab_container.all(by.css('cd-table')).get(1);
-
-    // check details table is visible
-    await expect(details_table.isDisplayed()).toBe(true);
-    // check at least one field is present
-    await expect(details_table.getText()).toMatch('ceph_version');
-    // check performance counters table is not currently visible
-    await expect(performance_counters_table.isDisplayed()).toBe(false);
-
-    // click on performance counters tab and check table is loaded
-    await element(by.cssContainingText('.nav-link', 'Performance Counters')).click();
-    await expect(performance_counters_table.isDisplayed()).toBe(true);
-    // check at least one field is present
-    await expect(performance_counters_table.getText()).toMatch('objecter.op_r');
-    // check details table is not currently visible
-    await expect(details_table.isDisplayed()).toBe(false);
-
-    // click on performance details tab
-    await element(by.cssContainingText('.nav-link', 'Performance Details')).click();
-    // checks the other tabs' content isn't visible
-    await expect(details_table.isDisplayed()).toBe(false);
-    await expect(performance_counters_table.isDisplayed()).toBe(false);
-    // TODO: Expect Grafana iFrame
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.e2e-spec.ts
deleted file mode 100644 (file)
index 2e93046..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-import { UsersPageHelper } from './users.po';
-
-describe('RGW users page', () => {
-  let users: UsersPageHelper;
-  const user_name = '000user_create_edit_delete';
-
-  beforeAll(() => {
-    users = new UsersPageHelper();
-  });
-
-  afterEach(async () => {
-    await UsersPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb tests', () => {
-    beforeEach(async () => {
-      await users.navigateTo();
-    });
-
-    it('should open and show breadcrumb', async () => {
-      await users.waitTextToBePresent(users.getBreadcrumb(), 'Users');
-    });
-  });
-
-  describe('create, edit & delete user tests', () => {
-    beforeEach(async () => {
-      await users.navigateTo();
-      await users.uncheckAllTableRows();
-    });
-
-    it('should create user', async () => {
-      await users.navigateTo('create');
-      await users.create(user_name, 'Some Name', 'original@website.com', '1200');
-      await expect(users.getFirstTableCellWithText(user_name).isPresent()).toBe(true);
-    });
-
-    it('should edit users full name, email and max buckets', async () => {
-      await users.edit(user_name, 'Another Identity', 'changed@othersite.com', '1969');
-    });
-
-    it('should delete user', async () => {
-      await users.delete(user_name);
-    });
-  });
-
-  describe('Invalid input tests', () => {
-    it('should put invalid input into user creation form and check fields are marked invalid', async () => {
-      await users.invalidCreate();
-    });
-
-    it('should put invalid input into user edit form and check fields are marked invalid', async () => {
-      await users.invalidEdit();
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/rgw/users.po.ts
deleted file mode 100644 (file)
index 77aa572..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-import { $, by, element } from 'protractor';
-import { protractor } from 'protractor/built/ptor';
-import { PageHelper } from '../page-helper.po';
-
-const pages = {
-  index: '/#/rgw/user',
-  create: '/#/rgw/user/create'
-};
-
-export class UsersPageHelper extends PageHelper {
-  pages = pages;
-
-  @PageHelper.restrictTo(pages.create)
-  async create(username: string, fullname: string, email: string, maxbuckets: string) {
-    // Enter in  username
-    await element(by.id('uid')).sendKeys(username);
-
-    // Enter in full name
-    await element(by.id('display_name')).click();
-    await element(by.id('display_name')).sendKeys(fullname);
-
-    // Enter in email
-    await element(by.id('email')).click();
-    await element(by.id('email')).sendKeys(email);
-
-    // Enter max buckets
-    await this.selectOption('max_buckets_mode', 'Custom');
-    await element(by.id('max_buckets')).click();
-    await element(by.id('max_buckets')).clear();
-    await element(by.id('max_buckets')).sendKeys(maxbuckets);
-
-    // Click the create button and wait for user to be made
-    await element(by.cssContainingText('button', 'Create User')).click();
-    await this.waitPresence(this.getFirstTableCellWithText(username));
-  }
-
-  @PageHelper.restrictTo(pages.index)
-  async edit(name: string, new_fullname: string, new_email: string, new_maxbuckets: string) {
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(name)); // wait for table to load and click
-    await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page
-
-    await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
-
-    // Change the full name field
-    await element(by.id('display_name')).click();
-    await element(by.id('display_name')).clear();
-    await element(by.id('display_name')).sendKeys(new_fullname);
-
-    // Change the email field
-    await element(by.id('email')).click();
-    await element(by.id('email')).clear();
-    await element(by.id('email')).sendKeys(new_email);
-
-    // Change the max buckets field
-    await this.selectOption('max_buckets_mode', 'Custom');
-    await element(by.id('max_buckets')).click();
-    await element(by.id('max_buckets')).clear();
-    await element(by.id('max_buckets')).sendKeys(new_maxbuckets);
-
-    const editbutton = element(by.cssContainingText('button', 'Edit User'));
-    await editbutton.click();
-    // Click the user and check its details table for updated content
-    await this.waitClickableAndClick(this.getExpandCollapseElement(name));
-    await expect($('.active.tab-pane').getText()).toMatch(new_fullname); // check full name was changed
-    await expect($('.active.tab-pane').getText()).toMatch(new_email); // check email was changed
-    await expect($('.active.tab-pane').getText()).toMatch(new_maxbuckets); // check max buckets was changed
-  }
-
-  async invalidCreate() {
-    const uname = '000invalid_create_user';
-    // creating this user in order to check that you can't give two users the same name
-    await this.navigateTo('create');
-    await this.create(uname, 'xxx', 'xxx@xxx', '1');
-
-    await this.navigateTo('create');
-
-    const username_field = element(by.id('uid'));
-
-    // No username had been entered. Field should be invalid
-    await expect(username_field.getAttribute('class')).toContain('ng-invalid');
-
-    // Try to give user already taken name. Should make field invalid.
-    await username_field.clear();
-    await username_field.sendKeys(uname);
-    await expect(username_field.getAttribute('class')).toContain('ng-invalid');
-    await element(by.id('display_name')).click(); // trigger validation check
-    await expect(element(by.css('#uid + .invalid-feedback')).getText()).toMatch(
-      'The chosen user ID is already in use.'
-    );
-
-    // check that username field is marked invalid if username has been cleared off
-    await username_field.click();
-    for (let i = 0; i < uname.length; i++) {
-      await username_field.sendKeys(protractor.Key.BACK_SPACE);
-    }
-    await expect(username_field.getAttribute('class')).toContain('ng-invalid');
-    await element(by.id('display_name')).click(); // trigger validation check
-    await expect(element(by.css('#uid + .invalid-feedback')).getText()).toMatch(
-      'This field is required.'
-    );
-
-    // No display name has been given so field should be invalid
-    await expect(element(by.id('display_name')).getAttribute('class')).toContain('ng-invalid');
-
-    // display name field should also be marked invalid if given input then emptied
-    await element(by.id('display_name')).click();
-    await element(by.id('display_name')).sendKeys('a');
-    await element(by.id('display_name')).sendKeys(protractor.Key.BACK_SPACE);
-    await expect(element(by.id('display_name')).getAttribute('class')).toContain('ng-invalid');
-    await username_field.click(); // trigger validation check
-    await expect(element(by.css('#display_name + .invalid-feedback')).getText()).toMatch(
-      'This field is required.'
-    );
-
-    // put invalid email to make field invalid
-    await element(by.id('email')).click();
-    await element(by.id('email')).sendKeys('a');
-    await expect(element(by.id('email')).getAttribute('class')).toContain('ng-invalid');
-    await username_field.click(); // trigger validation check
-    await expect(element(by.css('#email + .invalid-feedback')).getText()).toMatch(
-      'This is not a valid email address.'
-    );
-
-    // put negative max buckets to make field invalid
-    await this.expectSelectOption('max_buckets_mode', 'Custom');
-    await element(by.id('max_buckets')).click();
-    await element(by.id('max_buckets')).clear();
-    await element(by.id('max_buckets')).sendKeys('-5');
-    await expect(element(by.id('max_buckets')).getAttribute('class')).toContain('ng-invalid');
-    await username_field.click(); // trigger validation check
-    await expect(element(by.css('#max_buckets + .invalid-feedback')).getText()).toMatch(
-      'The entered value must be >= 1.'
-    );
-
-    await this.navigateTo();
-    await this.delete(uname);
-  }
-
-  async invalidEdit() {
-    const uname = '000invalid_edit_user';
-    // creating this user to edit for the test
-    await this.navigateTo('create');
-    await this.create(uname, 'xxx', 'xxx@xxx', '1');
-
-    await this.navigateTo();
-
-    // wait for table to load and click on the bucket you want to edit in the table
-    await this.waitClickableAndClick(this.getFirstTableCellWithText(uname));
-    await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page
-
-    await this.waitTextToBePresent(this.getBreadcrumb(), 'Edit');
-
-    // put invalid email to make field invalid
-    await element(by.id('email')).click();
-    await element(by.id('email')).clear();
-    await element(by.id('email')).sendKeys('a');
-    await this.waitFn(
-      async () => !(await element(by.id('email')).getAttribute('class')).includes('ng-pending')
-    );
-    await expect(element(by.id('email')).getAttribute('class')).toContain('ng-invalid');
-    await element(by.id('display_name')).click(); // trigger validation check
-    await expect(element(by.css('#email + .invalid-feedback')).getText()).toMatch(
-      'This is not a valid email address.'
-    );
-
-    // empty the display name field making it invalid
-    await element(by.id('display_name')).click();
-    for (let i = 0; i < 3; i++) {
-      await element(by.id('display_name')).sendKeys(protractor.Key.BACK_SPACE);
-    }
-    await expect(element(by.id('display_name')).getAttribute('class')).toContain('ng-invalid');
-    await element(by.id('email')).click(); // trigger validation check
-    await expect(element(by.css('#display_name + .invalid-feedback')).getText()).toMatch(
-      'This field is required.'
-    );
-
-    // put negative max buckets to make field invalid
-    await this.expectSelectOption('max_buckets_mode', 'Custom');
-    await element(by.id('max_buckets')).click();
-    await element(by.id('max_buckets')).clear();
-    await element(by.id('max_buckets')).sendKeys('-5');
-    await expect(element(by.id('max_buckets')).getAttribute('class')).toContain('ng-invalid');
-    await element(by.id('email')).click(); // trigger validation check
-    await expect(element(by.css('#max_buckets + .invalid-feedback')).getText()).toMatch(
-      'The entered value must be >= 1.'
-    );
-
-    await this.navigateTo();
-    await this.delete(uname);
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/tsconfig.e2e.json b/src/pybind/mgr/dashboard/frontend/e2e/tsconfig.e2e.json
deleted file mode 100644 (file)
index e3d6ae7..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "extends": "../tsconfig.json",
-  "compilerOptions": {
-    "outDir": "../out-tsc/e2e",
-    "baseUrl": "./",
-    "module": "commonjs",
-    "target": "es5",
-    "types": [
-      "jasmine",
-      "jasminewd2",
-      "node"
-    ],
-    "noEmit": true
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/ui/dashboard.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/ui/dashboard.e2e-spec.ts
deleted file mode 100644 (file)
index d15c358..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-import { browser } from 'protractor';
-import { IscsiPageHelper } from '../block/iscsi.po';
-import { HostsPageHelper } from '../cluster/hosts.po';
-import { MonitorsPageHelper } from '../cluster/monitors.po';
-import { OSDsPageHelper } from '../cluster/osds.po';
-import { PageHelper } from '../page-helper.po';
-import { PoolPageHelper } from '../pools/pools.po';
-import { DaemonsPageHelper } from '../rgw/daemons.po';
-import { DashboardPageHelper } from './dashboard.po';
-
-describe('Dashboard Main Page', () => {
-  let dashboard: DashboardPageHelper;
-  let daemons: DaemonsPageHelper;
-  let hosts: HostsPageHelper;
-  let osds: OSDsPageHelper;
-  let pools: PoolPageHelper;
-  let monitors: MonitorsPageHelper;
-  let iscsi: IscsiPageHelper;
-
-  beforeAll(() => {
-    dashboard = new DashboardPageHelper();
-    daemons = new DaemonsPageHelper();
-    hosts = new HostsPageHelper();
-    osds = new OSDsPageHelper();
-    pools = new PoolPageHelper();
-    monitors = new MonitorsPageHelper();
-    iscsi = new IscsiPageHelper();
-  });
-
-  afterEach(async () => {
-    await DashboardPageHelper.checkConsole();
-  });
-
-  describe('Check that all hyperlinks on info cards lead to the correct page and fields exist', () => {
-    beforeEach(async () => {
-      await dashboard.navigateTo();
-    });
-
-    it('should ensure that all linked info cards lead to correct page', async () => {
-      const expectationMap = {
-        Monitors: 'Monitors',
-        OSDs: 'OSDs',
-        Hosts: 'Hosts',
-        'Object Gateways': 'Daemons',
-        'iSCSI Gateways': 'Overview',
-        Pools: 'Pools'
-      };
-
-      for (const [linkText, breadcrumbText] of Object.entries(expectationMap)) {
-        await expect(browser.getCurrentUrl()).toContain('/#/dashboard');
-        await dashboard.clickInfoCardLink(linkText);
-        await dashboard.waitTextToBePresent(dashboard.getBreadcrumb(), breadcrumbText);
-        await dashboard.navigateBack();
-      }
-    });
-
-    it('should verify that info cards exist on dashboard in proper order', async () => {
-      // Ensures that info cards are all displayed on the dashboard tab while being in the proper
-      // order, checks for card title and position via indexing into a list of all info cards.
-      const order = [
-        'Cluster Status',
-        'Monitors',
-        'OSDs',
-        'Manager Daemons',
-        'Hosts',
-        'Object Gateways',
-        'Metadata Servers',
-        'iSCSI Gateways',
-        'Client IOPS',
-        'Client Throughput',
-        'Client Read/Write',
-        'Recovery Throughput',
-        'Scrub',
-        'Pools',
-        'Raw Capacity',
-        'Objects',
-        'PGs per OSD',
-        'PG Status'
-      ];
-
-      for (let i = 0; i < order.length; i++) {
-        await expect((await dashboard.infoCard(i)).getText()).toContain(
-          order[i],
-          `Order of ${order[i]} seems to be wrong`
-        );
-      }
-    });
-
-    it('should verify that info card group titles are present and in the right order', async () => {
-      await expect(browser.getCurrentUrl()).toContain('/#/dashboard');
-      await expect(dashboard.infoGroupTitle(0)).toBe('Status');
-      await expect(dashboard.infoGroupTitle(1)).toBe('Performance');
-      await expect(dashboard.infoGroupTitle(2)).toBe('Capacity');
-    });
-  });
-
-  it('Should check that dashboard cards have correct information', async () => {
-    interface TestSpec {
-      cardName: string;
-      regexMatcher?: RegExp;
-      pageObject: PageHelper;
-    }
-
-    const testSpecs: TestSpec[] = [
-      { cardName: 'Object Gateways', regexMatcher: /(\d+)\s+total/, pageObject: daemons },
-      { cardName: 'Monitors', regexMatcher: /(\d+)\s+\(quorum/, pageObject: monitors },
-      { cardName: 'Hosts', regexMatcher: /(\d+)\s+total/, pageObject: hosts },
-      { cardName: 'OSDs', regexMatcher: /(\d+)\s+total/, pageObject: osds },
-      { cardName: 'Pools', pageObject: pools },
-      { cardName: 'iSCSI Gateways', regexMatcher: /(\d+)\s+total/, pageObject: iscsi }
-    ];
-
-    for (let i = 0; i < testSpecs.length; i++) {
-      const spec = testSpecs[i];
-      await dashboard.navigateTo();
-      const infoCardBodyText = await dashboard.infoCardBodyText(spec.cardName);
-      let dashCount = 0;
-      if (spec.regexMatcher) {
-        const match = infoCardBodyText.match(new RegExp(spec.regexMatcher));
-        if (match && match.length > 1) {
-          dashCount = Number(match[1]);
-        } else {
-          return Promise.reject(
-            `Regex ${spec.regexMatcher} did not find a match for card with name ` +
-              `${spec.cardName}`
-          );
-        }
-      } else {
-        dashCount = Number(infoCardBodyText);
-      }
-      await spec.pageObject.navigateTo();
-      const tableCount = await spec.pageObject.getTableTotalCount();
-      await expect(dashCount).toBe(
-        tableCount,
-        `Text of card "${spec.cardName}" and regex "${spec.regexMatcher}" resulted in ${dashCount} ` +
-          `but did not match table count ${tableCount}`
-      );
-    }
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/ui/dashboard.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/ui/dashboard.po.ts
deleted file mode 100644 (file)
index 1293d53..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-import { $, $$, by, ElementFinder } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-export class DashboardPageHelper extends PageHelper {
-  pages = {
-    index: '/#/dashboard'
-  };
-
-  async infoGroupTitle(index: number): Promise<string> {
-    return $$('.info-group-title').get(index).getText();
-  }
-
-  async clickInfoCardLink(cardName: string): Promise<void> {
-    await $(`cd-info-card[cardtitle="${cardName}"]`).element(by.linkText(cardName)).click();
-  }
-
-  async infoCard(indexOrTitle: number | string): Promise<ElementFinder> {
-    let infoCards = $$('cd-info-card');
-    if (typeof indexOrTitle === 'number') {
-      if ((await infoCards.count()) <= indexOrTitle) {
-        return Promise.reject(
-          `No element found for index ${indexOrTitle}. Elements array has ` +
-            `only ${await infoCards.count()} elements.`
-        );
-      }
-      return infoCards.get(indexOrTitle);
-    } else if (typeof indexOrTitle === 'string') {
-      infoCards = infoCards.filter(
-        async (e) => (await e.$('.card-title').getText()) === indexOrTitle
-      );
-      if ((await infoCards.count()) === 0) {
-        return Promise.reject(`No element found for title "${indexOrTitle}"`);
-      }
-      return infoCards.first();
-    }
-  }
-
-  async infoCardBodyText(
-    infoCard: ElementFinder | Promise<ElementFinder> | string
-  ): Promise<string> {
-    let _infoCard: ElementFinder;
-    if (typeof infoCard === 'string') {
-      _infoCard = await this.infoCard(infoCard);
-    } else {
-      _infoCard = typeof infoCard.then === 'function' ? await infoCard : infoCard;
-    }
-    return _infoCard.$('.card-text').getText();
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/ui/notification.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/ui/notification.e2e-spec.ts
deleted file mode 100644 (file)
index 3aedae5..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-import { PoolPageHelper } from '../pools/pools.po';
-import { NotificationSidebarPageHelper } from './notification.po';
-
-describe('Notification page', () => {
-  let notification: NotificationSidebarPageHelper;
-  let pools: PoolPageHelper;
-
-  beforeAll(() => {
-    notification = new NotificationSidebarPageHelper();
-    pools = new PoolPageHelper();
-  });
-
-  afterEach(async () => {
-    await NotificationSidebarPageHelper.checkConsole();
-  });
-
-  it('should open notification sidebar', async () => {
-    await notification.waitInvisibility(notification.getSidebar());
-    await notification.open();
-    await notification.waitVisibility(notification.getSidebar());
-  });
-
-  it('should display a running task', async () => {
-    const poolName = 'e2e_notification_pool';
-
-    await pools.navigateTo('create');
-    await pools.create(poolName, 8);
-    await pools.edit_pool_pg(poolName, 4, false);
-    await notification.waitStaleness(notification.getToast());
-
-    // Check that running task is shown.
-    await notification.open();
-    await notification.waitFn(async () => {
-      const task = await notification.getTasks().first();
-      const text = await task.getText();
-      return text.includes(poolName);
-    }, 'Timed out verifying task.');
-
-    // Delete pool after task is complete (otherwise we get an error).
-    await notification.waitFn(
-      async () => {
-        const tasks = await notification.getTasks();
-        return tasks.length === 0 ? true : !(await tasks[0].getText()).includes(poolName);
-      },
-      'Timed out waiting for task to complete.',
-      40000
-    );
-    await pools.delete(poolName);
-  });
-
-  it('should have notifications', async () => {
-    await notification.open();
-    await expect((await notification.getNotifications()).length).toBeGreaterThan(0);
-  });
-
-  it('should clear notifications', async () => {
-    await notification.waitStaleness(notification.getToast());
-    await expect((await notification.getNotifications()).length).toBeGreaterThan(0);
-    await notification.waitVisibility(notification.getClearNotficationsBtn());
-
-    // It can happen that although notifications are cleared, by the time we check the
-    // notifications amount, another notification can appear, so we check it more than once (if needed).
-    await notification.waitClickableAndClick(notification.getClearNotficationsBtn());
-    await notification.waitFn(async () => {
-      const notifications = await notification.getNotifications();
-      if (notifications.length > 0) {
-        await notification.waitClickableAndClick(notification.getClearNotficationsBtn());
-        return false;
-      }
-      return true;
-    }, 'Timed out checking that notifications are cleared.');
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/ui/notification.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/ui/notification.po.ts
deleted file mode 100644 (file)
index 68eeb61..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import { by, element } from 'protractor';
-
-import { PageHelper } from '../page-helper.po';
-
-export class NotificationSidebarPageHelper extends PageHelper {
-  getNotificatinoIcon() {
-    return element(by.css('cd-notifications a'));
-  }
-
-  getSidebar() {
-    return element(by.css('cd-notifications-sidebar'));
-  }
-
-  getTasks() {
-    return this.getSidebar().all(by.css('.card.tc_task'));
-  }
-
-  getNotifications() {
-    return this.getSidebar().all(by.css('.card.tc_notification'));
-  }
-
-  getClearNotficationsBtn() {
-    return this.getSidebar().element(by.css('button.btn-block'));
-  }
-
-  getCloseBtn() {
-    return this.getSidebar().element(by.css('button.close'));
-  }
-
-  async open() {
-    await this.waitClickableAndClick(this.getNotificatinoIcon());
-    return this.waitVisibility(this.getSidebar());
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/ui/role-mgmt.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/ui/role-mgmt.e2e-spec.ts
deleted file mode 100644 (file)
index badb3c9..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-import { RoleMgmtPageHelper } from './role-mgmt.po';
-
-describe('Role Management page', () => {
-  let roleMgmt: RoleMgmtPageHelper;
-  const role_name = 'user_mgmt_create_edit_delete_role';
-
-  beforeAll(() => {
-    roleMgmt = new RoleMgmtPageHelper();
-  });
-
-  afterEach(async () => {
-    await RoleMgmtPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb tests', () => {
-    it('should check breadcrumb on roles tab on user management page', async () => {
-      await roleMgmt.navigateTo();
-      await roleMgmt.waitTextToBePresent(roleMgmt.getBreadcrumb(), 'Roles');
-    });
-
-    it('should check breadcrumb on role creation page', async () => {
-      await roleMgmt.navigateTo('create');
-      await roleMgmt.waitTextToBePresent(roleMgmt.getBreadcrumb(), 'Create');
-    });
-  });
-
-  describe('role create, edit & delete test', () => {
-    it('should create a role', async () => {
-      await roleMgmt.create(role_name, 'An interesting description');
-    });
-
-    it('should edit a role', async () => {
-      await roleMgmt.edit(role_name, 'A far more interesting description');
-    });
-
-    it('should delete a role', async () => {
-      await roleMgmt.navigateTo();
-      await roleMgmt.delete(role_name);
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/ui/role-mgmt.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/ui/role-mgmt.po.ts
deleted file mode 100644 (file)
index dbec067..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-import { by, element } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-export class RoleMgmtPageHelper extends PageHelper {
-  pages = {
-    index: '/#/user-management/roles',
-    create: '/#/user-management/roles/create'
-  };
-
-  async create(name: string, description: string): Promise<void> {
-    await this.navigateTo('create');
-
-    // fill in fields
-    await element(by.id('name')).sendKeys(name);
-    await element(by.id('description')).sendKeys(description);
-
-    // Click the create button and wait for role to be made
-    const createButton = element(by.cssContainingText('button', 'Create Role'));
-    await createButton.click();
-
-    await this.waitPresence(this.getFirstTableCellWithText(name));
-  }
-
-  async edit(name: string, description: string): Promise<void> {
-    await this.navigateTo();
-
-    await this.getFirstTableCellWithText(name).click(); // select role from table
-    await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page
-
-    // fill in fields with new values
-    await element(by.id('description')).clear();
-    await element(by.id('description')).sendKeys(description);
-
-    // Click the edit button and check new values are present in table
-    const editButton = element(by.cssContainingText('button', 'Edit Role'));
-    await editButton.click();
-
-    await this.waitPresence(this.getFirstTableCellWithText(name));
-    await this.waitPresence(this.getFirstTableCellWithText(description));
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/ui/user-mgmt.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/ui/user-mgmt.e2e-spec.ts
deleted file mode 100644 (file)
index f40ce1f..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-import { UserMgmtPageHelper } from './user-mgmt.po';
-
-describe('User Management page', () => {
-  let userMgmt: UserMgmtPageHelper;
-  const user_name = 'user_mgmt_create_edit_delete_user';
-
-  beforeAll(() => {
-    userMgmt = new UserMgmtPageHelper();
-  });
-
-  afterEach(async () => {
-    await UserMgmtPageHelper.checkConsole();
-  });
-
-  describe('breadcrumb tests', () => {
-    it('should check breadcrumb on users tab of user management page', async () => {
-      await userMgmt.navigateTo();
-      await userMgmt.waitTextToBePresent(userMgmt.getBreadcrumb(), 'Users');
-    });
-
-    it('should check breadcrumb on user creation page', async () => {
-      await userMgmt.navigateTo('create');
-      await userMgmt.waitTextToBePresent(userMgmt.getBreadcrumb(), 'Create');
-    });
-  });
-
-  describe('user create, edit & delete test', () => {
-    it('should create a user', async () => {
-      await userMgmt.create(user_name, 'cool_password', 'Jeff', 'realemail@realwebsite.com');
-    });
-
-    it('should edit a user', async () => {
-      await userMgmt.edit(user_name, 'cool_password_number_2', 'Geoff', 'w@m');
-    });
-
-    it('should delete a user', async () => {
-      await userMgmt.navigateTo();
-      await userMgmt.delete(user_name);
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/e2e/ui/user-mgmt.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/ui/user-mgmt.po.ts
deleted file mode 100644 (file)
index d59eddb..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-import { by, element } from 'protractor';
-import { PageHelper } from '../page-helper.po';
-
-export class UserMgmtPageHelper extends PageHelper {
-  pages = {
-    index: '/#/user-management/users',
-    create: '/#/user-management/users/create'
-  };
-
-  async create(username: string, password: string, name: string, email: string): Promise<void> {
-    await this.navigateTo('create');
-
-    // fill in fields
-    await element(by.id('username')).sendKeys(username);
-    await element(by.id('password')).sendKeys(password);
-    await element(by.id('confirmpassword')).sendKeys(password);
-    await element(by.id('name')).sendKeys(name);
-    await element(by.id('email')).sendKeys(email);
-
-    // Click the create button and wait for user to be made
-    const createButton = element(by.cssContainingText('button', 'Create User'));
-    await createButton.click();
-    await this.waitPresence(this.getFirstTableCellWithText(username));
-  }
-
-  async edit(username: string, password: string, name: string, email: string): Promise<void> {
-    await this.navigateTo();
-
-    await this.getFirstTableCellWithText(username).click(); // select user from table
-    await element(by.cssContainingText('button', 'Edit')).click(); // click button to move to edit page
-
-    // fill in fields with new values
-    await element(by.id('password')).clear();
-    await element(by.id('password')).sendKeys(password);
-    await element(by.id('confirmpassword')).clear();
-    await element(by.id('confirmpassword')).sendKeys(password);
-    await element(by.id('name')).clear();
-    await element(by.id('name')).sendKeys(name);
-    await element(by.id('email')).clear();
-    await element(by.id('email')).sendKeys(email);
-
-    // Click the edit button and check new values are present in table
-    const editButton = element(by.cssContainingText('button', 'Edit User'));
-    await editButton.click();
-    await this.waitPresence(this.getFirstTableCellWithText(email));
-    await this.waitPresence(this.getFirstTableCellWithText(name));
-  }
-}
index 4dc78438ac1d4c46d12bff9d1660602d232c7d15..139d7887769e2245414c9d6decfeb8f48a5a8aa8 100644 (file)
         "worker-plugin": "3.2.0"
       },
       "dependencies": {
+        "core-js": {
+          "version": "3.6.4",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
+          "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==",
+          "dev": true
+        },
         "glob": {
           "version": "7.1.4",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
         "convert-source-map": "^1.5.1",
         "dependency-graph": "^0.7.2",
         "magic-string": "^0.25.0",
-        "minimist": "1.2.5",
+        "minimist": "^1.2.0",
         "reflect-metadata": "^0.1.2",
         "source-map": "^0.6.1",
         "tslib": "^1.9.0",
               }
             },
             "minimist": {
+              "version": "1.2.5",
+              "bundled": true,
               "dev": true,
-              "optional": true,
-              "version": "1.2.5"
+              "optional": true
             },
             "minipass": {
               "version": "2.9.0",
               "dev": true,
               "optional": true,
               "requires": {
-                "minimist": "1.2.5"
-              },
-              "dependencies": {
-                "minimist": {
-                  "version": "1.2.5"
-                }
+                "minimist": "^1.2.5"
               }
             },
             "ms": {
               "requires": {
                 "deep-extend": "^0.6.0",
                 "ini": "~1.3.0",
-                "minimist": "1.2.5",
+                "minimist": "^1.2.0",
                 "strip-json-comments": "~2.0.1"
-              },
-              "dependencies": {
-                "minimist": {
-                  "version": "1.2.5"
-                }
               }
             },
             "readable-stream": {
           "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
           "dev": true
         },
-        "minimist": {
-          "version": "1.2.5"
-        },
         "os-locale": {
           "version": "3.1.0",
           "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
           }
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
       },
       "dependencies": {
         "json5": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.2.tgz",
-          "integrity": "sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==",
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
+          "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
           "dev": true,
           "requires": {
-            "minimist": "1.2.5"
-          },
-          "dependencies": {
-            "minimist": {
-              "version": "1.2.5"
-            }
+            "minimist": "^1.2.5"
           }
         },
         "semver": {
       }
     },
     "@babel/generator": {
-      "version": "7.9.4",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.4.tgz",
-      "integrity": "sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==",
+      "version": "7.9.5",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz",
+      "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.9.0",
+        "@babel/types": "^7.9.5",
         "jsesc": "^2.5.1",
         "lodash": "^4.17.13",
         "source-map": "^0.5.0"
       }
     },
     "@babel/helper-function-name": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz",
-      "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==",
+      "version": "7.9.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz",
+      "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==",
       "dev": true,
       "requires": {
         "@babel/helper-get-function-arity": "^7.8.3",
         "@babel/template": "^7.8.3",
-        "@babel/types": "^7.8.3"
+        "@babel/types": "^7.9.5"
       }
     },
     "@babel/helper-get-function-arity": {
       }
     },
     "@babel/helper-validator-identifier": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz",
-      "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==",
+      "version": "7.9.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz",
+      "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==",
       "dev": true
     },
     "@babel/helper-wrap-function": {
       }
     },
     "@babel/plugin-proposal-object-rest-spread": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz",
-      "integrity": "sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow==",
+      "version": "7.9.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.5.tgz",
+      "integrity": "sha512-VP2oXvAf7KCYTthbUHwBlewbl1Iq059f6seJGsxMizaCdgHIeczOr7FBqELhSqfkIl04Fi8okzWzl63UKbQmmg==",
       "dev": true,
       "requires": {
         "@babel/helper-plugin-utils": "^7.8.3",
-        "@babel/plugin-syntax-object-rest-spread": "^7.8.0"
+        "@babel/plugin-syntax-object-rest-spread": "^7.8.0",
+        "@babel/plugin-transform-parameters": "^7.9.5"
       }
     },
     "@babel/plugin-proposal-optional-catch-binding": {
         "@babel/helper-plugin-utils": "^7.8.0"
       }
     },
+    "@babel/plugin-syntax-class-properties": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz",
+      "integrity": "sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.8.3"
+      }
+    },
     "@babel/plugin-syntax-dynamic-import": {
       "version": "7.8.3",
       "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
         "@babel/helper-plugin-utils": "^7.8.0"
       }
     },
+    "@babel/plugin-syntax-logical-assignment-operators": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz",
+      "integrity": "sha512-Zpg2Sgc++37kuFl6ppq2Q7Awc6E6AIW671x5PY8E/f7MCIyPPGK/EoeZXvvY3P42exZ3Q4/t3YOzP/HiN79jDg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.8.3"
+      }
+    },
     "@babel/plugin-syntax-nullish-coalescing-operator": {
       "version": "7.8.3",
       "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
         "@babel/helper-plugin-utils": "^7.8.0"
       }
     },
+    "@babel/plugin-syntax-numeric-separator": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz",
+      "integrity": "sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.8.3"
+      }
+    },
     "@babel/plugin-syntax-object-rest-spread": {
       "version": "7.8.3",
       "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
       }
     },
     "@babel/plugin-transform-classes": {
-      "version": "7.9.2",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz",
-      "integrity": "sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ==",
+      "version": "7.9.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.5.tgz",
+      "integrity": "sha512-x2kZoIuLC//O5iA7PEvecB105o7TLzZo8ofBVhP79N+DO3jaX+KYfww9TQcfBEZD0nikNyYcGB1IKtRq36rdmg==",
       "dev": true,
       "requires": {
         "@babel/helper-annotate-as-pure": "^7.8.3",
         "@babel/helper-define-map": "^7.8.3",
-        "@babel/helper-function-name": "^7.8.3",
+        "@babel/helper-function-name": "^7.9.5",
         "@babel/helper-optimise-call-expression": "^7.8.3",
         "@babel/helper-plugin-utils": "^7.8.3",
         "@babel/helper-replace-supers": "^7.8.6",
       }
     },
     "@babel/plugin-transform-destructuring": {
-      "version": "7.8.8",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz",
-      "integrity": "sha512-eRJu4Vs2rmttFCdhPUM3bV0Yo/xPSdPw6ML9KHs/bjB4bLA5HXlbvYXPOD5yASodGod+krjYx21xm1QmL8dCJQ==",
+      "version": "7.9.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.9.5.tgz",
+      "integrity": "sha512-j3OEsGel8nHL/iusv/mRd5fYZ3DrOxWC82x0ogmdN/vHfAP4MYw+AFKYanzWlktNwikKvlzUV//afBW5FTp17Q==",
       "dev": true,
       "requires": {
         "@babel/helper-plugin-utils": "^7.8.3"
       }
     },
     "@babel/plugin-transform-parameters": {
-      "version": "7.9.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz",
-      "integrity": "sha512-fzrQFQhp7mIhOzmOtPiKffvCYQSK10NR8t6BBz2yPbeUHb9OLW8RZGtgDRBn8z2hGcwvKDL3vC7ojPTLNxmqEg==",
+      "version": "7.9.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.5.tgz",
+      "integrity": "sha512-0+1FhHnMfj6lIIhVvS4KGQJeuhe1GI//h5uptK4PvLt+BGBxsoUJbd3/IW002yk//6sZPlFgsG1hY6OHLcy6kA==",
       "dev": true,
       "requires": {
         "@babel/helper-get-function-arity": "^7.8.3",
       }
     },
     "@babel/traverse": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.0.tgz",
-      "integrity": "sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==",
+      "version": "7.9.5",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz",
+      "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.8.3",
-        "@babel/generator": "^7.9.0",
-        "@babel/helper-function-name": "^7.8.3",
+        "@babel/generator": "^7.9.5",
+        "@babel/helper-function-name": "^7.9.5",
         "@babel/helper-split-export-declaration": "^7.8.3",
         "@babel/parser": "^7.9.0",
-        "@babel/types": "^7.9.0",
+        "@babel/types": "^7.9.5",
         "debug": "^4.1.0",
         "globals": "^11.1.0",
         "lodash": "^4.17.13"
       }
     },
     "@babel/types": {
-      "version": "7.9.0",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz",
-      "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==",
+      "version": "7.9.5",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz",
+      "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==",
       "dev": true,
       "requires": {
-        "@babel/helper-validator-identifier": "^7.9.0",
+        "@babel/helper-validator-identifier": "^7.9.5",
         "lodash": "^4.17.13",
         "to-fast-properties": "^2.0.0"
       }
       "dev": true,
       "requires": {
         "exec-sh": "^0.3.2",
-        "minimist": "1.2.5"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "1.2.5"
-        }
+        "minimist": "^1.2.0"
       }
     },
     "@compodoc/compodoc": {
         "loglevel-plugin-prefix": "^0.8.4",
         "lunr": "^2.3.6",
         "marked": "^0.7.0",
-        "minimist": "1.2.5",
+        "minimist": "^1.2.0",
         "opencollective-postinstall": "^2.0.2",
         "os-name": "^3.1.0",
         "pdfmake": "^0.1.60",
           }
         },
         "json5": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.2.tgz",
-          "integrity": "sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==",
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
+          "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
           "dev": true,
           "requires": {
-            "minimist": "1.2.5"
-          },
-          "dependencies": {
-            "minimist": {
-              "version": "1.2.5"
-            }
+            "minimist": "^1.2.5"
           }
-        },
-        "minimist": {
-          "version": "1.2.5"
         }
       }
     },
         }
       }
     },
+    "@cypress/listr-verbose-renderer": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz",
+      "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "cli-cursor": "^1.0.2",
+        "date-fns": "^1.27.2",
+        "figures": "^1.7.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "cli-cursor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
+          "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^1.0.1"
+          }
+        },
+        "figures": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+          "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5",
+            "object-assign": "^4.1.0"
+          }
+        },
+        "onetime": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+          "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
+          "dev": true
+        },
+        "restore-cursor": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
+          "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
+          "dev": true,
+          "requires": {
+            "exit-hook": "^1.0.0",
+            "onetime": "^1.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "@cypress/request": {
+      "version": "2.88.5",
+      "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz",
+      "integrity": "sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA==",
+      "dev": true,
+      "requires": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "har-validator": "~5.1.3",
+        "http-signature": "~1.2.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "oauth-sign": "~0.9.0",
+        "performance-now": "^2.1.0",
+        "qs": "~6.5.2",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "~2.5.0",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^3.3.2"
+      }
+    },
+    "@cypress/xvfb": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
+      "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.1.0",
+        "lodash.once": "^4.1.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
+      }
+    },
     "@dsherret/to-absolute-glob": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",
         "is-negated-glob": "^1.0.0"
       }
     },
+    "@hapi/address": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
+      "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==",
+      "dev": true
+    },
+    "@hapi/formula": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-1.2.0.tgz",
+      "integrity": "sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA==",
+      "dev": true
+    },
+    "@hapi/hoek": {
+      "version": "8.5.1",
+      "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz",
+      "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==",
+      "dev": true
+    },
+    "@hapi/joi": {
+      "version": "16.1.8",
+      "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-16.1.8.tgz",
+      "integrity": "sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg==",
+      "dev": true,
+      "requires": {
+        "@hapi/address": "^2.1.2",
+        "@hapi/formula": "^1.2.0",
+        "@hapi/hoek": "^8.2.4",
+        "@hapi/pinpoint": "^1.0.2",
+        "@hapi/topo": "^3.1.3"
+      }
+    },
+    "@hapi/pinpoint": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-1.0.2.tgz",
+      "integrity": "sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ==",
+      "dev": true
+    },
+    "@hapi/topo": {
+      "version": "3.1.6",
+      "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz",
+      "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==",
+      "dev": true,
+      "requires": {
+        "@hapi/hoek": "^8.3.0"
+      }
+    },
     "@istanbuljs/load-nyc-config": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz",
           }
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
       "dev": true
     },
     "@jest/console": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.2.3.tgz",
-      "integrity": "sha512-k+37B1aSvOt9tKHWbZZSOy1jdgzesB0bj96igCVUG1nAH1W5EoUfgc5EXbBVU08KSLvkVdWopLXaO3xfVGlxtQ==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.4.0.tgz",
+      "integrity": "sha512-CfE0erx4hdJ6t7RzAcE1wLG6ZzsHSmybvIBQDoCkDM1QaSeWL9wJMzID/2BbHHa7ll9SsbbK43HjbERbBaFX2A==",
       "dev": true,
       "requires": {
-        "@jest/source-map": "^25.2.1",
+        "@jest/types": "^25.4.0",
         "chalk": "^3.0.0",
-        "jest-util": "^25.2.3",
+        "jest-message-util": "^25.4.0",
+        "jest-util": "^25.4.0",
         "slash": "^3.0.0"
       },
       "dependencies": {
       }
     },
     "@jest/core": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.2.4.tgz",
-      "integrity": "sha512-WcWYShl0Bqfcb32oXtjwbiR78D/djhMdJW+ulp4/bmHgeODcsieqUJfUH+kEv8M7VNV77E6jds5aA+WuGh1nmg==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.4.0.tgz",
+      "integrity": "sha512-h1x9WSVV0+TKVtATGjyQIMJENs8aF6eUjnCoi4jyRemYZmekLr8EJOGQqTWEX8W6SbZ6Skesy9pGXrKeAolUJw==",
       "dev": true,
       "requires": {
-        "@jest/console": "^25.2.3",
-        "@jest/reporters": "^25.2.4",
-        "@jest/test-result": "^25.2.4",
-        "@jest/transform": "^25.2.4",
-        "@jest/types": "^25.2.3",
+        "@jest/console": "^25.4.0",
+        "@jest/reporters": "^25.4.0",
+        "@jest/test-result": "^25.4.0",
+        "@jest/transform": "^25.4.0",
+        "@jest/types": "^25.4.0",
         "ansi-escapes": "^4.2.1",
         "chalk": "^3.0.0",
         "exit": "^0.1.2",
         "graceful-fs": "^4.2.3",
-        "jest-changed-files": "^25.2.3",
-        "jest-config": "^25.2.4",
-        "jest-haste-map": "^25.2.3",
-        "jest-message-util": "^25.2.4",
-        "jest-regex-util": "^25.2.1",
-        "jest-resolve": "^25.2.3",
-        "jest-resolve-dependencies": "^25.2.4",
-        "jest-runner": "^25.2.4",
-        "jest-runtime": "^25.2.4",
-        "jest-snapshot": "^25.2.4",
-        "jest-util": "^25.2.3",
-        "jest-validate": "^25.2.3",
-        "jest-watcher": "^25.2.4",
+        "jest-changed-files": "^25.4.0",
+        "jest-config": "^25.4.0",
+        "jest-haste-map": "^25.4.0",
+        "jest-message-util": "^25.4.0",
+        "jest-regex-util": "^25.2.6",
+        "jest-resolve": "^25.4.0",
+        "jest-resolve-dependencies": "^25.4.0",
+        "jest-runner": "^25.4.0",
+        "jest-runtime": "^25.4.0",
+        "jest-snapshot": "^25.4.0",
+        "jest-util": "^25.4.0",
+        "jest-validate": "^25.4.0",
+        "jest-watcher": "^25.4.0",
         "micromatch": "^4.0.2",
         "p-each-series": "^2.1.0",
         "realpath-native": "^2.0.0",
       }
     },
     "@jest/environment": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.2.4.tgz",
-      "integrity": "sha512-wA4xlhD19/gukkDpJ5HQsTle0pgnzI5qMFEjw267lpTDC8d9N7Ihqr5pI+l0p8Qn1SQhai+glSqxrGdzKy4jxw==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.4.0.tgz",
+      "integrity": "sha512-KDctiak4mu7b4J6BIoN/+LUL3pscBzoUCP+EtSPd2tK9fqyDY5OF+CmkBywkFWezS9tyH5ACOQNtpjtueEDH6Q==",
       "dev": true,
       "requires": {
-        "@jest/fake-timers": "^25.2.4",
-        "@jest/types": "^25.2.3",
-        "jest-mock": "^25.2.3"
+        "@jest/fake-timers": "^25.4.0",
+        "@jest/types": "^25.4.0",
+        "jest-mock": "^25.4.0"
       }
     },
     "@jest/fake-timers": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.2.4.tgz",
-      "integrity": "sha512-oC1TJiwfMcBttVN7Wz+VZnqEAgYTiEMu0QLOXpypR89nab0uCB31zm/QeBZddhSstn20qe3yqOXygp6OwvKT/Q==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.4.0.tgz",
+      "integrity": "sha512-lI9z+VOmVX4dPPFzyj0vm+UtaB8dCJJ852lcDnY0uCPRvZAaVGnMwBBc1wxtf+h7Vz6KszoOvKAt4QijDnHDkg==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3",
-        "jest-message-util": "^25.2.4",
-        "jest-mock": "^25.2.3",
-        "jest-util": "^25.2.3",
+        "@jest/types": "^25.4.0",
+        "jest-message-util": "^25.4.0",
+        "jest-mock": "^25.4.0",
+        "jest-util": "^25.4.0",
         "lolex": "^5.0.0"
       }
     },
     "@jest/reporters": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.2.4.tgz",
-      "integrity": "sha512-VHbLxM03jCc+bTLOluW/IqHR2G0Cl0iATwIQbuZtIUast8IXO4fD0oy4jpVGpG5b20S6REA8U3BaQoCW/CeVNQ==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.4.0.tgz",
+      "integrity": "sha512-bhx/buYbZgLZm4JWLcRJ/q9Gvmd3oUh7k2V7gA4ZYBx6J28pIuykIouclRdiAC6eGVX1uRZT+GK4CQJLd/PwPg==",
       "dev": true,
       "requires": {
         "@bcoe/v8-coverage": "^0.2.3",
-        "@jest/console": "^25.2.3",
-        "@jest/test-result": "^25.2.4",
-        "@jest/transform": "^25.2.4",
-        "@jest/types": "^25.2.3",
+        "@jest/console": "^25.4.0",
+        "@jest/test-result": "^25.4.0",
+        "@jest/transform": "^25.4.0",
+        "@jest/types": "^25.4.0",
         "chalk": "^3.0.0",
         "collect-v8-coverage": "^1.0.0",
         "exit": "^0.1.2",
         "istanbul-lib-instrument": "^4.0.0",
         "istanbul-lib-report": "^3.0.0",
         "istanbul-lib-source-maps": "^4.0.0",
-        "istanbul-reports": "^3.0.0",
-        "jest-haste-map": "^25.2.3",
-        "jest-resolve": "^25.2.3",
-        "jest-util": "^25.2.3",
-        "jest-worker": "^25.2.1",
+        "istanbul-reports": "^3.0.2",
+        "jest-haste-map": "^25.4.0",
+        "jest-resolve": "^25.4.0",
+        "jest-util": "^25.4.0",
+        "jest-worker": "^25.4.0",
         "node-notifier": "^6.0.0",
         "slash": "^3.0.0",
         "source-map": "^0.6.0",
         "string-length": "^3.1.0",
         "terminal-link": "^2.0.0",
-        "v8-to-istanbul": "^4.0.1"
+        "v8-to-istanbul": "^4.1.3"
       },
       "dependencies": {
         "ansi-styles": {
           "dev": true
         },
         "jest-worker": {
-          "version": "25.2.1",
-          "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.2.1.tgz",
-          "integrity": "sha512-IHnpekk8H/hCUbBlfeaPZzU6v75bqwJp3n4dUrQuQOAgOneI4tx3jV2o8pvlXnDfcRsfkFIUD//HWXpCmR+evQ==",
+          "version": "25.4.0",
+          "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.4.0.tgz",
+          "integrity": "sha512-ghAs/1FtfYpMmYQ0AHqxV62XPvKdUDIBBApMZfly+E9JEmYh2K45G0R5dWxx986RN12pRCxsViwQVtGl+N4whw==",
           "dev": true,
           "requires": {
             "merge-stream": "^2.0.0",
       }
     },
     "@jest/source-map": {
-      "version": "25.2.1",
-      "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.2.1.tgz",
-      "integrity": "sha512-PgScGJm1U27+9Te/cxP4oUFqJ2PX6NhBL2a6unQ7yafCgs8k02c0LSyjSIx/ao0AwcAdCczfAPDf5lJ7zoB/7A==",
+      "version": "25.2.6",
+      "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.2.6.tgz",
+      "integrity": "sha512-VuIRZF8M2zxYFGTEhkNSvQkUKafQro4y+mwUxy5ewRqs5N/ynSFUODYp3fy1zCnbCMy1pz3k+u57uCqx8QRSQQ==",
       "dev": true,
       "requires": {
         "callsites": "^3.0.0",
       }
     },
     "@jest/test-result": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.2.4.tgz",
-      "integrity": "sha512-AI7eUy+q2lVhFnaibDFg68NGkrxVWZdD6KBr9Hm6EvN0oAe7GxpEwEavgPfNHQjU2mi6g+NsFn/6QPgTUwM1qg==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.4.0.tgz",
+      "integrity": "sha512-8BAKPaMCHlL941eyfqhWbmp3MebtzywlxzV+qtngQ3FH+RBqnoSAhNEPj4MG7d2NVUrMOVfrwuzGpVIK+QnMAA==",
       "dev": true,
       "requires": {
-        "@jest/console": "^25.2.3",
-        "@jest/transform": "^25.2.4",
-        "@jest/types": "^25.2.3",
+        "@jest/console": "^25.4.0",
+        "@jest/types": "^25.4.0",
         "@types/istanbul-lib-coverage": "^2.0.0",
         "collect-v8-coverage": "^1.0.0"
       }
     },
     "@jest/test-sequencer": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.2.4.tgz",
-      "integrity": "sha512-TEZm/Rkd6YgskdpTJdYLBtu6Gc11tfWPuSpatq0duH77ekjU8dpqX2zkPdY/ayuHxztV5LTJoV5BLtI9mZfXew==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.4.0.tgz",
+      "integrity": "sha512-240cI+nsM3attx2bMp9uGjjHrwrpvxxrZi8Tyqp/cfOzl98oZXVakXBgxODGyBYAy/UGXPKXLvNc2GaqItrsJg==",
       "dev": true,
       "requires": {
-        "@jest/test-result": "^25.2.4",
-        "jest-haste-map": "^25.2.3",
-        "jest-runner": "^25.2.4",
-        "jest-runtime": "^25.2.4"
+        "@jest/test-result": "^25.4.0",
+        "jest-haste-map": "^25.4.0",
+        "jest-runner": "^25.4.0",
+        "jest-runtime": "^25.4.0"
       }
     },
     "@jest/transform": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.2.4.tgz",
-      "integrity": "sha512-6eRigvb+G6bs4kW5j1/y8wu4nCrmVuIe0epPBbiWaYlwawJ8yi1EIyK3d/btDqmBpN5GpN4YhR6iPPnDmkYdTA==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.4.0.tgz",
+      "integrity": "sha512-t1w2S6V1sk++1HHsxboWxPEuSpN8pxEvNrZN+Ud/knkROWtf8LeUmz73A4ezE8476a5AM00IZr9a8FO9x1+j3g==",
       "dev": true,
       "requires": {
         "@babel/core": "^7.1.0",
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "babel-plugin-istanbul": "^6.0.0",
         "chalk": "^3.0.0",
         "convert-source-map": "^1.4.0",
         "fast-json-stable-stringify": "^2.0.0",
         "graceful-fs": "^4.2.3",
-        "jest-haste-map": "^25.2.3",
-        "jest-regex-util": "^25.2.1",
-        "jest-util": "^25.2.3",
+        "jest-haste-map": "^25.4.0",
+        "jest-regex-util": "^25.2.6",
+        "jest-util": "^25.4.0",
         "micromatch": "^4.0.2",
         "pirates": "^4.0.1",
         "realpath-native": "^2.0.0",
       }
     },
     "@jest/types": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.2.3.tgz",
-      "integrity": "sha512-6oLQwO9mKif3Uph3RX5J1i3S7X7xtDHWBaaaoeKw8hOzV6YUd0qDcYcHZ6QXMHDIzSr7zzrEa51o2Ovlj6AtKQ==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.4.0.tgz",
+      "integrity": "sha512-XBeaWNzw2PPnGW5aXvZt3+VO60M+34RY3XDsCK5tW7kyj3RK0XClRutCfjqcBuaR2aBQTbluEDME9b5MB9UAPw==",
       "dev": true,
       "requires": {
         "@types/istanbul-lib-coverage": "^2.0.0",
       "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
       "dev": true
     },
+    "@samverschueren/stream-to-observable": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
+      "integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==",
+      "dev": true,
+      "requires": {
+        "any-observable": "^0.3.0"
+      }
+    },
     "@schematics/angular": {
       "version": "8.3.26",
       "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-8.3.26.tgz",
       }
     },
     "@sinonjs/commons": {
-      "version": "1.7.1",
-      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz",
-      "integrity": "sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ==",
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.2.tgz",
+      "integrity": "sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw==",
       "dev": true,
       "requires": {
         "type-detect": "4.0.8"
       }
     },
     "@types/babel__core": {
-      "version": "7.1.6",
-      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.6.tgz",
-      "integrity": "sha512-tTnhWszAqvXnhW7m5jQU9PomXSiKXk2sFxpahXvI20SZKu9ylPi8WtIxueZ6ehDWikPT0jeFujMj3X4ZHuf3Tg==",
+      "version": "7.1.7",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.7.tgz",
+      "integrity": "sha512-RL62NqSFPCDK2FM1pSDH0scHpJvsXtZNiYlMB73DgPBaG1E38ZYVL+ei5EkWRbr+KC4YNiAUNBnRj+bgwpgjMw==",
       "dev": true,
       "requires": {
         "@babel/parser": "^7.1.0",
       }
     },
     "@types/babel__traverse": {
-      "version": "7.0.9",
-      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.9.tgz",
-      "integrity": "sha512-jEFQ8L1tuvPjOI8lnpaf73oCJe+aoxL6ygqSy6c8LcW98zaC+4mzWuQIRCEvKeCOu+lbqdXcg4Uqmm1S8AP1tw==",
+      "version": "7.0.10",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.10.tgz",
+      "integrity": "sha512-74fNdUGrWsgIB/V9kTO5FGHPWYY6Eqn+3Z7L6Hc4e/BxjYV7puvBqp5HwsVYYfLm6iURYBNCx4Ut37OF9yitCw==",
       "dev": true,
       "requires": {
         "@babel/types": "^7.3.0"
       }
     },
+    "@types/blob-util": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz",
+      "integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==",
+      "dev": true
+    },
+    "@types/bluebird": {
+      "version": "3.5.29",
+      "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.29.tgz",
+      "integrity": "sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==",
+      "dev": true
+    },
+    "@types/chai": {
+      "version": "4.2.7",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz",
+      "integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==",
+      "dev": true
+    },
+    "@types/chai-jquery": {
+      "version": "1.1.40",
+      "resolved": "https://registry.npmjs.org/@types/chai-jquery/-/chai-jquery-1.1.40.tgz",
+      "integrity": "sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ==",
+      "dev": true,
+      "requires": {
+        "@types/chai": "*",
+        "@types/jquery": "*"
+      }
+    },
     "@types/chart.js": {
-      "version": "2.9.18",
-      "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.18.tgz",
-      "integrity": "sha512-D7oaYQqYGdfoa1Wv9doxQJ9Sv/W7jfbiXMT/wVRiM0AsPJsHWLRn7U46xhDkPRGmLCpQGlN2oZYBIwEpMMleog==",
+      "version": "2.9.19",
+      "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.19.tgz",
+      "integrity": "sha512-sFxlMb+ElfJelXh0Z8spmiLRrnXCd7CaT6WGQtckhjETK1H5i1nYKN4TOExhqPeDZ6u+w4bJ20UYqELWOEfAKQ==",
       "requires": {
         "moment": "^2.10.2"
       }
         "@types/istanbul-lib-report": "*"
       }
     },
-    "@types/jasmine": {
-      "version": "3.5.10",
-      "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.10.tgz",
-      "integrity": "sha512-3F8qpwBAiVc5+HPJeXJpbrl+XjawGmciN5LgiO7Gv1pl1RHtjoMNqZpqEksaPJW05ViKe8snYInRs6xB25Xdew==",
-      "dev": true
-    },
-    "@types/jasminewd2": {
-      "version": "2.0.8",
-      "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.8.tgz",
-      "integrity": "sha512-d9p31r7Nxk0ZH0U39PTH0hiDlJ+qNVGjlt1ucOoTUptxb2v+Y5VMnsxfwN+i3hK4yQnqBi3FMmoMFcd1JHDxdg==",
-      "dev": true,
-      "requires": {
-        "@types/jasmine": "*"
-      }
-    },
     "@types/jest": {
       "version": "25.1.4",
       "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.1.4.tgz",
         "pretty-format": "^25.1.0"
       }
     },
+    "@types/jquery": {
+      "version": "3.3.31",
+      "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz",
+      "integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
+      "dev": true,
+      "requires": {
+        "@types/sizzle": "*"
+      }
+    },
     "@types/lodash": {
       "version": "4.14.149",
       "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
       "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
       "dev": true
     },
+    "@types/mocha": {
+      "version": "5.2.7",
+      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
+      "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
+      "dev": true
+    },
     "@types/node": {
       "version": "12.12.34",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.34.tgz",
       "integrity": "sha512-BneGN0J9ke24lBRn44hVHNeDlrXRYF+VRp0HbSUNnEZahXGAysHZIqnf/hER6aabdBgzM4YOV4jrR8gj4Zfi0g==",
       "dev": true
     },
+    "@types/normalize-package-data": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+      "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
+      "dev": true
+    },
     "@types/prettier": {
       "version": "1.19.1",
       "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz",
       "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==",
       "dev": true
     },
-    "@types/q": {
-      "version": "0.0.32",
-      "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz",
-      "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=",
-      "dev": true
-    },
-    "@types/selenium-webdriver": {
-      "version": "3.0.17",
-      "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz",
-      "integrity": "sha512-tGomyEuzSC1H28y2zlW6XPCaDaXFaD6soTdb4GNdmte2qfHtrKqhy0ZFs4r/1hpazCfEZqeTSRLvSasmEx89uw==",
-      "dev": true
-    },
     "@types/simplebar": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/@types/simplebar/-/simplebar-5.1.1.tgz",
       "integrity": "sha512-nC9iBQ4dfvvzJ3iAbL1qCfwjUyaF8EO56l/ApcRXUFK2zLOb8GDXC55V08JZvpzkUxGHtWVunp17KKH/3/KFJA==",
       "dev": true
     },
+    "@types/sinon": {
+      "version": "7.5.1",
+      "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.1.tgz",
+      "integrity": "sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==",
+      "dev": true
+    },
+    "@types/sinon-chai": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.3.tgz",
+      "integrity": "sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ==",
+      "dev": true,
+      "requires": {
+        "@types/chai": "*",
+        "@types/sinon": "*"
+      }
+    },
+    "@types/sizzle": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
+      "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
+      "dev": true
+    },
     "@types/source-list-map": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
       "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==",
       "dev": true
     },
-    "adm-zip": {
-      "version": "0.4.14",
-      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.14.tgz",
-      "integrity": "sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g==",
-      "dev": true
-    },
     "agent-base": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
       "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=",
       "dev": true
     },
+    "any-observable": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
+      "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==",
+      "dev": true
+    },
     "anymatch": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
       "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
       "dev": true
     },
+    "arch": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz",
+      "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==",
+      "dev": true
+    },
     "arg": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
       }
     },
     "babel-jest": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.2.4.tgz",
-      "integrity": "sha512-+yDzlyJVWrqih9i2Cvjpt7COaN8vUwCsKGtxJLzg6I0xhxD54K8mvDUCliPKLufyzHh/c5C4MRj4Vk7VMjOjIg==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.4.0.tgz",
+      "integrity": "sha512-p+epx4K0ypmHuCnd8BapfyOwWwosNCYhedetQey1awddtfmEX0MmdxctGl956uwUmjwXR5VSS5xJcGX9DvdIog==",
       "dev": true,
       "requires": {
-        "@jest/transform": "^25.2.4",
-        "@jest/types": "^25.2.3",
-        "@types/babel__core": "^7.1.0",
+        "@jest/transform": "^25.4.0",
+        "@jest/types": "^25.4.0",
+        "@types/babel__core": "^7.1.7",
         "babel-plugin-istanbul": "^6.0.0",
-        "babel-preset-jest": "^25.2.1",
+        "babel-preset-jest": "^25.4.0",
         "chalk": "^3.0.0",
         "slash": "^3.0.0"
       },
       }
     },
     "babel-plugin-dynamic-import-node": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz",
-      "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==",
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
+      "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==",
       "dev": true,
       "requires": {
         "object.assign": "^4.1.0"
       }
     },
     "babel-plugin-jest-hoist": {
-      "version": "25.2.1",
-      "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.2.1.tgz",
-      "integrity": "sha512-HysbCQfJhxLlyxDbKcB2ucGYV0LjqK4h6dBoI3RtFuOxTiTWK6XGZMsHb0tGh8iJdV4hC6Z2GCHzVvDeh9i0lQ==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.4.0.tgz",
+      "integrity": "sha512-M3a10JCtTyKevb0MjuH6tU+cP/NVQZ82QPADqI1RQYY1OphztsCeIeQmTsHmF/NS6m0E51Zl4QNsI3odXSQF5w==",
       "dev": true,
       "requires": {
         "@types/babel__traverse": "^7.0.6"
       }
     },
+    "babel-preset-current-node-syntax": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.2.tgz",
+      "integrity": "sha512-u/8cS+dEiK1SFILbOC8/rUI3ml9lboKuuMvZ/4aQnQmhecQAgPw5ew066C1ObnEAUmlx7dv/s2z52psWEtLNiw==",
+      "dev": true,
+      "requires": {
+        "@babel/plugin-syntax-async-generators": "^7.8.4",
+        "@babel/plugin-syntax-bigint": "^7.8.3",
+        "@babel/plugin-syntax-class-properties": "^7.8.3",
+        "@babel/plugin-syntax-json-strings": "^7.8.3",
+        "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3",
+        "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+        "@babel/plugin-syntax-numeric-separator": "^7.8.3",
+        "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+        "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+        "@babel/plugin-syntax-optional-chaining": "^7.8.3"
+      }
+    },
     "babel-preset-jest": {
-      "version": "25.2.1",
-      "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.2.1.tgz",
-      "integrity": "sha512-zXHJBM5iR8oEO4cvdF83AQqqJf3tJrXy3x8nfu2Nlqvn4cneg4Ca8M7cQvC5S9BzDDy1O0tZ9iXru9J6E3ym+A==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.4.0.tgz",
+      "integrity": "sha512-PwFiEWflHdu3JCeTr0Pb9NcHHE34qWFnPQRVPvqQITx4CsDCzs6o05923I10XvLvn9nNsRHuiVgB72wG/90ZHQ==",
       "dev": true,
       "requires": {
-        "@babel/plugin-syntax-bigint": "^7.0.0",
-        "@babel/plugin-syntax-object-rest-spread": "^7.0.0",
-        "babel-plugin-jest-hoist": "^25.2.1"
+        "babel-plugin-jest-hoist": "^25.4.0",
+        "babel-preset-current-node-syntax": "^0.1.2"
       }
     },
     "babel-runtime": {
       "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
       "dev": true
     },
-    "blocking-proxy": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz",
-      "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==",
-      "dev": true,
-      "requires": {
-        "minimist": "1.2.5"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "1.2.5"
-        }
-      }
-    },
     "bluebird": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
         "pkg-up": "^3.1.0"
       }
     },
-    "browserstack": {
-      "version": "1.5.3",
-      "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.3.tgz",
-      "integrity": "sha512-AO+mECXsW4QcqC9bxwM29O7qWa7bJT94uBFzeb5brylIQwawuEziwq20dPYbins95GlWzOawgyDNdjYAo32EKg==",
-      "dev": true,
-      "requires": {
-        "https-proxy-agent": "^2.2.1"
-      }
-    },
     "bs-logger": {
       "version": "0.2.6",
       "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
         "isarray": "^1.0.0"
       }
     },
+    "buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
+      "dev": true
+    },
     "buffer-equal": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
         "unset-value": "^1.0.0"
       }
     },
+    "cachedir": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
+      "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==",
+      "dev": true
+    },
     "call-me-maybe": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
         "color-name": "^1.0.0"
       }
     },
+    "check-more-types": {
+      "version": "2.24.0",
+      "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
+      "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=",
+      "dev": true
+    },
     "cheerio": {
       "version": "1.0.0-rc.3",
       "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz",
       }
     },
     "chokidar": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
-      "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
+      "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==",
       "dev": true,
       "requires": {
         "anymatch": "~3.1.1",
         "is-binary-path": "~2.1.0",
         "is-glob": "~4.0.1",
         "normalize-path": "~3.0.0",
-        "readdirp": "~3.3.0"
+        "readdirp": "~3.4.0"
       },
       "dependencies": {
         "fsevents": {
       "integrity": "sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw==",
       "dev": true
     },
-    "circular-json": {
-      "version": "0.5.9",
-      "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz",
-      "integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==",
-      "dev": true
-    },
     "cjson": {
       "version": "0.5.0",
       "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.5.0.tgz",
         "restore-cursor": "^3.1.0"
       }
     },
+    "cli-table3": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz",
+      "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==",
+      "dev": true,
+      "requires": {
+        "colors": "^1.1.2",
+        "object-assign": "^4.1.0",
+        "string-width": "^2.1.1"
+      }
+    },
+    "cli-truncate": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz",
+      "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=",
+      "dev": true,
+      "requires": {
+        "slice-ansi": "0.0.4",
+        "string-width": "^1.0.1"
+      },
+      "dependencies": {
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        }
+      }
+    },
     "cli-width": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
-      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
+      "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
       "dev": true
     },
     "cliui": {
       }
     },
     "collect-v8-coverage": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.0.tgz",
-      "integrity": "sha512-VKIhJgvk8E1W28m5avZ2Gv2Ruv5YiF56ug2oclvaG9md69BuZImMG2sk9g7QNKLUbtYAKQjXjYxbYZVUlMMKmQ==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
+      "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==",
       "dev": true
     },
     "collection-visit": {
       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
       "dev": true
     },
+    "common-tags": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz",
+      "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==",
+      "dev": true
+    },
     "commondir": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
           }
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
       }
     },
     "core-js": {
-      "version": "3.6.4",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
-      "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw=="
+      "version": "3.6.5",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
+      "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA=="
     },
     "core-js-compat": {
-      "version": "3.6.4",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.4.tgz",
-      "integrity": "sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA==",
+      "version": "3.6.5",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz",
+      "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==",
       "dev": true,
       "requires": {
-        "browserslist": "^4.8.3",
+        "browserslist": "^4.8.5",
         "semver": "7.0.0"
       },
       "dependencies": {
       },
       "dependencies": {
         "ajv": {
-          "version": "6.12.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
-          "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
+          "version": "6.12.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
+          "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
           "dev": true,
           "requires": {
             "fast-deep-equal": "^3.1.1",
           "dev": true
         },
         "schema-utils": {
-          "version": "2.6.5",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz",
-          "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==",
+          "version": "2.6.6",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz",
+          "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==",
           "dev": true,
           "requires": {
             "ajv": "^6.12.0",
       "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
       "dev": true
     },
+    "cypress": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/cypress/-/cypress-4.4.0.tgz",
+      "integrity": "sha512-ZpsV3pVemANGi4Cxu0UIqFv23uHdDJZYlKY+8P/eixujCpI1TQ5RSPBp2grfV3ZvlGYrOXPJY44j9iEh1xoQug==",
+      "dev": true,
+      "requires": {
+        "@cypress/listr-verbose-renderer": "0.4.1",
+        "@cypress/request": "2.88.5",
+        "@cypress/xvfb": "1.2.4",
+        "@types/blob-util": "1.3.3",
+        "@types/bluebird": "3.5.29",
+        "@types/chai": "4.2.7",
+        "@types/chai-jquery": "1.1.40",
+        "@types/jquery": "3.3.31",
+        "@types/lodash": "4.14.149",
+        "@types/minimatch": "3.0.3",
+        "@types/mocha": "5.2.7",
+        "@types/sinon": "7.5.1",
+        "@types/sinon-chai": "3.2.3",
+        "@types/sizzle": "2.3.2",
+        "arch": "2.1.1",
+        "bluebird": "3.7.2",
+        "cachedir": "2.3.0",
+        "chalk": "2.4.2",
+        "check-more-types": "2.24.0",
+        "cli-table3": "0.5.1",
+        "commander": "4.1.0",
+        "common-tags": "1.8.0",
+        "debug": "4.1.1",
+        "eventemitter2": "4.1.2",
+        "execa": "1.0.0",
+        "executable": "4.1.1",
+        "extract-zip": "1.7.0",
+        "fs-extra": "8.1.0",
+        "getos": "3.1.4",
+        "is-ci": "2.0.0",
+        "is-installed-globally": "0.1.0",
+        "lazy-ass": "1.6.0",
+        "listr": "0.14.3",
+        "lodash": "4.17.15",
+        "log-symbols": "3.0.0",
+        "minimist": "1.2.5",
+        "moment": "2.24.0",
+        "ospath": "1.2.2",
+        "pretty-bytes": "5.3.0",
+        "ramda": "0.26.1",
+        "request-progress": "3.0.0",
+        "supports-color": "7.1.0",
+        "tmp": "0.1.0",
+        "untildify": "4.0.0",
+        "url": "0.11.0",
+        "yauzl": "2.10.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.0.tgz",
+          "integrity": "sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw==",
+          "dev": true
+        },
+        "cross-spawn": {
+          "version": "6.0.5",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+          "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+          "dev": true,
+          "requires": {
+            "nice-try": "^1.0.4",
+            "path-key": "^2.0.1",
+            "semver": "^5.5.0",
+            "shebang-command": "^1.2.0",
+            "which": "^1.2.9"
+          }
+        },
+        "execa": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+          "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+          "dev": true,
+          "requires": {
+            "cross-spawn": "^6.0.0",
+            "get-stream": "^4.0.0",
+            "is-stream": "^1.1.0",
+            "npm-run-path": "^2.0.0",
+            "p-finally": "^1.0.0",
+            "signal-exit": "^3.0.0",
+            "strip-eof": "^1.0.0"
+          }
+        },
+        "get-stream": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+          "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+          "dev": true,
+          "requires": {
+            "pump": "^3.0.0"
+          }
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "semver": {
+          "version": "5.7.1",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "7.1.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+          "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "tmp": {
+          "version": "0.1.0",
+          "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz",
+          "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==",
+          "dev": true,
+          "requires": {
+            "rimraf": "^2.6.3"
+          }
+        }
+      }
+    },
     "d": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
         "whatwg-url": "^7.0.0"
       }
     },
+    "date-fns": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
+      "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
+      "dev": true
+    },
     "debug": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
       "dev": true
     },
     "diff-sequences": {
-      "version": "25.2.1",
-      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.1.tgz",
-      "integrity": "sha512-foe7dXnGlSh3jR1ovJmdv+77VQj98eKCHHwJPbZ2eEf0fHwKbkZicpPxEch9smZ+n2dnF6QFwkOQdLq9hpeJUg==",
+      "version": "25.2.6",
+      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz",
+      "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==",
       "dev": true
     },
     "diffie-hellman": {
       "dev": true
     },
     "electron-to-chromium": {
-      "version": "1.3.391",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.391.tgz",
-      "integrity": "sha512-WOi6loSnDmfICOqGRrgeK7bZeWDAbGjCptDhI5eyJAqSzWfoeRuOOU1rOTZRL29/9AaxTndZB6Uh8YrxRfZJqw==",
+      "version": "1.3.418",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.418.tgz",
+      "integrity": "sha512-i2QrQtHes5fK/F9QGG5XacM5WKEuR322fxTYF9e8O9Gu0mc0WmjjwGpV8c7Htso6Zf2Di18lc3SIPxmMeRFBug==",
+      "dev": true
+    },
+    "elegant-spinner": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz",
+      "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=",
       "dev": true
     },
     "elliptic": {
       "dev": true
     },
     "escodegen": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz",
-      "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==",
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz",
+      "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==",
       "dev": true,
       "requires": {
-        "esprima": "^3.1.3",
+        "esprima": "^4.0.1",
         "estraverse": "^4.2.0",
         "esutils": "^2.0.2",
         "optionator": "^0.8.1",
         "source-map": "~0.6.1"
       },
       "dependencies": {
-        "esprima": {
-          "version": "3.1.3",
-          "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
-          "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
-          "dev": true
-        },
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
         "through": "~2.3.1"
       }
     },
+    "eventemitter2": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-4.1.2.tgz",
+      "integrity": "sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU=",
+      "dev": true
+    },
     "eventemitter3": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz",
         "strip-eof": "^1.0.0"
       }
     },
+    "executable": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
+      "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==",
+      "dev": true,
+      "requires": {
+        "pify": "^2.2.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
     "exit": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
       "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
       "dev": true
     },
+    "exit-hook": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
+      "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=",
+      "dev": true
+    },
     "expand-brackets": {
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
       }
     },
     "expect": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/expect/-/expect-25.2.4.tgz",
-      "integrity": "sha512-hfuPhPds4yOsZtIw4kwAg70r0hqGmpqekgA+VX7pf/3wZ6FY+xIOXZhNsPMMMsspYG/YIsbAiwqsdnD4Ht+bCA==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/expect/-/expect-25.4.0.tgz",
+      "integrity": "sha512-7BDIX99BTi12/sNGJXA9KMRcby4iAmu1xccBOhyKCyEhjcVKS3hPmHdA/4nSI9QGIOkUropKqr3vv7WMDM5lvQ==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "ansi-styles": "^4.0.0",
-        "jest-get-type": "^25.2.1",
-        "jest-matcher-utils": "^25.2.3",
-        "jest-message-util": "^25.2.4",
-        "jest-regex-util": "^25.2.1"
+        "jest-get-type": "^25.2.6",
+        "jest-matcher-utils": "^25.4.0",
+        "jest-message-util": "^25.4.0",
+        "jest-regex-util": "^25.2.6"
       },
       "dependencies": {
         "ansi-styles": {
         }
       }
     },
-    "extsprintf": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
-      "dev": true
-    },
-    "falafel": {
-      "version": "2.2.4",
-      "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.4.tgz",
-      "integrity": "sha512-0HXjo8XASWRmsS0X1EkhwEMZaD3Qvp7FfURwjLKjG1ghfRm/MGZl2r4cWUTv41KdNghTw4OUMmVtdGQp3+H+uQ==",
+    "extract-zip": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
+      "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==",
       "dev": true,
       "requires": {
-        "acorn": "^7.1.1",
-        "foreach": "^2.0.5",
-        "isarray": "^2.0.1",
-        "object-keys": "^1.0.6"
+        "concat-stream": "^1.6.2",
+        "debug": "^2.6.9",
+        "mkdirp": "^0.5.4",
+        "yauzl": "^2.10.0"
       },
       "dependencies": {
-        "acorn": {
-          "version": "7.1.1",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
-          "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
-          "dev": true
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
         },
-        "isarray": {
-          "version": "2.0.5",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
-          "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
           "dev": true
         }
       }
     },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+      "dev": true
+    },
     "fancy-log": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz",
         "bser": "2.1.1"
       }
     },
+    "fd-slicer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+      "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
+      "dev": true,
+      "requires": {
+        "pend": "~1.2.0"
+      }
+    },
     "figgy-pudding": {
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
       },
       "dependencies": {
         "ajv": {
-          "version": "6.12.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
-          "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
+          "version": "6.12.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
+          "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
           "dev": true,
           "requires": {
             "fast-deep-equal": "^3.1.1",
           "dev": true
         },
         "schema-utils": {
-          "version": "2.6.5",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz",
-          "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==",
+          "version": "2.6.6",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz",
+          "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==",
           "dev": true,
           "requires": {
             "ajv": "^6.12.0",
           }
         },
         "make-dir": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz",
-          "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==",
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+          "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
           "dev": true,
           "requires": {
             "semver": "^6.0.0"
           }
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
       }
     },
     "fontkit": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.8.0.tgz",
-      "integrity": "sha512-EFDRCca7khfQWYu1iFhsqeABpi87f03MBdkT93ZE6YhqCdMzb5Eojb6c4dlJikGv5liuhByyzA7ikpIPTSBWbQ==",
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.8.1.tgz",
+      "integrity": "sha512-BsNCjDoYRxmNWFdAuK1y9bQt+igIxGtTC9u/jSFjR9MKhmI00rP1fwSvERt+5ddE82544l0XH5mzXozQVUy2Tw==",
       "dev": true,
       "requires": {
-        "babel-runtime": "^6.11.6",
-        "brfs": "^1.4.0",
+        "babel-runtime": "^6.26.0",
+        "brfs": "^2.0.0",
         "brotli": "^1.2.0",
-        "browserify-optional": "^1.0.0",
-        "clone": "^1.0.1",
+        "browserify-optional": "^1.0.1",
+        "clone": "^1.0.4",
         "deep-equal": "^1.0.0",
-        "dfa": "^1.0.0",
+        "dfa": "^1.2.0",
         "restructure": "^0.5.3",
         "tiny-inflate": "^1.0.2",
-        "unicode-properties": "^1.0.0",
+        "unicode-properties": "^1.2.2",
         "unicode-trie": "^0.3.0"
       },
       "dependencies": {
-        "brfs": {
-          "version": "1.6.1",
-          "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz",
-          "integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==",
-          "dev": true,
-          "requires": {
-            "quote-stream": "^1.0.1",
-            "resolve": "^1.1.5",
-            "static-module": "^2.2.0",
-            "through2": "^2.0.0"
-          }
-        },
         "clone": {
           "version": "1.0.4",
           "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
           "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
           "dev": true
         },
-        "magic-string": {
-          "version": "0.22.5",
-          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
-          "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==",
-          "dev": true,
-          "requires": {
-            "vlq": "^0.2.2"
-          }
-        },
-        "merge-source-map": {
-          "version": "1.0.4",
-          "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz",
-          "integrity": "sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=",
-          "dev": true,
-          "requires": {
-            "source-map": "^0.5.6"
-          }
-        },
-        "object-inspect": {
-          "version": "1.4.1",
-          "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz",
-          "integrity": "sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==",
-          "dev": true
-        },
         "pako": {
           "version": "0.2.9",
           "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
           "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=",
           "dev": true
         },
-        "source-map": {
-          "version": "0.5.7",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
-          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
-          "dev": true
-        },
-        "static-module": {
-          "version": "2.2.5",
-          "resolved": "https://registry.npmjs.org/static-module/-/static-module-2.2.5.tgz",
-          "integrity": "sha512-D8vv82E/Kpmz3TXHKG8PPsCPg+RAX6cbCOyvjM6x04qZtQ47EtJFVwRsdov3n5d6/6ynrOY9XB4JkaZwB2xoRQ==",
-          "dev": true,
-          "requires": {
-            "concat-stream": "~1.6.0",
-            "convert-source-map": "^1.5.1",
-            "duplexer2": "~0.1.4",
-            "escodegen": "~1.9.0",
-            "falafel": "^2.1.0",
-            "has": "^1.0.1",
-            "magic-string": "^0.22.4",
-            "merge-source-map": "1.0.4",
-            "object-inspect": "~1.4.0",
-            "quote-stream": "~1.0.2",
-            "readable-stream": "~2.3.3",
-            "shallow-copy": "~0.0.1",
-            "static-eval": "^2.0.0",
-            "through2": "~2.0.3"
-          }
-        },
         "unicode-trie": {
           "version": "0.3.1",
           "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz",
         "for-in": "^1.0.1"
       }
     },
-    "foreach": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
-      "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=",
-      "dev": true
-    },
     "forever-agent": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
       "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
       "dev": true
     },
+    "getos": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/getos/-/getos-3.1.4.tgz",
+      "integrity": "sha512-UORPzguEB/7UG5hqiZai8f0vQ7hzynMQyJLxStoQ8dPGAcmgsfXOPA4iE/fGtweHYkK+z4zc9V0g+CIFRf5HYw==",
+      "dev": true,
+      "requires": {
+        "async": "^3.1.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
+          "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==",
+          "dev": true
+        }
+      }
+    },
     "getpass": {
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
       "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=",
       "dev": true
     },
+    "global-dirs": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz",
+      "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=",
+      "dev": true,
+      "requires": {
+        "ini": "^1.3.4"
+      }
+    },
     "global-modules": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
       "dev": true
     },
     "handlebars": {
-      "version": "4.7.3",
-      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.3.tgz",
-      "integrity": "sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg==",
+      "version": "4.7.6",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz",
+      "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==",
       "dev": true,
       "requires": {
+        "minimist": "^1.2.5",
         "neo-async": "^2.6.0",
-        "optimist": "^0.6.1",
         "source-map": "^0.6.1",
-        "uglify-js": "^3.1.4"
+        "uglify-js": "^3.1.4",
+        "wordwrap": "^1.0.0"
       },
       "dependencies": {
         "source-map": {
       }
     },
     "html-entities": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
-      "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz",
+      "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==",
       "dev": true
     },
     "html-escaper": {
       "dev": true,
       "optional": true
     },
-    "immediate": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
-      "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
-      "dev": true
-    },
     "import-cwd": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
       "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
       "dev": true
     },
+    "indent-string": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz",
+      "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=",
+      "dev": true
+    },
     "infer-owner": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
         "is-extglob": "^2.1.1"
       }
     },
+    "is-installed-globally": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz",
+      "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=",
+      "dev": true,
+      "requires": {
+        "global-dirs": "^0.1.0",
+        "is-path-inside": "^1.0.0"
+      },
+      "dependencies": {
+        "is-path-inside": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+          "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+          "dev": true,
+          "requires": {
+            "path-is-inside": "^1.0.1"
+          }
+        }
+      }
+    },
     "is-negated-glob": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
       "dev": true
     },
+    "is-observable": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz",
+      "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==",
+      "dev": true,
+      "requires": {
+        "symbol-observable": "^1.1.0"
+      }
+    },
     "is-path-cwd": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
       }
     },
     "is-promise": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
-      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+      "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
       "dev": true
     },
     "is-regex": {
           "dev": true
         },
         "make-dir": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz",
-          "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==",
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+          "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
           "dev": true,
           "requires": {
             "semver": "^6.0.0"
       }
     },
     "istanbul-reports": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
-      "integrity": "sha512-Vm9xwCiQ8t2cNNnckyeAV0UdxKpcQUz4nMxsBvIu8n2kmPSiyb5uaF/8LpmKr+yqL/MdOXaX2Nmdo4Qyxium9Q==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz",
+      "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==",
       "dev": true,
       "requires": {
         "html-escaper": "^2.0.0",
         "istanbul-lib-report": "^3.0.0"
       }
     },
-    "jasmine": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz",
-      "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=",
-      "dev": true,
-      "requires": {
-        "exit": "^0.1.2",
-        "glob": "^7.0.6",
-        "jasmine-core": "~2.8.0"
-      },
-      "dependencies": {
-        "jasmine-core": {
-          "version": "2.8.0",
-          "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz",
-          "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=",
-          "dev": true
-        }
-      }
-    },
-    "jasmine-core": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
-      "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
-      "dev": true
-    },
-    "jasmine-fail-fast": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/jasmine-fail-fast/-/jasmine-fail-fast-2.0.1.tgz",
-      "integrity": "sha512-En8ONwvDQOV+jyiZEZvbvUSLWSdJFj9HiWjhLdGq/V/gxs4XyST730ooe928BbRxv4bfy05OpykKuoOU4aLC5w==",
-      "dev": true,
-      "requires": {
-        "lodash": "^4.17.15"
-      }
-    },
-    "jasmine-spec-reporter": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-5.0.1.tgz",
-      "integrity": "sha512-RrOZ+bSPnbk1/9KKs5lm0Nl0cqDCh/XXVlCmu3nkhEJH6HTDh4hoJZu3q8e9aq37C0eXEf/JEJnYy+t4m3arZQ==",
-      "dev": true,
-      "requires": {
-        "colors": "1.4.0"
-      }
-    },
-    "jasminewd2": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz",
-      "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=",
-      "dev": true
-    },
     "jest": {
       "version": "25.2.4",
       "resolved": "https://registry.npmjs.org/jest/-/jest-25.2.4.tgz",
           "dev": true
         },
         "jest-cli": {
-          "version": "25.2.4",
-          "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.2.4.tgz",
-          "integrity": "sha512-zeY2pRDWKj2LZudIncvvguwLMEdcnJqc2jJbwza1beqi80qqLvkPF/BjbFkK2sIV3r+mfTJS+7ITrvK6pCdRjg==",
+          "version": "25.4.0",
+          "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.4.0.tgz",
+          "integrity": "sha512-usyrj1lzCJZMRN1r3QEdnn8e6E6yCx/QN7+B1sLoA68V7f3WlsxSSQfy0+BAwRiF4Hz2eHauf11GZG3PIfWTXQ==",
           "dev": true,
           "requires": {
-            "@jest/core": "^25.2.4",
-            "@jest/test-result": "^25.2.4",
-            "@jest/types": "^25.2.3",
+            "@jest/core": "^25.4.0",
+            "@jest/test-result": "^25.4.0",
+            "@jest/types": "^25.4.0",
             "chalk": "^3.0.0",
             "exit": "^0.1.2",
             "import-local": "^3.0.2",
             "is-ci": "^2.0.0",
-            "jest-config": "^25.2.4",
-            "jest-util": "^25.2.3",
-            "jest-validate": "^25.2.3",
+            "jest-config": "^25.4.0",
+            "jest-util": "^25.4.0",
+            "jest-validate": "^25.4.0",
             "prompts": "^2.0.1",
             "realpath-native": "^2.0.0",
             "yargs": "^15.3.1"
           }
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
           }
         },
         "yargs-parser": {
-          "version": "18.1.2",
-          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz",
-          "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==",
+          "version": "18.1.3",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+          "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
           "dev": true,
           "requires": {
             "camelcase": "^5.0.0",
       }
     },
     "jest-changed-files": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.2.3.tgz",
-      "integrity": "sha512-EFxy94dvvbqRB36ezIPLKJ4fDIC+jAdNs8i8uTwFpaXd6H3LVc3ova1lNS4ZPWk09OCR2vq5kSdSQgar7zMORg==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.4.0.tgz",
+      "integrity": "sha512-VR/rfJsEs4BVMkwOTuStRyS630fidFVekdw/lBaBQjx9KK3VZFOZ2c0fsom2fRp8pMCrCTP6LGna00o/DXGlqA==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "execa": "^3.2.0",
         "throat": "^5.0.0"
       },
       "dependencies": {
         "cross-spawn": {
-          "version": "7.0.1",
-          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz",
-          "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==",
+          "version": "7.0.2",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz",
+          "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==",
           "dev": true,
           "requires": {
             "path-key": "^3.1.0",
       }
     },
     "jest-config": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.2.4.tgz",
-      "integrity": "sha512-fxy3nIpwJqOUQJRVF/q+pNQb6dv5b9YufOeCbpPZJ/md1zXpiupbhfehpfODhnKOfqbzSiigtSLzlWWmbRxnqQ==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.4.0.tgz",
+      "integrity": "sha512-egT9aKYxMyMSQV1aqTgam0SkI5/I2P9qrKexN5r2uuM2+68ypnc+zPGmfUxK7p1UhE7dYH9SLBS7yb+TtmT1AA==",
       "dev": true,
       "requires": {
         "@babel/core": "^7.1.0",
-        "@jest/test-sequencer": "^25.2.4",
-        "@jest/types": "^25.2.3",
-        "babel-jest": "^25.2.4",
+        "@jest/test-sequencer": "^25.4.0",
+        "@jest/types": "^25.4.0",
+        "babel-jest": "^25.4.0",
         "chalk": "^3.0.0",
         "deepmerge": "^4.2.2",
         "glob": "^7.1.1",
-        "jest-environment-jsdom": "^25.2.4",
-        "jest-environment-node": "^25.2.4",
-        "jest-get-type": "^25.2.1",
-        "jest-jasmine2": "^25.2.4",
-        "jest-regex-util": "^25.2.1",
-        "jest-resolve": "^25.2.3",
-        "jest-util": "^25.2.3",
-        "jest-validate": "^25.2.3",
+        "jest-environment-jsdom": "^25.4.0",
+        "jest-environment-node": "^25.4.0",
+        "jest-get-type": "^25.2.6",
+        "jest-jasmine2": "^25.4.0",
+        "jest-regex-util": "^25.2.6",
+        "jest-resolve": "^25.4.0",
+        "jest-util": "^25.4.0",
+        "jest-validate": "^25.4.0",
         "micromatch": "^4.0.2",
-        "pretty-format": "^25.2.3",
+        "pretty-format": "^25.4.0",
         "realpath-native": "^2.0.0"
       },
       "dependencies": {
       }
     },
     "jest-diff": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.2.3.tgz",
-      "integrity": "sha512-VtZ6LAQtaQpFsmEzps15dQc5ELbJxy4L2DOSo2Ev411TUEtnJPkAMD7JneVypeMJQ1y3hgxN9Ao13n15FAnavg==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.4.0.tgz",
+      "integrity": "sha512-kklLbJVXW0y8UKOWOdYhI6TH5MG6QAxrWiBMgQaPIuhj3dNFGirKCd+/xfplBXICQ7fI+3QcqHm9p9lWu1N6ug==",
       "dev": true,
       "requires": {
         "chalk": "^3.0.0",
-        "diff-sequences": "^25.2.1",
-        "jest-get-type": "^25.2.1",
-        "pretty-format": "^25.2.3"
+        "diff-sequences": "^25.2.6",
+        "jest-get-type": "^25.2.6",
+        "pretty-format": "^25.4.0"
       },
       "dependencies": {
         "ansi-styles": {
       }
     },
     "jest-docblock": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.2.3.tgz",
-      "integrity": "sha512-d3/tmjLLrH5fpRGmIm3oFa3vOaD/IjPxtXVOrfujpfJ9y1tCDB1x/tvunmdOVAyF03/xeMwburl6ITbiQT1mVA==",
+      "version": "25.3.0",
+      "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz",
+      "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==",
       "dev": true,
       "requires": {
         "detect-newline": "^3.0.0"
       }
     },
     "jest-each": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.2.3.tgz",
-      "integrity": "sha512-RTlmCjsBDK2c9T5oO4MqccA3/5Y8BUtiEy7OOQik1iyCgdnNdHbI0pNEpyapZPBG0nlvZ4mIu7aY6zNUvLraAQ==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.4.0.tgz",
+      "integrity": "sha512-lwRIJ8/vQU/6vq3nnSSUw1Y3nz5tkYSFIywGCZpUBd6WcRgpn8NmJoQICojbpZmsJOJNHm0BKdyuJ6Xdx+eDQQ==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "chalk": "^3.0.0",
-        "jest-get-type": "^25.2.1",
-        "jest-util": "^25.2.3",
-        "pretty-format": "^25.2.3"
+        "jest-get-type": "^25.2.6",
+        "jest-util": "^25.4.0",
+        "pretty-format": "^25.4.0"
       },
       "dependencies": {
         "ansi-styles": {
       }
     },
     "jest-environment-jsdom": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.2.4.tgz",
-      "integrity": "sha512-5dm+tNwrLmhELdjAwiQnVGf/U9iFMWdTL4/wyrMg2HU6RQnCiuxpWbIigLHUhuP1P2Ak0F4k3xhjrikboKyShA==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.4.0.tgz",
+      "integrity": "sha512-KTitVGMDrn2+pt7aZ8/yUTuS333w3pWt1Mf88vMntw7ZSBNDkRS6/4XLbFpWXYfWfp1FjcjQTOKzbK20oIehWQ==",
       "dev": true,
       "requires": {
-        "@jest/environment": "^25.2.4",
-        "@jest/fake-timers": "^25.2.4",
-        "@jest/types": "^25.2.3",
-        "jest-mock": "^25.2.3",
-        "jest-util": "^25.2.3",
+        "@jest/environment": "^25.4.0",
+        "@jest/fake-timers": "^25.4.0",
+        "@jest/types": "^25.4.0",
+        "jest-mock": "^25.4.0",
+        "jest-util": "^25.4.0",
         "jsdom": "^15.2.1"
       }
     },
     "jest-environment-node": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.2.4.tgz",
-      "integrity": "sha512-Jkc5Y8goyXPrLRHnrUlqC7P4o5zn2m4zw6qWoRJ59kxV1f2a5wK+TTGhrhCwnhW/Ckpdl/pm+LufdvhJkvJbiw==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.4.0.tgz",
+      "integrity": "sha512-wryZ18vsxEAKFH7Z74zi/y/SyI1j6UkVZ6QsllBuT/bWlahNfQjLNwFsgh/5u7O957dYFoXj4yfma4n4X6kU9A==",
       "dev": true,
       "requires": {
-        "@jest/environment": "^25.2.4",
-        "@jest/fake-timers": "^25.2.4",
-        "@jest/types": "^25.2.3",
-        "jest-mock": "^25.2.3",
-        "jest-util": "^25.2.3",
+        "@jest/environment": "^25.4.0",
+        "@jest/fake-timers": "^25.4.0",
+        "@jest/types": "^25.4.0",
+        "jest-mock": "^25.4.0",
+        "jest-util": "^25.4.0",
         "semver": "^6.3.0"
       }
     },
     "jest-get-type": {
-      "version": "25.2.1",
-      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.1.tgz",
-      "integrity": "sha512-EYjTiqcDTCRJDcSNKbLTwn/LcDPEE7ITk8yRMNAOjEsN6yp+Uu+V1gx4djwnuj/DvWg0YGmqaBqPVGsPxlvE7w==",
+      "version": "25.2.6",
+      "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz",
+      "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==",
       "dev": true
     },
     "jest-haste-map": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.2.3.tgz",
-      "integrity": "sha512-pAP22OHtPr4qgZlJJFks2LLgoQUr4XtM1a+F5UaPIZNiCRnePA0hM3L7aiJ0gzwiNIYwMTfKRwG/S1L28J3A3A==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.4.0.tgz",
+      "integrity": "sha512-5EoCe1gXfGC7jmXbKzqxESrgRcaO3SzWXGCnvp9BcT0CFMyrB1Q6LIsjl9RmvmJGQgW297TCfrdgiy574Rl9HQ==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "anymatch": "^3.0.3",
         "fb-watchman": "^2.0.0",
         "fsevents": "2.1.2",
         "graceful-fs": "^4.2.3",
-        "jest-serializer": "^25.2.1",
-        "jest-util": "^25.2.3",
-        "jest-worker": "^25.2.1",
+        "jest-serializer": "^25.2.6",
+        "jest-util": "^25.4.0",
+        "jest-worker": "^25.4.0",
         "micromatch": "^4.0.2",
         "sane": "^4.0.3",
         "walker": "^1.0.7",
           "dev": true
         },
         "jest-worker": {
-          "version": "25.2.1",
-          "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.2.1.tgz",
-          "integrity": "sha512-IHnpekk8H/hCUbBlfeaPZzU6v75bqwJp3n4dUrQuQOAgOneI4tx3jV2o8pvlXnDfcRsfkFIUD//HWXpCmR+evQ==",
+          "version": "25.4.0",
+          "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.4.0.tgz",
+          "integrity": "sha512-ghAs/1FtfYpMmYQ0AHqxV62XPvKdUDIBBApMZfly+E9JEmYh2K45G0R5dWxx986RN12pRCxsViwQVtGl+N4whw==",
           "dev": true,
           "requires": {
             "merge-stream": "^2.0.0",
       }
     },
     "jest-jasmine2": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.2.4.tgz",
-      "integrity": "sha512-juoKrmNmLwaheNbAg71SuUF9ovwUZCFNTpKVhvCXWk+SSeORcIUMptKdPCoLXV3D16htzhTSKmNxnxSk4SrTjA==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.4.0.tgz",
+      "integrity": "sha512-QccxnozujVKYNEhMQ1vREiz859fPN/XklOzfQjm2j9IGytAkUbSwjFRBtQbHaNZ88cItMpw02JnHGsIdfdpwxQ==",
       "dev": true,
       "requires": {
         "@babel/traverse": "^7.1.0",
-        "@jest/environment": "^25.2.4",
-        "@jest/source-map": "^25.2.1",
-        "@jest/test-result": "^25.2.4",
-        "@jest/types": "^25.2.3",
+        "@jest/environment": "^25.4.0",
+        "@jest/source-map": "^25.2.6",
+        "@jest/test-result": "^25.4.0",
+        "@jest/types": "^25.4.0",
         "chalk": "^3.0.0",
         "co": "^4.6.0",
-        "expect": "^25.2.4",
+        "expect": "^25.4.0",
         "is-generator-fn": "^2.0.0",
-        "jest-each": "^25.2.3",
-        "jest-matcher-utils": "^25.2.3",
-        "jest-message-util": "^25.2.4",
-        "jest-runtime": "^25.2.4",
-        "jest-snapshot": "^25.2.4",
-        "jest-util": "^25.2.3",
-        "pretty-format": "^25.2.3",
+        "jest-each": "^25.4.0",
+        "jest-matcher-utils": "^25.4.0",
+        "jest-message-util": "^25.4.0",
+        "jest-runtime": "^25.4.0",
+        "jest-snapshot": "^25.4.0",
+        "jest-util": "^25.4.0",
+        "pretty-format": "^25.4.0",
         "throat": "^5.0.0"
       },
       "dependencies": {
       }
     },
     "jest-leak-detector": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.2.3.tgz",
-      "integrity": "sha512-yblCMPE7NJKl7778Cf/73yyFWAas5St0iiEBwq7RDyaz6Xd4WPFnPz2j7yDb/Qce71A1IbDoLADlcwD8zT74Aw==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.4.0.tgz",
+      "integrity": "sha512-7Y6Bqfv2xWsB+7w44dvZuLs5SQ//fzhETgOGG7Gq3TTGFdYvAgXGwV8z159RFZ6fXiCPm/szQ90CyfVos9JIFQ==",
       "dev": true,
       "requires": {
-        "jest-get-type": "^25.2.1",
-        "pretty-format": "^25.2.3"
+        "jest-get-type": "^25.2.6",
+        "pretty-format": "^25.4.0"
       }
     },
     "jest-matcher-utils": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.2.3.tgz",
-      "integrity": "sha512-ZmiXiwQRVM9MoKjGMP5YsGGk2Th5ncyRxfXKz5AKsmU8m43kgNZirckVzaP61MlSa9LKmXbevdYqVp1ZKAw2Rw==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.4.0.tgz",
+      "integrity": "sha512-yPMdtj7YDgXhnGbc66bowk8AkQ0YwClbbwk3Kzhn5GVDrciiCr27U4NJRbrqXbTdtxjImONITg2LiRIw650k5A==",
       "dev": true,
       "requires": {
         "chalk": "^3.0.0",
-        "jest-diff": "^25.2.3",
-        "jest-get-type": "^25.2.1",
-        "pretty-format": "^25.2.3"
+        "jest-diff": "^25.4.0",
+        "jest-get-type": "^25.2.6",
+        "pretty-format": "^25.4.0"
       },
       "dependencies": {
         "ansi-styles": {
       }
     },
     "jest-message-util": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.2.4.tgz",
-      "integrity": "sha512-9wWMH3Bf+GVTv0GcQLmH/FRr0x0toptKw9TA8U5YFLVXx7Tq9pvcNzTyJrcTJ+wLqNbMPPJlJNft4MnlcrtF5Q==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.4.0.tgz",
+      "integrity": "sha512-LYY9hRcVGgMeMwmdfh9tTjeux1OjZHMusq/E5f3tJN+dAoVVkJtq5ZUEPIcB7bpxDUt2zjUsrwg0EGgPQ+OhXQ==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
-        "@jest/test-result": "^25.2.4",
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "@types/stack-utils": "^1.0.1",
         "chalk": "^3.0.0",
         "micromatch": "^4.0.2",
       }
     },
     "jest-mock": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.2.3.tgz",
-      "integrity": "sha512-xlf+pyY0j47zoCs8zGGOGfWyxxLximE8YFOfEK8s4FruR8DtM/UjNj61um+iDuMAFEBDe1bhCXkqiKoCmWjJzg==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.4.0.tgz",
+      "integrity": "sha512-MdazSfcYAUjJjuVTTnusLPzE0pE4VXpOUzWdj8sbM+q6abUjm3bATVPXFqTXrxSieR8ocpvQ9v/QaQCftioQFg==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3"
+        "@jest/types": "^25.4.0"
       }
     },
     "jest-pnp-resolver": {
       }
     },
     "jest-regex-util": {
-      "version": "25.2.1",
-      "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.1.tgz",
-      "integrity": "sha512-wroFVJw62LdqTdkL508ZLV82FrJJWVJMIuYG7q4Uunl1WAPTf4ftPKrqqfec4SvOIlvRZUdEX2TFpWR356YG/w==",
+      "version": "25.2.6",
+      "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz",
+      "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==",
       "dev": true
     },
     "jest-resolve": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.2.3.tgz",
-      "integrity": "sha512-1vZMsvM/DBH258PnpUNSXIgtzpYz+vCVCj9+fcy4akZl4oKbD+9hZSlfe9RIDpU0Fc28ozHQrmwX3EqFRRIHGg==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.4.0.tgz",
+      "integrity": "sha512-wOsKqVDFWUiv8BtLMCC6uAJ/pHZkfFgoBTgPtmYlsprAjkxrr2U++ZnB3l5ykBMd2O24lXvf30SMAjJIW6k2aA==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "browser-resolve": "^1.11.3",
         "chalk": "^3.0.0",
         "jest-pnp-resolver": "^1.2.1",
+        "read-pkg-up": "^7.0.1",
         "realpath-native": "^2.0.0",
-        "resolve": "^1.15.1"
+        "resolve": "^1.15.1",
+        "slash": "^3.0.0"
       },
       "dependencies": {
         "ansi-styles": {
           "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
           "dev": true
         },
+        "slash": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+          "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+          "dev": true
+        },
         "supports-color": {
           "version": "7.1.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
       }
     },
     "jest-resolve-dependencies": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.2.4.tgz",
-      "integrity": "sha512-qhUnK4PfNHzNdca7Ub1mbAqE0j5WNyMTwxBZZJjQlUrdqsiYho/QGK65FuBkZuSoYtKIIqriR9TpGrPEc3P5Gg==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.4.0.tgz",
+      "integrity": "sha512-A0eoZXx6kLiuG1Ui7wITQPl04HwjLErKIJTt8GR3c7UoDAtzW84JtCrgrJ6Tkw6c6MwHEyAaLk7dEPml5pf48A==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3",
-        "jest-regex-util": "^25.2.1",
-        "jest-snapshot": "^25.2.4"
+        "@jest/types": "^25.4.0",
+        "jest-regex-util": "^25.2.6",
+        "jest-snapshot": "^25.4.0"
       }
     },
     "jest-runner": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.2.4.tgz",
-      "integrity": "sha512-5xaIfqqxck9Wg2CV4b9KmJtf/sWO7zWQx7O+34GCLGPzoPcVmB3mZtdrQI1/jS3Reqjru9ycLjgLHSf6XoxRqA==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.4.0.tgz",
+      "integrity": "sha512-wWQSbVgj2e/1chFdMRKZdvlmA6p1IPujhpLT7TKNtCSl1B0PGBGvJjCaiBal/twaU2yfk8VKezHWexM8IliBfA==",
       "dev": true,
       "requires": {
-        "@jest/console": "^25.2.3",
-        "@jest/environment": "^25.2.4",
-        "@jest/test-result": "^25.2.4",
-        "@jest/types": "^25.2.3",
+        "@jest/console": "^25.4.0",
+        "@jest/environment": "^25.4.0",
+        "@jest/test-result": "^25.4.0",
+        "@jest/types": "^25.4.0",
         "chalk": "^3.0.0",
         "exit": "^0.1.2",
         "graceful-fs": "^4.2.3",
-        "jest-config": "^25.2.4",
-        "jest-docblock": "^25.2.3",
-        "jest-haste-map": "^25.2.3",
-        "jest-jasmine2": "^25.2.4",
-        "jest-leak-detector": "^25.2.3",
-        "jest-message-util": "^25.2.4",
-        "jest-resolve": "^25.2.3",
-        "jest-runtime": "^25.2.4",
-        "jest-util": "^25.2.3",
-        "jest-worker": "^25.2.1",
+        "jest-config": "^25.4.0",
+        "jest-docblock": "^25.3.0",
+        "jest-haste-map": "^25.4.0",
+        "jest-jasmine2": "^25.4.0",
+        "jest-leak-detector": "^25.4.0",
+        "jest-message-util": "^25.4.0",
+        "jest-resolve": "^25.4.0",
+        "jest-runtime": "^25.4.0",
+        "jest-util": "^25.4.0",
+        "jest-worker": "^25.4.0",
         "source-map-support": "^0.5.6",
         "throat": "^5.0.0"
       },
           "dev": true
         },
         "jest-worker": {
-          "version": "25.2.1",
-          "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.2.1.tgz",
-          "integrity": "sha512-IHnpekk8H/hCUbBlfeaPZzU6v75bqwJp3n4dUrQuQOAgOneI4tx3jV2o8pvlXnDfcRsfkFIUD//HWXpCmR+evQ==",
+          "version": "25.4.0",
+          "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.4.0.tgz",
+          "integrity": "sha512-ghAs/1FtfYpMmYQ0AHqxV62XPvKdUDIBBApMZfly+E9JEmYh2K45G0R5dWxx986RN12pRCxsViwQVtGl+N4whw==",
           "dev": true,
           "requires": {
             "merge-stream": "^2.0.0",
       }
     },
     "jest-runtime": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.2.4.tgz",
-      "integrity": "sha512-6ehOUizgIghN+aV5YSrDzTZ+zJ9omgEjJbTHj3Jqes5D52XHfhzT7cSfdREwkNjRytrR7mNwZ7pRauoyNLyJ8Q==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.4.0.tgz",
+      "integrity": "sha512-lgNJlCDULtXu9FumnwCyWlOub8iytijwsPNa30BKrSNtgoT6NUMXOPrZvsH06U6v0wgD/Igwz13nKA2wEKU2VA==",
       "dev": true,
       "requires": {
-        "@jest/console": "^25.2.3",
-        "@jest/environment": "^25.2.4",
-        "@jest/source-map": "^25.2.1",
-        "@jest/test-result": "^25.2.4",
-        "@jest/transform": "^25.2.4",
-        "@jest/types": "^25.2.3",
+        "@jest/console": "^25.4.0",
+        "@jest/environment": "^25.4.0",
+        "@jest/source-map": "^25.2.6",
+        "@jest/test-result": "^25.4.0",
+        "@jest/transform": "^25.4.0",
+        "@jest/types": "^25.4.0",
         "@types/yargs": "^15.0.0",
         "chalk": "^3.0.0",
         "collect-v8-coverage": "^1.0.0",
         "exit": "^0.1.2",
         "glob": "^7.1.3",
         "graceful-fs": "^4.2.3",
-        "jest-config": "^25.2.4",
-        "jest-haste-map": "^25.2.3",
-        "jest-message-util": "^25.2.4",
-        "jest-mock": "^25.2.3",
-        "jest-regex-util": "^25.2.1",
-        "jest-resolve": "^25.2.3",
-        "jest-snapshot": "^25.2.4",
-        "jest-util": "^25.2.3",
-        "jest-validate": "^25.2.3",
+        "jest-config": "^25.4.0",
+        "jest-haste-map": "^25.4.0",
+        "jest-message-util": "^25.4.0",
+        "jest-mock": "^25.4.0",
+        "jest-regex-util": "^25.2.6",
+        "jest-resolve": "^25.4.0",
+        "jest-snapshot": "^25.4.0",
+        "jest-util": "^25.4.0",
+        "jest-validate": "^25.4.0",
         "realpath-native": "^2.0.0",
         "slash": "^3.0.0",
         "strip-bom": "^4.0.0",
           }
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
           }
         },
         "yargs-parser": {
-          "version": "18.1.2",
-          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz",
-          "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==",
+          "version": "18.1.3",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+          "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
           "dev": true,
           "requires": {
             "camelcase": "^5.0.0",
       }
     },
     "jest-serializer": {
-      "version": "25.2.1",
-      "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.1.tgz",
-      "integrity": "sha512-fibDi7M5ffx6c/P66IkvR4FKkjG5ldePAK1WlbNoaU4GZmIAkS9Le/frAwRUFEX0KdnisSPWf+b1RC5jU7EYJQ==",
+      "version": "25.2.6",
+      "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.6.tgz",
+      "integrity": "sha512-RMVCfZsezQS2Ww4kB5HJTMaMJ0asmC0BHlnobQC6yEtxiFKIxohFA4QSXSabKwSggaNkqxn6Z2VwdFCjhUWuiQ==",
       "dev": true
     },
     "jest-silent-reporter": {
       }
     },
     "jest-snapshot": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.2.4.tgz",
-      "integrity": "sha512-nIwpW7FZCq5p0AE3Oyqyb6jL0ENJixXzJ5/CD/XRuOqp3gS5OM3O/k+NnTrniCXxPFV4ry6s9HNfiPQBi0wcoA==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.4.0.tgz",
+      "integrity": "sha512-J4CJ0X2SaGheYRZdLz9CRHn9jUknVmlks4UBeu270hPAvdsauFXOhx9SQP2JtRzhnR3cvro/9N9KP83/uvFfRg==",
       "dev": true,
       "requires": {
         "@babel/types": "^7.0.0",
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "@types/prettier": "^1.19.0",
         "chalk": "^3.0.0",
-        "expect": "^25.2.4",
-        "jest-diff": "^25.2.3",
-        "jest-get-type": "^25.2.1",
-        "jest-matcher-utils": "^25.2.3",
-        "jest-message-util": "^25.2.4",
-        "jest-resolve": "^25.2.3",
+        "expect": "^25.4.0",
+        "jest-diff": "^25.4.0",
+        "jest-get-type": "^25.2.6",
+        "jest-matcher-utils": "^25.4.0",
+        "jest-message-util": "^25.4.0",
+        "jest-resolve": "^25.4.0",
         "make-dir": "^3.0.0",
         "natural-compare": "^1.4.0",
-        "pretty-format": "^25.2.3",
+        "pretty-format": "^25.4.0",
         "semver": "^6.3.0"
       },
       "dependencies": {
           "dev": true
         },
         "make-dir": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz",
-          "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==",
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+          "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
           "dev": true,
           "requires": {
             "semver": "^6.0.0"
       }
     },
     "jest-util": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.2.3.tgz",
-      "integrity": "sha512-7tWiMICVSo9lNoObFtqLt9Ezt5exdFlWs5fLe1G4XLY2lEbZc814cw9t4YHScqBkWMfzth8ASHKlYBxiX2rdCw==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.4.0.tgz",
+      "integrity": "sha512-WSZD59sBtAUjLv1hMeKbNZXmMcrLRWcYqpO8Dz8b4CeCTZpfNQw2q9uwrYAD+BbJoLJlu4ezVPwtAmM/9/SlZA==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "chalk": "^3.0.0",
         "is-ci": "^2.0.0",
         "make-dir": "^3.0.0"
           "dev": true
         },
         "make-dir": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz",
-          "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==",
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+          "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
           "dev": true,
           "requires": {
             "semver": "^6.0.0"
       }
     },
     "jest-validate": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.2.3.tgz",
-      "integrity": "sha512-GObn91jzU0B0Bv4cusAwjP6vnWy78hJUM8MOSz7keRfnac/ZhQWIsUjvk01IfeXNTemCwgR57EtdjQMzFZGREg==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.4.0.tgz",
+      "integrity": "sha512-hvjmes/EFVJSoeP1yOl8qR8mAtMR3ToBkZeXrD/ZS9VxRyWDqQ/E1C5ucMTeSmEOGLipvdlyipiGbHJ+R1MQ0g==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "camelcase": "^5.3.1",
         "chalk": "^3.0.0",
-        "jest-get-type": "^25.2.1",
+        "jest-get-type": "^25.2.6",
         "leven": "^3.1.0",
-        "pretty-format": "^25.2.3"
+        "pretty-format": "^25.4.0"
       },
       "dependencies": {
         "ansi-styles": {
       }
     },
     "jest-watcher": {
-      "version": "25.2.4",
-      "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.2.4.tgz",
-      "integrity": "sha512-p7g7s3zqcy69slVzQYcphyzkB2FBmJwMbv6k6KjI5mqd6KnUnQPfQVKuVj2l+34EeuxnbXqnrjtUFmxhcL87rg==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.4.0.tgz",
+      "integrity": "sha512-36IUfOSRELsKLB7k25j/wutx0aVuHFN6wO94gPNjQtQqFPa2rkOymmx9rM5EzbF3XBZZ2oqD9xbRVoYa2w86gw==",
       "dev": true,
       "requires": {
-        "@jest/test-result": "^25.2.4",
-        "@jest/types": "^25.2.3",
+        "@jest/test-result": "^25.4.0",
+        "@jest/types": "^25.4.0",
         "ansi-escapes": "^4.2.1",
         "chalk": "^3.0.0",
-        "jest-util": "^25.2.3",
+        "jest-util": "^25.4.0",
         "string-length": "^3.1.0"
       },
       "dependencies": {
           "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
           "dev": true
         },
-        "escodegen": {
-          "version": "1.14.1",
-          "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz",
-          "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==",
-          "dev": true,
-          "requires": {
-            "esprima": "^4.0.1",
-            "estraverse": "^4.2.0",
-            "esutils": "^2.0.2",
-            "optionator": "^0.8.1",
-            "source-map": "~0.6.1"
-          }
-        },
         "parse5": {
           "version": "5.1.0",
           "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz",
           "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
           "dev": true
         },
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true,
-          "optional": true
-        },
         "tough-cookie": {
           "version": "3.0.1",
           "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
           }
         },
         "ws": {
-          "version": "7.2.3",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz",
-          "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==",
+          "version": "7.2.5",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.5.tgz",
+          "integrity": "sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA==",
           "dev": true
         }
       }
       "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
       "dev": true,
       "requires": {
-        "minimist": "1.2.5"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "1.2.5"
-        }
+        "minimist": "^1.2.0"
       }
     },
     "jsonfile": {
         "verror": "1.10.0"
       }
     },
-    "jszip": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.2.2.tgz",
-      "integrity": "sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA==",
-      "dev": true,
-      "requires": {
-        "lie": "~3.3.0",
-        "pako": "~1.0.2",
-        "readable-stream": "~2.3.6",
-        "set-immediate-shim": "~1.0.1"
-      }
-    },
     "karma-source-map-support": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz",
       "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
       "dev": true
     },
-    "klaw-sync": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
-      "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.1.11"
-      }
-    },
     "kleur": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
       "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
       "dev": true
     },
+    "lazy-ass": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
+      "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=",
+      "dev": true
+    },
     "lcid": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
         "webpack-sources": "^1.2.0"
       }
     },
-    "lie": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
-      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
-      "dev": true,
-      "requires": {
-        "immediate": "~3.0.5"
-      }
-    },
     "liftoff": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz",
         }
       }
     },
+    "lines-and-columns": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
+      "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
+      "dev": true
+    },
+    "listr": {
+      "version": "0.14.3",
+      "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz",
+      "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==",
+      "dev": true,
+      "requires": {
+        "@samverschueren/stream-to-observable": "^0.3.0",
+        "is-observable": "^1.1.0",
+        "is-promise": "^2.1.0",
+        "is-stream": "^1.1.0",
+        "listr-silent-renderer": "^1.1.1",
+        "listr-update-renderer": "^0.5.0",
+        "listr-verbose-renderer": "^0.5.0",
+        "p-map": "^2.0.0",
+        "rxjs": "^6.3.3"
+      }
+    },
+    "listr-silent-renderer": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz",
+      "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=",
+      "dev": true
+    },
+    "listr-update-renderer": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz",
+      "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "cli-truncate": "^0.2.1",
+        "elegant-spinner": "^1.0.1",
+        "figures": "^1.7.0",
+        "indent-string": "^3.0.0",
+        "log-symbols": "^1.0.2",
+        "log-update": "^2.3.0",
+        "strip-ansi": "^3.0.1"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "figures": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+          "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5",
+            "object-assign": "^4.1.0"
+          }
+        },
+        "log-symbols": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
+          "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
+          "dev": true,
+          "requires": {
+            "chalk": "^1.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "listr-verbose-renderer": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz",
+      "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "cli-cursor": "^2.1.0",
+        "date-fns": "^1.27.2",
+        "figures": "^2.0.0"
+      },
+      "dependencies": {
+        "cli-cursor": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^2.0.0"
+          }
+        },
+        "figures": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5"
+          }
+        },
+        "onetime": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^1.0.0"
+          }
+        },
+        "restore-cursor": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+          "dev": true,
+          "requires": {
+            "onetime": "^2.0.0",
+            "signal-exit": "^3.0.2"
+          }
+        }
+      }
+    },
     "live-server": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/live-server/-/live-server-1.2.1.tgz",
               }
             },
             "minimist": {
+              "version": "1.2.5",
+              "bundled": true,
               "dev": true,
-              "optional": true,
-              "version": "1.2.5"
+              "optional": true
             },
             "minipass": {
               "version": "2.9.0",
               "dev": true,
               "optional": true,
               "requires": {
-                "minimist": "1.2.5"
-              },
-              "dependencies": {
-                "minimist": {
-                  "version": "1.2.5"
-                }
+                "minimist": "^1.2.5"
               }
             },
             "ms": {
               "requires": {
                 "deep-extend": "^0.6.0",
                 "ini": "~1.3.0",
-                "minimist": "1.2.5",
+                "minimist": "^1.2.0",
                 "strip-json-comments": "~2.0.1"
-              },
-              "dependencies": {
-                "minimist": {
-                  "version": "1.2.5"
-                }
               }
             },
             "readable-stream": {
       "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
       "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
     },
+    "lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=",
+      "dev": true
+    },
     "lodash.sortby": {
       "version": "4.7.0",
       "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
       "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
       "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
     },
+    "log-symbols": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
+      "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.2"
+      }
+    },
+    "log-update": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz",
+      "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=",
+      "dev": true,
+      "requires": {
+        "ansi-escapes": "^3.0.0",
+        "cli-cursor": "^2.0.0",
+        "wrap-ansi": "^3.0.1"
+      },
+      "dependencies": {
+        "ansi-escapes": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+          "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+          "dev": true
+        },
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "cli-cursor": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^2.0.0"
+          }
+        },
+        "onetime": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^1.0.0"
+          }
+        },
+        "restore-cursor": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+          "dev": true,
+          "requires": {
+            "onetime": "^2.0.0",
+            "signal-exit": "^3.0.2"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        },
+        "wrap-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz",
+          "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=",
+          "dev": true,
+          "requires": {
+            "string-width": "^2.1.1",
+            "strip-ansi": "^4.0.0"
+          }
+        }
+      }
+    },
     "loglevel": {
-      "version": "1.6.7",
-      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz",
-      "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==",
+      "version": "1.6.8",
+      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz",
+      "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==",
       "dev": true
     },
     "loglevel-plugin-prefix": {
       "dev": true
     },
     "mime-db": {
-      "version": "1.43.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
-      "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==",
+      "version": "1.44.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
+      "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==",
       "dev": true
     },
     "mime-types": {
-      "version": "2.1.26",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
-      "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
+      "version": "2.1.27",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
+      "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
       "dev": true,
       "requires": {
-        "mime-db": "1.43.0"
+        "mime-db": "1.44.0"
       }
     },
     "mimic-fn": {
       }
     },
     "minimist": {
-      "dev": true,
-      "version": "1.2.5"
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "dev": true
     },
     "minipass": {
       "version": "2.9.0",
       }
     },
     "mkdirp": {
-      "version": "0.5.4",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz",
-      "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==",
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
       "dev": true,
       "requires": {
-        "minimist": "1.2.5"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "1.2.5"
-        }
+        "minimist": "^1.2.5"
       }
     },
     "mobx": {
             "which": "^1.2.9"
           }
         },
+        "read-pkg": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+          "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=",
+          "dev": true,
+          "requires": {
+            "load-json-file": "^4.0.0",
+            "normalize-package-data": "^2.3.2",
+            "path-type": "^3.0.0"
+          }
+        },
         "semver": {
           "version": "5.7.1",
           "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
       "dev": true
     },
     "object-is": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz",
-      "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==",
-      "dev": true
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz",
+      "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==",
+      "dev": true,
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.5"
+      }
     },
     "object-keys": {
       "version": "1.1.1",
         "is-wsl": "^1.1.0"
       }
     },
-    "optimist": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
-      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
-      "dev": true,
-      "requires": {
-        "minimist": "1.2.5",
-        "wordwrap": "~0.0.2"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "1.2.5"
-        }
-      }
-    },
     "optionator": {
       "version": "0.8.3",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
         "os-tmpdir": "^1.0.0"
       }
     },
+    "ospath": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
+      "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=",
+      "dev": true
+    },
     "p-defer": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
         }
       }
     },
+    "pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
+      "dev": true
+    },
     "performance-now": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
           }
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
           }
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
       "integrity": "sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==",
       "dev": true
     },
+    "pretty-bytes": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz",
+      "integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==",
+      "dev": true
+    },
     "pretty-format": {
-      "version": "25.2.3",
-      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.2.3.tgz",
-      "integrity": "sha512-IP4+5UOAVGoyqC/DiomOeHBUKN6q00gfyT2qpAsRH64tgOKB2yF7FHJXC18OCiU0/YFierACup/zdCOWw0F/0w==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.4.0.tgz",
+      "integrity": "sha512-PI/2dpGjXK5HyXexLPZU/jw5T9Q6S1YVXxxVxco+LIqzUFHXIbKZKdUVt7GcX7QUCr31+3fzhi4gN4/wUYPVxQ==",
       "dev": true,
       "requires": {
-        "@jest/types": "^25.2.3",
+        "@jest/types": "^25.4.0",
         "ansi-regex": "^5.0.0",
         "ansi-styles": "^4.0.0",
         "react-is": "^16.12.0"
           "dev": true
         },
         "cross-spawn": {
-          "version": "7.0.1",
-          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz",
-          "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==",
+          "version": "7.0.2",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz",
+          "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==",
           "dev": true,
           "requires": {
             "path-key": "^3.1.0",
           "dev": true
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
         "genfun": "^5.0.0"
       }
     },
-    "protractor": {
-      "version": "5.4.3",
-      "resolved": "https://registry.npmjs.org/protractor/-/protractor-5.4.3.tgz",
-      "integrity": "sha512-7pMAolv8Ah1yJIqaorDTzACtn3gk7BamVKPTeO5lqIGOrfosjPgXFx/z1dqSI+m5EeZc2GMJHPr5DYlodujDNA==",
-      "dev": true,
-      "requires": {
-        "@types/q": "^0.0.32",
-        "@types/selenium-webdriver": "^3.0.0",
-        "blocking-proxy": "^1.0.0",
-        "browserstack": "^1.5.1",
-        "chalk": "^1.1.3",
-        "glob": "^7.0.3",
-        "jasmine": "2.8.0",
-        "jasminewd2": "^2.1.0",
-        "optimist": "~0.6.0",
-        "q": "1.4.1",
-        "saucelabs": "^1.5.0",
-        "selenium-webdriver": "3.6.0",
-        "source-map-support": "~0.4.0",
-        "webdriver-js-extender": "2.1.0",
-        "webdriver-manager": "^12.0.6"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "2.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
-          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
-          "dev": true
-        },
-        "chalk": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
-          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^2.2.1",
-            "escape-string-regexp": "^1.0.2",
-            "has-ansi": "^2.0.0",
-            "strip-ansi": "^3.0.0",
-            "supports-color": "^2.0.0"
-          }
-        },
-        "del": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
-          "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
-          "dev": true,
-          "requires": {
-            "globby": "^5.0.0",
-            "is-path-cwd": "^1.0.0",
-            "is-path-in-cwd": "^1.0.0",
-            "object-assign": "^4.0.1",
-            "pify": "^2.0.0",
-            "pinkie-promise": "^2.0.0",
-            "rimraf": "^2.2.8"
-          }
-        },
-        "globby": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
-          "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
-          "dev": true,
-          "requires": {
-            "array-union": "^1.0.1",
-            "arrify": "^1.0.0",
-            "glob": "^7.0.3",
-            "object-assign": "^4.0.1",
-            "pify": "^2.0.0",
-            "pinkie-promise": "^2.0.0"
-          }
-        },
-        "is-path-cwd": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
-          "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=",
-          "dev": true
-        },
-        "is-path-in-cwd": {
-          "version": "1.0.1",
-          "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
-          "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
-          "dev": true,
-          "requires": {
-            "is-path-inside": "^1.0.0"
-          }
-        },
-        "is-path-inside": {
-          "version": "1.0.1",
-          "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
-          "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
-          "dev": true,
-          "requires": {
-            "path-is-inside": "^1.0.1"
-          }
-        },
-        "pify": {
-          "version": "2.3.0",
-          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
-          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
-          "dev": true
-        },
-        "semver": {
-          "version": "5.7.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-          "dev": true
-        },
-        "source-map": {
-          "version": "0.5.7",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
-          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
-          "dev": true
-        },
-        "source-map-support": {
-          "version": "0.4.18",
-          "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
-          "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
-          "dev": true,
-          "requires": {
-            "source-map": "^0.5.6"
-          }
-        },
-        "supports-color": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
-          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
-          "dev": true
-        },
-        "webdriver-manager": {
-          "version": "12.1.7",
-          "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.7.tgz",
-          "integrity": "sha512-XINj6b8CYuUYC93SG3xPkxlyUc3IJbD6Vvo75CVGuG9uzsefDzWQrhz0Lq8vbPxtb4d63CZdYophF8k8Or/YiA==",
-          "dev": true,
-          "requires": {
-            "adm-zip": "^0.4.9",
-            "chalk": "^1.1.1",
-            "del": "^2.2.0",
-            "glob": "^7.0.3",
-            "ini": "^1.3.4",
-            "minimist": "1.2.5",
-            "q": "^1.4.1",
-            "request": "^2.87.0",
-            "rimraf": "^2.5.2",
-            "semver": "^5.3.0",
-            "xml2js": "^0.4.17"
-          },
-          "dependencies": {
-            "minimist": {
-              "version": "1.2.5"
-            }
-          }
-        }
-      }
-    },
-    "protractor-fail-fast": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/protractor-fail-fast/-/protractor-fail-fast-3.1.0.tgz",
-      "integrity": "sha512-OjuIFmY7hm5R/Msmioyg3aBevySpmpIgtm2TGUvMEqTzviPk/Fqd1HYmMjIQ+NzFMzrK+93LJa4civDvw1+hEg==",
-      "dev": true,
-      "requires": {
-        "jasmine-fail-fast": "~2.0.0"
-      }
-    },
-    "protractor-screenshoter-plugin": {
-      "version": "0.10.3",
-      "resolved": "https://registry.npmjs.org/protractor-screenshoter-plugin/-/protractor-screenshoter-plugin-0.10.3.tgz",
-      "integrity": "sha512-OF9kGe1rMxBQY4uXzXQUFT14EB83rz8DlDcxmH5HcOHPBpUhGh+Nwo7+K87w1LoLcTuGdG7Bz+/hGwoGguDfsA==",
-      "dev": true,
-      "requires": {
-        "circular-json": "^0.5.1",
-        "fs-extra": "^7.0.0",
-        "klaw-sync": "^6.0.0",
-        "lodash": "^4.17.11",
-        "mkdirp": "^0.5.1",
-        "moment": "^2.20.1",
-        "q": "^1.5.1",
-        "screenshoter-report-analyzer": "^0.6",
-        "uuid": "^3.1.0"
-      },
-      "dependencies": {
-        "fs-extra": {
-          "version": "7.0.1",
-          "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
-          "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
-          "dev": true,
-          "requires": {
-            "graceful-fs": "^4.1.2",
-            "jsonfile": "^4.0.0",
-            "universalify": "^0.1.0"
-          }
-        },
-        "q": {
-          "version": "1.5.1",
-          "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
-          "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
-          "dev": true
-        }
-      }
-    },
     "proxy-addr": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
       "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
       "dev": true
     },
+    "ps-tree": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz",
+      "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==",
+      "dev": true,
+      "requires": {
+        "event-stream": "=3.3.4"
+      }
+    },
     "pseudomap": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
       "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
     },
-    "q": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz",
-      "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=",
-      "dev": true
-    },
     "qs": {
       "version": "6.5.2",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
       "dev": true,
       "requires": {
         "buffer-equal": "0.0.1",
-        "minimist": "1.2.5",
+        "minimist": "^1.1.3",
         "through2": "^2.0.0"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "1.2.5"
-        }
       }
     },
+    "ramda": {
+      "version": "0.26.1",
+      "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz",
+      "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==",
+      "dev": true
+    },
     "randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
       },
       "dependencies": {
         "ajv": {
-          "version": "6.12.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
-          "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
+          "version": "6.12.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
+          "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
           "dev": true,
           "requires": {
             "fast-deep-equal": "^3.1.1",
           "dev": true
         },
         "schema-utils": {
-          "version": "2.6.5",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz",
-          "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==",
+          "version": "2.6.6",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz",
+          "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==",
           "dev": true,
           "requires": {
             "ajv": "^6.12.0",
       }
     },
     "read-pkg": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
-      "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=",
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+      "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
       "dev": true,
       "requires": {
-        "load-json-file": "^4.0.0",
-        "normalize-package-data": "^2.3.2",
-        "path-type": "^3.0.0"
+        "@types/normalize-package-data": "^2.4.0",
+        "normalize-package-data": "^2.5.0",
+        "parse-json": "^5.0.0",
+        "type-fest": "^0.6.0"
+      },
+      "dependencies": {
+        "parse-json": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
+          "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
+          "dev": true,
+          "requires": {
+            "@babel/code-frame": "^7.0.0",
+            "error-ex": "^1.3.1",
+            "json-parse-better-errors": "^1.0.1",
+            "lines-and-columns": "^1.1.6"
+          }
+        },
+        "type-fest": {
+          "version": "0.6.0",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+          "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg-up": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+      "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+      "dev": true,
+      "requires": {
+        "find-up": "^4.1.0",
+        "read-pkg": "^5.2.0",
+        "type-fest": "^0.8.1"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^5.0.0",
+            "path-exists": "^4.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+          "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^4.1.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+          "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.2.0"
+          }
+        },
+        "p-try": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+          "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+          "dev": true
+        },
+        "path-exists": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+          "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+          "dev": true
+        },
+        "type-fest": {
+          "version": "0.8.1",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+          "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+          "dev": true
+        }
       }
     },
     "readable-stream": {
       }
     },
     "readdirp": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
-      "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
+      "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
       "dev": true,
       "requires": {
-        "picomatch": "^2.0.7"
+        "picomatch": "^2.2.1"
       }
     },
     "realpath-native": {
           }
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
           }
         },
         "yargs-parser": {
-          "version": "18.1.2",
-          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz",
-          "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==",
+          "version": "18.1.3",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+          "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
           "dev": true,
           "requires": {
             "camelcase": "^5.0.0",
         "uuid": "^3.3.2"
       }
     },
+    "request-progress": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
+      "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=",
+      "dev": true,
+      "requires": {
+        "throttleit": "^1.0.0"
+      }
+    },
     "request-promise-core": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz",
       "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
     },
     "resolve": {
-      "version": "1.15.1",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz",
-      "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==",
+      "version": "1.17.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
+      "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
       "dev": true,
       "requires": {
         "path-parse": "^1.0.6"
       "dev": true
     },
     "run-async": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz",
-      "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==",
-      "dev": true,
-      "requires": {
-        "is-promise": "^2.1.0"
-      }
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
+      "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
+      "dev": true
     },
     "run-queue": {
       "version": "1.0.3",
         "execa": "^1.0.0",
         "fb-watchman": "^2.0.0",
         "micromatch": "^3.1.4",
-        "minimist": "1.2.5",
+        "minimist": "^1.1.1",
         "walker": "~1.0.5"
       },
       "dependencies": {
             "pump": "^3.0.0"
           }
         },
-        "minimist": {
-          "version": "1.2.5"
-        },
         "normalize-path": {
           "version": "2.1.1",
           "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
         }
       }
     },
-    "saucelabs": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz",
-      "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==",
-      "dev": true,
-      "requires": {
-        "https-proxy-agent": "^2.2.1"
-      }
-    },
     "sax": {
       "version": "0.5.8",
       "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz",
         "get-assigned-identifiers": "^1.1.0"
       }
     },
-    "screenshoter-report-analyzer": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/screenshoter-report-analyzer/-/screenshoter-report-analyzer-0.6.0.tgz",
-      "integrity": "sha1-Cm+I1fXRrBa2z3Ji7/ujH+5I7RI=",
-      "dev": true
-    },
     "select-hose": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
       "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
       "dev": true
     },
-    "selenium-webdriver": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz",
-      "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==",
-      "dev": true,
-      "requires": {
-        "jszip": "^3.1.3",
-        "rimraf": "^2.5.4",
-        "tmp": "0.0.30",
-        "xml2js": "^0.4.17"
-      },
-      "dependencies": {
-        "tmp": {
-          "version": "0.0.30",
-          "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz",
-          "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=",
-          "dev": true,
-          "requires": {
-            "os-tmpdir": "~1.0.1"
-          }
-        }
-      }
-    },
     "selfsigned": {
       "version": "1.10.7",
       "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
       "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
     },
-    "set-immediate-shim": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
-      "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
-      "dev": true
-    },
     "set-value": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
       "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
       "dev": true
     },
+    "slice-ansi": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
+      "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
+      "dev": true
+    },
     "smart-buffer": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz",
       }
     },
     "spdx-exceptions": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
-      "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
+      "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
       "dev": true
     },
     "spdx-expression-parse": {
       "dev": true
     },
     "spdy": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz",
-      "integrity": "sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
+      "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
       "dev": true,
       "requires": {
         "debug": "^4.1.0",
       "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==",
       "dev": true
     },
-    "static-eval": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.5.tgz",
-      "integrity": "sha512-nNbV6LbGtMBgv7e9LFkt5JV8RVlRsyJrphfAt9tOtBBW/SfnzZDf2KnS72an8e434A+9e/BmJuTxeGPvrAK7KA==",
+    "start-server-and-test": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.11.0.tgz",
+      "integrity": "sha512-FhkJFYL/lvbd0tKWvbxWNWjtFtq3Zpa09QDjA8EUH88AsgNL4hkAAKYNmbac+fFM8/GIZoJ1Mj4mm3SMI0X1bA==",
       "dev": true,
       "requires": {
-        "escodegen": "^1.11.1"
+        "bluebird": "3.7.2",
+        "check-more-types": "2.24.0",
+        "debug": "4.1.1",
+        "execa": "3.4.0",
+        "lazy-ass": "1.6.0",
+        "ps-tree": "1.2.0",
+        "wait-on": "4.0.0"
       },
       "dependencies": {
-        "escodegen": {
-          "version": "1.14.1",
-          "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz",
-          "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==",
+        "cross-spawn": {
+          "version": "7.0.2",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz",
+          "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==",
           "dev": true,
           "requires": {
-            "esprima": "^4.0.1",
-            "estraverse": "^4.2.0",
-            "esutils": "^2.0.2",
-            "optionator": "^0.8.1",
-            "source-map": "~0.6.1"
+            "path-key": "^3.1.0",
+            "shebang-command": "^2.0.0",
+            "which": "^2.0.1"
           }
         },
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+        "execa": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz",
+          "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==",
           "dev": true,
-          "optional": true
+          "requires": {
+            "cross-spawn": "^7.0.0",
+            "get-stream": "^5.0.0",
+            "human-signals": "^1.1.1",
+            "is-stream": "^2.0.0",
+            "merge-stream": "^2.0.0",
+            "npm-run-path": "^4.0.0",
+            "onetime": "^5.1.0",
+            "p-finally": "^2.0.0",
+            "signal-exit": "^3.0.2",
+            "strip-final-newline": "^2.0.0"
+          }
+        },
+        "get-stream": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
+          "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
+          "dev": true,
+          "requires": {
+            "pump": "^3.0.0"
+          }
+        },
+        "is-stream": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
+          "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
+          "dev": true
+        },
+        "npm-run-path": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+          "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+          "dev": true,
+          "requires": {
+            "path-key": "^3.0.0"
+          }
+        },
+        "p-finally": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
+          "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==",
+          "dev": true
+        },
+        "path-key": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+          "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+          "dev": true
+        },
+        "shebang-command": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+          "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+          "dev": true,
+          "requires": {
+            "shebang-regex": "^3.0.0"
+          }
+        },
+        "shebang-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+          "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+          "dev": true
+        },
+        "which": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+          "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+          "dev": true,
+          "requires": {
+            "isexe": "^2.0.0"
+          }
         }
       }
     },
+    "static-eval": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.5.tgz",
+      "integrity": "sha512-nNbV6LbGtMBgv7e9LFkt5JV8RVlRsyJrphfAt9tOtBBW/SfnzZDf2KnS72an8e434A+9e/BmJuTxeGPvrAK7KA==",
+      "dev": true,
+      "requires": {
+        "escodegen": "^1.11.1"
+      }
+    },
     "static-extend": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
       }
     },
     "static-module": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/static-module/-/static-module-3.0.3.tgz",
-      "integrity": "sha512-RDaMYaI5o/ym0GkCqL/PlD1Pn216omp8fY81okxZ6f6JQxWW5tptOw9reXoZX85yt/scYvbWIt6uoszeyf+/MQ==",
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/static-module/-/static-module-3.0.4.tgz",
+      "integrity": "sha512-gb0v0rrgpBkifXCa3yZXxqVmXDVE+ETXj6YlC/jt5VzOnGXR2C15+++eXuMDUYsePnbhf+lwW0pE1UXyOLtGCw==",
       "dev": true,
       "requires": {
         "acorn-node": "^1.3.0",
         "concat-stream": "~1.6.0",
         "convert-source-map": "^1.5.1",
         "duplexer2": "~0.1.4",
-        "escodegen": "~1.9.0",
+        "escodegen": "^1.11.1",
         "has": "^1.0.1",
-        "magic-string": "^0.22.4",
+        "magic-string": "0.25.1",
         "merge-source-map": "1.0.4",
-        "object-inspect": "~1.4.0",
+        "object-inspect": "^1.6.0",
         "readable-stream": "~2.3.3",
         "scope-analyzer": "^2.0.1",
         "shallow-copy": "~0.0.1",
-        "static-eval": "^2.0.2",
+        "static-eval": "^2.0.5",
         "through2": "~2.0.3"
       },
       "dependencies": {
         "magic-string": {
-          "version": "0.22.5",
-          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
-          "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==",
+          "version": "0.25.1",
+          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.1.tgz",
+          "integrity": "sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg==",
           "dev": true,
           "requires": {
-            "vlq": "^0.2.2"
+            "sourcemap-codec": "^1.4.1"
           }
         },
         "merge-source-map": {
             "source-map": "^0.5.6"
           }
         },
-        "object-inspect": {
-          "version": "1.4.1",
-          "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz",
-          "integrity": "sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==",
-          "dev": true
-        },
         "source-map": {
           "version": "0.5.7",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
       }
     },
     "string.prototype.trimend": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.0.tgz",
-      "integrity": "sha512-EEJnGqa/xNfIg05SxiPSqRS7S9qwDhYts1TSLR1BQfYUfPe1stofgGKvwERK9+9yf+PpfBMlpBaCHucXGPQfUA==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
+      "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
       "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
       }
     },
     "string.prototype.trimstart": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.0.tgz",
-      "integrity": "sha512-iCP8g01NFYiiBOnwG1Xc3WZLyoo+RuBymwIlWncShXDDJYWN6DbnM3odslBJdgCdRlq94B5s63NWAZlcn2CS4w==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
+      "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
       "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
       },
       "dependencies": {
         "ajv": {
-          "version": "6.12.0",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
-          "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
+          "version": "6.12.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
+          "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
           "dev": true,
           "requires": {
             "fast-deep-equal": "^3.1.1",
           "dev": true
         },
         "schema-utils": {
-          "version": "2.6.5",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz",
-          "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==",
+          "version": "2.6.6",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz",
+          "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==",
           "dev": true,
           "requires": {
             "ajv": "^6.12.0",
       "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==",
       "dev": true
     },
+    "throttleit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+      "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=",
+      "dev": true
+    },
     "through": {
       "version": "2.3.8",
       "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
       "dev": true,
       "requires": {
         "colors": "^1.0.3",
-        "minimist": "1.2.5",
+        "minimist": "^1.2.0",
         "prompts": "^2.0.4",
         "request": "^2.88.0",
         "request-promise-native": "^1.0.7",
         "xliff": "^4.2.0"
-      },
-      "dependencies": {
-        "minimist": {
-          "version": "1.2.5"
-        }
       }
     },
     "traverse": {
       "dev": true
     },
     "ts-jest": {
-      "version": "25.3.0",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.3.0.tgz",
-      "integrity": "sha512-qH/uhaC+AFDU9JfAueSr0epIFJkGMvUPog4FxSEVAtPOur1Oni5WBJMiQIkfHvc7PviVRsnlVLLY2I6221CQew==",
+      "version": "25.4.0",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.4.0.tgz",
+      "integrity": "sha512-+0ZrksdaquxGUBwSdTIcdX7VXdwLIlSRsyjivVA9gcO+Cvr6ByqDhu/mi5+HCcb6cMkiQp5xZ8qRO7/eCqLeyw==",
       "dev": true,
       "requires": {
         "bs-logger": "0.x",
         "json5": "2.x",
         "lodash.memoize": "4.x",
         "make-error": "1.x",
+        "micromatch": "4.x",
         "mkdirp": "1.x",
         "resolve": "1.x",
         "semver": "6.x",
-        "yargs-parser": "^18.1.1"
+        "yargs-parser": "18.x"
       },
       "dependencies": {
         "camelcase": {
           "dev": true
         },
         "json5": {
-          "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.2.tgz",
-          "integrity": "sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==",
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
+          "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
           "dev": true,
           "requires": {
-            "minimist": "1.2.5"
-          },
-          "dependencies": {
-            "minimist": {
-              "version": "1.2.5"
-            }
+            "minimist": "^1.2.5"
+          }
+        },
+        "micromatch": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+          "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+          "dev": true,
+          "requires": {
+            "braces": "^3.0.1",
+            "picomatch": "^2.0.5"
           }
         },
         "mkdirp": {
-          "version": "1.0.3",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz",
-          "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==",
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+          "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
           "dev": true
         },
         "yargs-parser": {
-          "version": "18.1.2",
-          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz",
-          "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==",
+          "version": "18.1.3",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+          "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
           "dev": true,
           "requires": {
             "camelcase": "^5.0.0",
       "dev": true
     },
     "uglify-js": {
-      "version": "3.8.1",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.1.tgz",
-      "integrity": "sha512-W7KxyzeaQmZvUFbGj4+YFshhVrMBGSg2IbcYAjGWGvx8DHvJMclbTDMpffdxFUGPBHjIytk7KJUR/KUXstUGDw==",
+      "version": "3.9.1",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.1.tgz",
+      "integrity": "sha512-JUPoL1jHsc9fOjVFHdQIhqEEJsQvfKDjlubcCilu8U26uZ73qOg8VsN8O1jbuei44ZPlwL7kmbAdM4tzaUvqnA==",
       "dev": true,
       "optional": true,
       "requires": {
-        "commander": "~2.20.3",
-        "source-map": "~0.6.1"
-      },
-      "dependencies": {
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true,
-          "optional": true
-        }
+        "commander": "~2.20.3"
       }
     },
     "unc-path-regex": {
         }
       }
     },
+    "untildify": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+      "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+      "dev": true
+    },
     "upath": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
       "integrity": "sha512-W+1+N/hdzLpQZEcvz79n2IgUE9pfx6JLdHh3Kh8RGvLL8P1LdJVQmi2OsDcLdY4QVID4OUy+FPelyerX0nJxIQ==",
       "dev": true
     },
-    "vlq": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz",
-      "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==",
-      "dev": true
-    },
     "vm-browserify": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
         "xml-name-validator": "^3.0.0"
       }
     },
+    "wait-on": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-4.0.0.tgz",
+      "integrity": "sha512-QrW3J8LzS5ADPfD9Rx5S6KJck66xkqyiFKQs9jmUTkIhiEOmkzU7WRZc+MjsnmkrgjitS2xQ4bb13hnlQnKBUQ==",
+      "dev": true,
+      "requires": {
+        "@hapi/joi": "^16.1.8",
+        "lodash": "^4.17.15",
+        "minimist": "^1.2.0",
+        "request": "^2.88.0",
+        "request-promise-native": "^1.0.8",
+        "rxjs": "^6.5.4"
+      }
+    },
     "walker": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",
               }
             },
             "minimist": {
+              "version": "1.2.5",
+              "bundled": true,
               "dev": true,
-              "optional": true,
-              "version": "1.2.5"
+              "optional": true
             },
             "minipass": {
               "version": "2.9.0",
               "dev": true,
               "optional": true,
               "requires": {
-                "minimist": "1.2.5"
-              },
-              "dependencies": {
-                "minimist": {
-                  "version": "1.2.5"
-                }
+                "minimist": "^1.2.5"
               }
             },
             "ms": {
               "requires": {
                 "deep-extend": "^0.6.0",
                 "ini": "~1.3.0",
-                "minimist": "1.2.5",
+                "minimist": "^1.2.0",
                 "strip-json-comments": "~2.0.1"
-              },
-              "dependencies": {
-                "minimist": {
-                  "version": "1.2.5"
-                }
               }
             },
             "readable-stream": {
         "minimalistic-assert": "^1.0.0"
       }
     },
-    "webdriver-js-extender": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz",
-      "integrity": "sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ==",
-      "dev": true,
-      "requires": {
-        "@types/selenium-webdriver": "^3.0.0",
-        "selenium-webdriver": "^3.0.1"
-      }
-    },
     "webidl-conversions": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
               }
             },
             "minimist": {
+              "version": "1.2.5",
+              "bundled": true,
               "dev": true,
-              "optional": true,
-              "version": "1.2.5"
+              "optional": true
             },
             "minipass": {
               "version": "2.9.0",
               "dev": true,
               "optional": true,
               "requires": {
-                "minimist": "1.2.5"
-              },
-              "dependencies": {
-                "minimist": {
-                  "version": "1.2.5"
-                }
+                "minimist": "^1.2.5"
               }
             },
             "ms": {
               "requires": {
                 "deep-extend": "^0.6.0",
                 "ini": "~1.3.0",
-                "minimist": "1.2.5",
+                "minimist": "^1.2.0",
                 "strip-json-comments": "~2.0.1"
-              },
-              "dependencies": {
-                "minimist": {
-                  "version": "1.2.5"
-                }
               }
             },
             "readable-stream": {
           }
         },
         "p-limit": {
-          "version": "2.2.2",
-          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
-          "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
           "dev": true,
           "requires": {
             "p-try": "^2.0.0"
       "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
     },
     "windows-release": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz",
-      "integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==",
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.0.tgz",
+      "integrity": "sha512-2HetyTg1Y+R+rUgrKeUEhAG/ZuOmTrI1NBb3ZyAGQMYmOJjBBPe4MTodghRkmLJZHwkuPi02anbeGP+Zf401LQ==",
       "dev": true,
       "requires": {
         "execa": "^1.0.0"
       "dev": true
     },
     "wordwrap": {
-      "version": "0.0.3",
-      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
-      "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
       "dev": true
     },
     "worker-farm": {
       "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
       "dev": true
     },
-    "xml2js": {
-      "version": "0.4.23",
-      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
-      "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
-      "dev": true,
-      "requires": {
-        "sax": ">=0.6.0",
-        "xmlbuilder": "~11.0.0"
-      },
-      "dependencies": {
-        "sax": {
-          "version": "1.2.4",
-          "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
-          "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
-          "dev": true
-        }
-      }
-    },
-    "xmlbuilder": {
-      "version": "11.0.1",
-      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
-      "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
-      "dev": true
-    },
     "xmlchars": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
         "camelcase": "^4.1.0"
       }
     },
+    "yauzl": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+      "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
+      "dev": true,
+      "requires": {
+        "buffer-crc32": "~0.2.3",
+        "fd-slicer": "~1.1.0"
+      }
+    },
     "yn": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
index 282d98e85606cc1e67efa21b41c8f5da68f8225a..8aeece9c5d6dad9ba26706a962239d89efc0b088 100644 (file)
     "test": "npm run test:config && jest --watch",
     "test:ci": "npm run test:config && JEST_SILENT_REPORTER_DOTS=true jest --coverage --reporters jest-silent-reporter",
     "test:config": "if [ ! -e 'src/unit-test-configuration.ts' ]; then cp 'src/unit-test-configuration.ts.sample' 'src/unit-test-configuration.ts'; fi",
-    "e2e": "npm run env_build && npm run e2e:update && ng e2e --webdriverUpdate=false",
-    "e2e:ci": "npm run env_build && npm run e2e:update && ng e2e --dev-server-target --webdriverUpdate=false",
-    "e2e:update": "npx webdriver-manager update --gecko=false --versions.chrome=$(google-chrome --version | awk '{ print $3 }')",
+    "e2e": "start-test 4200 'cypress open'",
+    "e2e:ci": "start-test 4200 'cypress run -b chrome --headless'",
     "lint:tslint": "ng lint",
-    "lint:prettier": "prettier --list-different \"{src,e2e}/**/*.{ts,scss}\"",
+    "lint:prettier": "prettier --list-different \"{src,cypress}/**/*.{ts,scss}\"",
     "lint:html": "htmllint src/app/**/*.html && html-linter --config html-linter.config.json",
-    "lint:tsc": "npm run test:config && tsc -p src/tsconfig.app.json --noEmit && tsc -p tsconfig.spec.json --noEmit && tsc -p e2e/tsconfig.e2e.json --noEmit",
+    "lint:tsc": "npm run test:config && tsc -p src/tsconfig.app.json --noEmit && tsc -p tsconfig.spec.json --noEmit && tsc -p cypress/tsconfig.json --noEmit",
     "lint": "npm run lint:tsc && npm run lint:tslint && npm run lint:prettier && npm run lint:html",
-    "fix:prettier": "prettier --write \"{src,e2e}/**/*.{ts,scss}\"",
+    "fix:prettier": "prettier --write \"{src,cypress}/**/*.{ts,scss}\"",
     "fix:tslint": "npm run lint:tslint -- --fix",
-    "fixmod": "pretty-quick --pattern \"{src,e2e}/**/*.{ts,scss}\" --branch HEAD",
+    "fixmod": "pretty-quick --pattern \"{src,cypress}/**/*.{ts,scss}\" --branch HEAD",
     "fix": "npm run fix:tslint; npm run fix:prettier",
     "fix:audit": "npx npm-force-resolutions",
     "compodoc": "compodoc",
     "@angular/compiler-cli": "8.2.14",
     "@angular/language-service": "8.2.14",
     "@compodoc/compodoc": "1.1.11",
-    "@types/jasmine": "3.5.10",
-    "@types/jasminewd2": "2.0.8",
     "@types/jest": "25.1.4",
     "@types/lodash": "4.14.149",
     "@types/node": "12.12.34",
     "@types/simplebar": "5.1.1",
     "codelyzer": "5.2.2",
+    "cypress": "4.4.0",
     "html-linter": "1.1.1",
     "htmllint-cli": "0.0.7",
-    "jasmine-core": "3.5.0",
-    "jasmine-spec-reporter": "5.0.1",
     "jest": "25.2.4",
     "jest-canvas-mock": "2.2.0",
     "jest-preset-angular": "8.1.3",
     "npm-run-all": "4.1.5",
     "prettier": "2.0.2",
     "pretty-quick": "2.0.1",
-    "protractor": "5.4.3",
-    "protractor-fail-fast": "3.1.0",
-    "protractor-screenshoter-plugin": "0.10.3",
     "replace-in-file": "5.0.2",
+    "start-server-and-test": "1.11.0",
     "transifex-i18ntool": "1.1.0",
     "ts-node": "8.8.1",
     "tslint": "6.1.0",
   },
   "resolutions": {
     "mem": "4.3.0",
-    "minimist": "1.2.5",
     "fsevents": "2.1.2"
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/protractor.conf.js b/src/pybind/mgr/dashboard/frontend/protractor.conf.js
deleted file mode 100644 (file)
index c7edd91..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-// Protractor configuration file, see link for more information
-// https://github.com/angular/protractor/blob/master/lib/config.ts
-
-const { SpecReporter } = require('jasmine-spec-reporter');
-let failFast = require('protractor-fail-fast');
-
-const config = {
-  SELENIUM_PROMISE_MANAGER: false,
-  allScriptsTimeout: 11000,
-  implicitWaitTimeout: 9000,
-  suites: {
-    block: './e2e/block/*.e2e-spec.ts',
-    cluster: './e2e/cluster/*.e2e-spec.ts',
-    filesystems: './e2e/filesystems/*.e2e-spec.ts',
-    nfs: './e2e/nfs/*.e2e-spec.ts',
-    pools: './e2e/pools/*.e2e-spec.ts',
-    rgw: './e2e/rgw/*.e2e-spec.ts',
-    ui: './e2e/ui/*.e2e-spec.ts'
-  },
-  capabilities: {
-    browserName: 'chrome',
-    chromeOptions: {
-      args: ['--no-sandbox', '--headless', '--window-size=1920x1080']
-    },
-    acceptInsecureCerts: true
-  },
-  directConnect: true,
-  baseUrl: process.env.BASE_URL || 'http://localhost:4200/',
-  framework: 'jasmine',
-  jasmineNodeOpts: {
-    showColors: true,
-    defaultTimeoutInterval: 300000,
-    print: function () {}
-  },
-  params: {
-    login: {
-      user: process.env.E2E_LOGIN_USER || 'admin',
-      password: process.env.E2E_LOGIN_PWD || 'admin'
-    }
-  },
-
-  plugins: [
-    {
-      package: 'protractor-screenshoter-plugin',
-      screenshotPath: '.protractor-report',
-      screenshotOnExpect: 'failure',
-      screenshotOnSpec: 'none',
-      withLogs: true,
-      writeReportFreq: 'asap',
-      imageToAscii: 'none',
-      clearFoldersBeforeTest: true
-    },
-    failFast.init()
-  ],
-  afterLaunch: function () {
-    failFast.clean();
-  }
-};
-
-config.onPrepare = async () => {
-  await browser.manage().timeouts().implicitlyWait(config.implicitWaitTimeout);
-
-  require('ts-node').register({
-    project: 'e2e/tsconfig.e2e.json'
-  });
-  jasmine
-    .getEnv()
-    .addReporter(
-      new SpecReporter({ spec: { displayStacktrace: 'pretty', displayDuration: true } })
-    );
-
-  await browser.get('/#/login');
-
-  await browser.driver.findElement(by.name('username')).clear();
-  await browser.driver.findElement(by.name('username')).sendKeys(browser.params.login.user);
-  await browser.driver.findElement(by.name('password')).clear();
-  await browser.driver.findElement(by.name('password')).sendKeys(browser.params.login.password);
-
-  await browser.driver.findElement(by.css('input[type="submit"]')).click();
-};
-
-exports.config = config;
index c9a95592224abb002f06632e18aba59d9fb0f1cc..441351b729a4d9b243f2e1f39f78e799bfaa0b18 100644 (file)
@@ -40,8 +40,7 @@ export class MonitorComponent {
             return lastValueA > lastValueB ? 1 : -1;
           }
         }
-      ],
-      data: []
+      ]
     };
 
     this.notInQuorum = {
@@ -49,8 +48,7 @@ export class MonitorComponent {
         { prop: 'name', name: this.i18n('Name'), cellTransformation: CellTemplate.routerLink },
         { prop: 'rank', name: this.i18n('Rank') },
         { prop: 'public_addr', name: this.i18n('Public Address') }
-      ],
-      data: []
+      ]
     };
   }
 
index c0cd8bb5f301d07534328951361cf2cb4a482e0b..61fa3585183f4f363213b2383c988b94d10b784b 100644 (file)
@@ -48,7 +48,7 @@ export class PoolListComponent extends ListWithDetails implements OnInit {
   @ViewChild('poolConfigurationSourceTpl', { static: false })
   poolConfigurationSourceTpl: TemplateRef<any>;
 
-  pools: Pool[] = [];
+  pools: Pool[];
   columns: CdTableColumn[];
   selection = new CdTableSelection();
   modalRef: BsModalRef;
index b72faa4b0f21383221471f6a09ec75d853124db9..ccdf3678dcc004fa62394c22fec2e984d7dc4276 100644 (file)
@@ -28,9 +28,9 @@
     "allowJs": true
   },
   "exclude": [
-    ".protractor-report",
     "coverage",
     "dist",
-    "node_modules"
+    "node_modules",
+    "cypress"
   ]
 }
index 043e4e00a3cff095772a637094f949fa28e8f3aa..56022a53e2785541d29987f9314d5e9bed1429e2 100755 (executable)
@@ -2,6 +2,25 @@
 
 set -e
 
+start_ceph() {
+    cd $FULL_PATH_BUILD_DIR
+
+    MGR=2 RGW=1 ../src/vstart.sh -n -d
+    sleep 10
+
+    # Create an Object Gateway User
+    ./bin/radosgw-admin user create --uid=dev --display-name=Developer --system
+    # Set the user-id
+    ./bin/ceph dashboard set-rgw-api-user-id dev
+    # Obtain and set access and secret key for the previously created user. $() is safer than backticks `..`
+    ./bin/ceph dashboard set-rgw-api-access-key $(./bin/radosgw-admin user info --uid=dev | jq -r .keys[0].access_key)
+    ./bin/ceph dashboard set-rgw-api-secret-key $(./bin/radosgw-admin user info --uid=dev | jq -r .keys[0].secret_key)
+    # Set SSL verify to False
+    ./bin/ceph dashboard set-rgw-api-ssl-verify False
+
+    CYPRESS_BASE_URL=$(./bin/ceph mgr services | jq -r .dashboard)
+}
+
 stop() {
     if [ "$REMOTE" == "false" ]; then
         cd ${FULL_PATH_BUILD_DIR}
@@ -10,78 +29,97 @@ stop() {
     exit $1
 }
 
-BASE_URL=''
+check_device_available() {
+    : ${DEVICE:="chrome"}
+    failed=false
+
+    if [ "$DEVICE" == "docker" ]; then
+        [ -x "$(command -v docker)" ] || failed=true
+    else
+        cd $DASH_DIR/frontend
+        npx cypress verify
+
+        case "$DEVICE" in
+            chrome)
+                [ -x "$(command -v google-chrome)" ] || [ -x "$(command -v google-chrome-stable)" ] ] || failed=true
+                ;;
+            chromium)
+                [ -x "$(command -v chromium)" ] || failed=true
+                ;;
+        esac
+    fi
+
+    if [ "$failed" = "true" ]; then
+            echo "ERROR: $DEVICE not found. You need to install $DEVICE or \
+    use a different device. Supported devices: chrome (default), chromium, electron or docker."
+        stop 1
+    fi
+}
+
+CYPRESS_BASE_URL=''
 DEVICE=''
-E2E_LOGIN_USER=''
-E2E_LOGIN_PWD=''
+CYPRESS_LOGIN_PWD=''
+CYPRESS_LOGIN_USER=''
+NO_COLOR=1
+RECORD=''
 REMOTE='false'
 
 while getopts 'd:p:r:u:' flag; do
   case "${flag}" in
     d) DEVICE=$OPTARG;;
-    p) E2E_LOGIN_PWD=$OPTARG;;
+    p) CYPRESS_LOGIN_PWD=$OPTARG;;
     r) REMOTE='true'
-       BASE_URL=$OPTARG;;
-    u) E2E_LOGIN_USER=$OPTARG;;
+       CYPRESS_BASE_URL=$OPTARG;;
+    u) CYPRESS_LOGIN_USER=$OPTARG;;
   esac
 done
 
-if [ "$DEVICE" == "" ]; then
-    if [ -x "$(command -v google-chrome)" ] || [ -x "$(command -v google-chrome-stable)" ]; then
-        DEVICE="chrome"
-    elif [ -x "$(command -v docker)" ]; then
-        DEVICE="docker"
-    else
-        echo "ERROR: Chrome and Docker not found. You need to install one of  \
-them to run the e2e frontend tests."
-        stop 1
-    fi
-fi
-
 DASH_DIR=`pwd`
-
 [ -z "$BUILD_DIR" ] && BUILD_DIR=build
-
 cd ../../../../${BUILD_DIR}
 FULL_PATH_BUILD_DIR=`pwd`
 
-if [ "$BASE_URL" == "" ]; then
-    MGR=2 RGW=1 ../src/vstart.sh -n -d
-    sleep 10
+[[ "$(command -v npm)" == '' ]] && . ${FULL_PATH_BUILD_DIR}/src/pybind/mgr/dashboard/node-env/bin/activate
 
-    # Create an Object Gateway User
-    ./bin/radosgw-admin user create --uid=dev --display-name=Developer --system
-    # Set the user-id
-    ./bin/ceph dashboard set-rgw-api-user-id dev
-    # Obtain and set access and secret key for the previously created user. $() is safer than backticks `..`
-    ./bin/ceph dashboard set-rgw-api-access-key $(./bin/radosgw-admin user info --uid=dev | jq -r .keys[0].access_key)
-    ./bin/ceph dashboard set-rgw-api-secret-key $(./bin/radosgw-admin user info --uid=dev | jq -r .keys[0].secret_key)
-    # Set SSL verify to False
-    ./bin/ceph dashboard set-rgw-api-ssl-verify False
+: ${CYPRESS_CACHE_FOLDER:="${FULL_PATH_BUILD_DIR}/src/pybind/mgr/dashboard/cypress"}
 
-    BASE_URL=$(./bin/ceph mgr services | jq -r .dashboard)
-fi
+export CYPRESS_BASE_URL CYPRESS_CACHE_FOLDER CYPRESS_LOGIN_USER CYPRESS_LOGIN_PWD NO_COLOR
 
-export BASE_URL E2E_LOGIN_USER E2E_LOGIN_PWD
+check_device_available
 
-cd $DASH_DIR/frontend
+if [ "$CYPRESS_BASE_URL" == "" ]; then
+    start_ceph
+fi
 
-[[ "$(command -v npm)" == '' ]] && . ${FULL_PATH_BUILD_DIR}/src/pybind/mgr/dashboard/node-env/bin/activate
+cd $DASH_DIR/frontend
 
-if [ "$DEVICE" == "chrome" ]; then
-    npm run e2e:ci || stop 1
-    stop 0
-elif [ "$DEVICE" == "docker" ]; then
-    failed=0
-    cat <<EOF > .env
-BASE_URL
-E2E_LOGIN_USER
-E2E_LOGIN_PWD
-EOF
-    docker run --rm -v $(pwd):/ceph --env-file .env --name=e2e --network=host --entrypoint "" \
-        docker.io/rhcsdashboard/e2e npm run e2e:ci || failed=1
-    stop $failed
-else
-    echo "ERROR: Device not recognized. Valid devices are 'chrome' and 'docker'."
-    stop 1
+if [ -n "$CYPRESS_RECORD_KEY" ]; then
+    RECORD="--record --key $CYPRESS_RECORD_KEY"
 fi
+
+case "$DEVICE" in
+    electron)
+        npx cypress run $RECORD || stop 1
+        ;;
+    chrome)
+        npx cypress run $RECORD --browser chrome --headless || stop 1
+        ;;
+    chromium)
+        npx cypress run $RECORD --browser chromium --headless || stop 1
+        ;;
+    docker)
+        failed=0
+        docker run \
+            -v $(pwd):/e2e \
+            -w /e2e \
+            --env CYPRESS_BASE_URL \
+            --env CYPRESS_LOGIN_USER \
+            --env CYPRESS_LOGIN_PWD \
+            --name=e2e \
+            --network=host \
+            cypress/included:4.4.0 || failed=1
+        stop $failed
+        ;;
+esac
+
+stop 0