summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/cypress/integration
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:45:59 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:45:59 +0000
commit19fcec84d8d7d21e796c7624e521b60d28ee21ed (patch)
tree42d26aa27d1e3f7c0b8bd3fd14e7d7082f5008dc /src/pybind/mgr/dashboard/frontend/cypress/integration
parentInitial commit. (diff)
downloadceph-19fcec84d8d7d21e796c7624e521b60d28ee21ed.tar.xz
ceph-19fcec84d8d7d21e796c7624e521b60d28ee21ed.zip
Adding upstream version 16.2.11+ds.upstream/16.2.11+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/cypress/integration')
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.e2e-spec.ts95
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.po.ts110
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.e2e-spec.ts25
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.po.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.e2e-spec.ts54
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.po.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts78
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.po.ts75
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts56
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.e2e-spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.po.ts13
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.e2e-spec.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts184
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts58
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.po.ts70
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.e2e-spec.ts78
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.po.ts57
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.e2e-spec.ts62
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.po.ts7
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts57
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts84
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts204
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/common/01-global.feature.po.ts188
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/common/create-cluster/create-cluster.feature.po.ts12
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/common/grafana.feature.po.ts86
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/common/urls.po.ts44
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.e2e-spec.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.po.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts86
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/03-inventory.e2e-spec.ts26
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/04-osds.e2e-spec.ts50
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/05-services.e2e-spec.ts36
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/grafana/grafana.feature63
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/01-create-cluster-welcome.feature26
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/02-create-cluster-add-host.feature74
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts47
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts41
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts67
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/06-cluster-check.e2e-spec.ts99
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/07-osds.e2e-spec.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/08-hosts.e2e-spec.ts49
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/09-services.e2e-spec.ts114
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/10-nfs-exports.e2e-spec.ts83
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/nfs/nfs-export.po.ts52
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts309
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.e2e-spec.ts54
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.po.ts70
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.e2e-spec.ts62
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.po.ts193
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.e2e-spec.ts35
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.po.ts34
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.e2e-spec.ts46
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.po.ts139
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/api-docs.e2e-spec.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/api-docs.po.ts5
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.e2e-spec.ts124
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.po.ts31
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/language.e2e-spec.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/language.po.ts15
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/login.e2e-spec.ts17
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/login.po.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.e2e-spec.ts24
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.po.ts69
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.e2e-spec.ts59
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.po.ts45
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.e2e-spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.po.ts40
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.e2e-spec.ts37
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.po.ts39
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/visualTests/dashboard.vrt-spec.ts22
-rw-r--r--src/pybind/mgr/dashboard/frontend/cypress/integration/visualTests/login.vrt-spec.ts19
72 files changed, 4374 insertions, 0 deletions
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
index 000000000..5c89359db
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.e2e-spec.ts
@@ -0,0 +1,95 @@
+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();
+ Cypress.Cookies.preserveOnce('token');
+ // Need pool for image testing
+ pools.navigateTo('create');
+ pools.create(poolName, 8, 'rbd');
+ pools.existTableCell(poolName);
+ });
+
+ after(() => {
+ // Deletes images test pool
+ pools.navigateTo();
+ pools.delete(poolName);
+ pools.navigateTo();
+ pools.existTableCell(poolName, false);
+ });
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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();
+ Cypress.Cookies.preserveOnce('token');
+ // 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
index 000000000..bf6cbc052
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.po.ts
@@ -0,0 +1,110 @@
+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.get('[data-cy=submitBtn]').click();
+ this.getFirstTableCell(name).should('exist');
+ }
+
+ editImage(name: string, pool: string, newName: string, newSize: string) {
+ this.navigateEdit(name);
+
+ // 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.get('[data-cy=submitBtn]').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('button.move-to-trash').click();
+
+ cy.get('[data-cy=submitBtn]').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('cd-modal #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('cd-modal #name').clear().type(newName);
+ }
+
+ cy.get('[data-cy=submitBtn]').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('[data-cy=submitBtn]').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
index 000000000..cef4874be
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.e2e-spec.ts
@@ -0,0 +1,25 @@
+import { IscsiPageHelper } from './iscsi.po';
+
+describe('Iscsi Page', () => {
+ const iscsi = new IscsiPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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('be.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
index 000000000..08efa6408
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.po.ts
@@ -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
index 000000000..dfba73b27
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.e2e-spec.ts
@@ -0,0 +1,54 @@
+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();
+ Cypress.Cookies.preserveOnce('token');
+ 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 (0)');
+ mirroring.getTabText(1).should('eq', 'Syncing (0)');
+ mirroring.getTabText(2).should('eq', 'Ready (0)');
+ });
+
+ 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.existTableCell(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
index 000000000..e24a3ebc0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.po.ts
@@ -0,0 +1,35 @@
+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) {
+ // Select 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');
+
+ // unselect the pool in the table
+ this.getFirstTableCell(name).click();
+ }
+}
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
index 000000000..d022d59cf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts
@@ -0,0 +1,78 @@
+import { ConfigurationPageHelper } from './configuration.po';
+
+describe('Configuration page', () => {
+ const configuration = new ConfigurationPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ configuration.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ configuration.expectBreadcrumbText('Configuration');
+ });
+ });
+
+ describe('fields check', () => {
+ beforeEach(() => {
+ configuration.getExpandCollapseElement().click();
+ });
+
+ it('should check that details table opens (w/o tab header)', () => {
+ configuration.getStatusTables().should('be.visible');
+ configuration.getTabs().should('not.exist');
+ });
+ });
+
+ describe('edit configuration test', () => {
+ const configName = 'client_cache_size';
+
+ beforeEach(() => {
+ configuration.clearTableSearchInput();
+ configuration.getTableCount('found').as('configFound');
+ });
+
+ 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 verify modified filter is applied properly', () => {
+ configuration.filterTable('Modified', 'no');
+ configuration.getTableCount('found').as('unmodifiedConfigs');
+
+ // Modified filter value to yes
+ configuration.filterTable('Modified', 'yes');
+ configuration.getTableCount('found').as('modifiedConfigs');
+
+ cy.get('@configFound').then((configFound) => {
+ cy.get('@unmodifiedConfigs').then((unmodifiedConfigs) => {
+ const modifiedConfigs = Number(configFound) - Number(unmodifiedConfigs);
+ configuration.getTableCount('found').should('eq', modifiedConfigs);
+ });
+ });
+
+ // Modified filter value to no
+ configuration.filterTable('Modified', 'no');
+ cy.get('@configFound').then((configFound) => {
+ cy.get('@modifiedConfigs').then((modifiedConfigs) => {
+ const unmodifiedConfigs = Number(configFound) - Number(modifiedConfigs);
+ configuration.getTableCount('found').should('eq', unmodifiedConfigs);
+ });
+ });
+ });
+ });
+});
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
index 000000000..0133dc31f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.po.ts
@@ -0,0 +1,75 @@
+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) {
+ const valList = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']; // Editable values
+
+ this.navigateEdit(name);
+ // Waits 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.get('[data-cy=submitBtn]').click();
+
+ // Enter config setting name into filter box
+ this.searchTable(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][]) {
+ this.navigateEdit(name);
+
+ // Waits for data to load
+ 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.get('[data-cy=submitBtn]').click();
+
+ // Enter config setting name into filter box
+ this.searchTable(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/create-cluster.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts
new file mode 100644
index 000000000..300eddbcc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/create-cluster.po.ts
@@ -0,0 +1,56 @@
+import { PageHelper } from '../page-helper.po';
+import { NotificationSidebarPageHelper } from '../ui/notification.po';
+import { HostsPageHelper } from './hosts.po';
+import { ServicesPageHelper } from './services.po';
+
+const pages = {
+ index: { url: '#/expand-cluster', id: 'cd-create-cluster' }
+};
+export class CreateClusterWizardHelper extends PageHelper {
+ pages = pages;
+
+ createCluster() {
+ cy.get('cd-create-cluster').should('contain.text', 'Please expand your cluster first');
+ cy.get('[name=expand-cluster]').click();
+ cy.get('cd-wizard').should('exist');
+ }
+
+ doSkip() {
+ cy.get('[name=skip-cluster-creation]').click();
+ cy.contains('cd-modal button', 'Continue').click();
+
+ cy.get('cd-dashboard').should('exist');
+ const notification = new NotificationSidebarPageHelper();
+ notification.open();
+ notification.getNotifications().should('contain', 'Cluster expansion skipped by user');
+ }
+}
+
+export class CreateClusterHostPageHelper extends HostsPageHelper {
+ pages = {
+ index: { url: '#/expand-cluster', id: 'cd-wizard' },
+ add: { url: '', id: 'cd-host-form' }
+ };
+
+ columnIndex = {
+ hostname: 1,
+ labels: 2,
+ status: 3,
+ services: 0
+ };
+}
+
+export class CreateClusterServicePageHelper extends ServicesPageHelper {
+ pages = {
+ index: { url: '#/expand-cluster', id: 'cd-wizard' },
+ create: { url: '', id: 'cd-service-form' }
+ };
+
+ columnIndex = {
+ service_name: 1,
+ placement: 2,
+ running: 0,
+ size: 0,
+ last_refresh: 0
+ };
+}
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
index 000000000..0a454739f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.e2e-spec.ts
@@ -0,0 +1,37 @@
+import { CrushMapPageHelper } from './crush-map.po';
+
+describe('CRUSH map page', () => {
+ const crushmap = new CrushMapPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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(0).click();
+
+ crushmap
+ .getLegends()
+ .invoke('text')
+ .then((legend) => {
+ crushmap.getCrushNode(0).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
index 000000000..a5d2d591c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.po.ts
@@ -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
index 000000000..e4f9936c3
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.e2e-spec.ts
@@ -0,0 +1,35 @@
+import { HostsPageHelper } from './hosts.po';
+
+describe('Hosts page', () => {
+ const hosts = new HostsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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();
+ });
+ });
+});
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
index 000000000..33fe756ff
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.po.ts
@@ -0,0 +1,184 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/hosts', id: 'cd-hosts' },
+ add: { url: '#/hosts/(modal:add)', id: 'cd-host-form' }
+};
+
+export class HostsPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ hostname: 2,
+ services: 3,
+ labels: 4,
+ status: 5
+ };
+
+ check_for_host() {
+ this.getTableCount('total').should('not.be.eq', 0);
+ }
+
+ add(hostname: string, exist?: boolean, maintenance?: boolean, labels: string[] = []) {
+ cy.get(`${this.pages.add.id}`).within(() => {
+ cy.get('#hostname').type(hostname);
+ if (maintenance) {
+ cy.get('label[for=maintenance]').click();
+ }
+ if (exist) {
+ cy.get('#hostname').should('have.class', 'ng-invalid');
+ }
+ });
+
+ if (labels.length) {
+ this.selectPredefinedLabels(labels);
+ }
+
+ cy.get('cd-submit-button').click();
+ // back to host list
+ cy.get(`${this.pages.index.id}`);
+ }
+
+ selectPredefinedLabels(labels: string[]) {
+ cy.get('a[data-testid=select-menu-edit]').click();
+ for (const label of labels) {
+ cy.get('.popover-body div.select-menu-item-content').contains(label).click();
+ }
+ }
+
+ checkExist(hostname: string, exist: boolean) {
+ this.getTableCell(this.columnIndex.hostname, hostname).should(($elements) => {
+ const hosts = $elements.map((_, el) => el.textContent).get();
+ if (exist) {
+ expect(hosts).to.include(hostname);
+ } else {
+ expect(hosts).to.not.include(hostname);
+ }
+ });
+ }
+
+ remove(hostname: string) {
+ super.delete(hostname, this.columnIndex.hostname, 'hosts');
+ }
+
+ // Add or remove labels on a host, then verify labels in the table
+ editLabels(hostname: string, labels: string[], add: boolean) {
+ this.getTableCell(this.columnIndex.hostname, hostname).click();
+ this.clickActionButton('edit');
+
+ // add or remove label badges
+ if (add) {
+ cy.get('cd-modal').find('.select-menu-edit').click();
+ for (const label of labels) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`)).should('not.exist');
+ cy.get('.popover-body input').type(`${label}{enter}`);
+ }
+ } else {
+ for (const label of labels) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`))
+ .find('.badge-remove')
+ .click();
+ }
+ }
+ cy.get('cd-modal cd-submit-button').click();
+ this.checkLabelExists(hostname, labels, add);
+ }
+
+ checkLabelExists(hostname: string, labels: string[], add: boolean) {
+ // Verify labels are added or removed from Labels column
+ // First find row with hostname, then find labels in the row
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .click()
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.labels}) .badge`)
+ .should(($ele) => {
+ const newLabels = $ele.toArray().map((v) => v.innerText);
+ for (const label of labels) {
+ if (add) {
+ expect(newLabels).to.include(label);
+ } else {
+ expect(newLabels).to.not.include(label);
+ }
+ }
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ maintenance(hostname: string, exit = false, force = false) {
+ this.clearTableSearchInput();
+ this.getTableCell(this.columnIndex.hostname, hostname).click();
+ if (force) {
+ this.clickActionButton('enter-maintenance');
+
+ cy.get('cd-modal').within(() => {
+ cy.contains('button', 'Continue').click();
+ });
+
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include('maintenance');
+ });
+ }
+ if (exit) {
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ .then(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ if (status[0].includes('maintenance')) {
+ this.clickActionButton('exit-maintenance');
+ }
+ });
+
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.not.include('maintenance');
+ });
+ } else {
+ this.clickActionButton('enter-maintenance');
+
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include('maintenance');
+ });
+ }
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ drain(hostname: string) {
+ this.getTableCell(this.columnIndex.hostname, hostname).click();
+ this.clickActionButton('start-drain');
+ this.checkLabelExists(hostname, ['_no_schedule'], true);
+
+ // unselect it to avoid colliding with any other selection
+ // in different steps
+ this.getTableCell(this.columnIndex.hostname, hostname).click();
+
+ this.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ cy.wait(20000);
+ this.expectTableCount('total', 0);
+ });
+ }
+
+ checkServiceInstancesExist(hostname: string, instances: string[]) {
+ this.getTableCell(this.columnIndex.hostname, hostname)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.services}) .badge`)
+ .should(($ele) => {
+ const serviceInstances = $ele.toArray().map((v) => v.innerText);
+ for (const instance of instances) {
+ expect(serviceInstances).to.include(instance);
+ }
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts
new file mode 100644
index 000000000..5a9abdc03
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/inventory.po.ts
@@ -0,0 +1,22 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/inventory', id: 'cd-inventory' }
+};
+
+export class InventoryPageHelper extends PageHelper {
+ pages = pages;
+
+ identify() {
+ // Nothing we can do, just verify the form is there
+ this.getFirstTableCell().click();
+ cy.contains('cd-table-actions button', 'Identify').click();
+ cy.get('cd-modal').within(() => {
+ cy.get('#duration').select('15 minutes');
+ cy.get('#duration').select('10 minutes');
+ cy.get('cd-back-button').click();
+ });
+ cy.get('cd-modal').should('not.exist');
+ cy.get(`${this.pages.index.id}`);
+ }
+}
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
index 000000000..9868b89ae
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts
@@ -0,0 +1,58 @@
+import { PoolPageHelper } from '../pools/pools.po';
+import { LogsPageHelper } from './logs.po';
+
+describe('Logs page', () => {
+ const logs = new LogsPageHelper();
+ const pools = new PoolPageHelper();
+
+ const poolname = 'e2e_logs_test_pool';
+ const today = new Date();
+ let hour = today.getHours();
+ if (hour > 12) {
+ hour = hour - 12;
+ }
+ const minute = today.getMinutes();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ });
+
+ 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.existTableCell(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);
+ });
+ });
+});
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
index 000000000..7efd8a652
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.po.ts
@@ -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('.ngb-tp-input').its(0).clear();
+
+ if (hour < 10) {
+ cy.get('.ngb-tp-input').its(0).type('0');
+ }
+ cy.get('.ngb-tp-input').its(0).type(`${hour}`);
+
+ cy.get('.ngb-tp-input').its(1).clear();
+ if (minute < 10) {
+ cy.get('.ngb-tp-input').its(1).type('0');
+ }
+ cy.get('.ngb-tp-input').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('.ngb-tp-input').its(0).clear();
+ if (hour < 10) {
+ cy.get('.ngb-tp-input').its(0).type('0');
+ }
+ cy.get('.ngb-tp-input').its(0).type(`${hour}`);
+
+ cy.get('.ngb-tp-input').its(1).clear();
+ if (minute < 10) {
+ cy.get('.ngb-tp-input').its(1).type('0');
+ }
+ cy.get('.ngb-tp-input').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
index 000000000..50656fece
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.e2e-spec.ts
@@ -0,0 +1,78 @@
+import { Input, ManagerModulesPageHelper } from './mgr-modules.po';
+
+describe('Manager modules page', () => {
+ const mgrmodules = new ManagerModulesPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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 balancer module', () => {
+ const balancerArr: Input[] = [
+ {
+ id: 'crush_compat_max_iterations',
+ newValue: '123',
+ oldValue: '25'
+ }
+ ];
+ mgrmodules.editMgrModule('balancer', balancerArr);
+ });
+
+ it('should test editing on dashboard module', () => {
+ const dashboardArr: Input[] = [
+ {
+ id: 'GRAFANA_API_PASSWORD',
+ newValue: 'rafa',
+ oldValue: ''
+ }
+ ];
+ mgrmodules.editMgrModule('dashboard', dashboardArr);
+ });
+
+ it('should test editing on devicehealth module', () => {
+ const devHealthArray: Input[] = [
+ {
+ id: 'mark_out_threshold',
+ newValue: '1987',
+ oldValue: '2419200'
+ },
+ {
+ id: 'pool_name',
+ newValue: 'sox',
+ oldValue: 'device_health_metrics'
+ },
+ {
+ id: 'retention_period',
+ newValue: '1999',
+ oldValue: '15552000'
+ },
+ {
+ id: 'scrape_frequency',
+ newValue: '2020',
+ oldValue: '86400'
+ },
+ {
+ id: 'sleep_interval',
+ newValue: '456',
+ oldValue: '600'
+ },
+ {
+ id: 'warn_threshold',
+ newValue: '567',
+ oldValue: '7257600'
+ }
+ ];
+
+ mgrmodules.editMgrModule('devicehealth', devHealthArray);
+ });
+ });
+});
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
index 000000000..04d2eee46
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.po.ts
@@ -0,0 +1,57 @@
+import { PageHelper } from '../page-helper.po';
+
+export class Input {
+ id: string;
+ oldValue: string;
+ newValue: string;
+}
+
+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.
+ */
+ editMgrModule(name: string, inputs: Input[]) {
+ this.navigateEdit(name);
+
+ for (const input of inputs) {
+ // Clears fields and adds edits
+ cy.get(`#${input.id}`).clear().type(input.newValue);
+ }
+
+ cy.contains('button', 'Update').click();
+ // Checks if edits appear
+ this.getExpandCollapseElement(name).should('be.visible').click();
+
+ for (const input of inputs) {
+ cy.get('.datatable-body').last().contains(input.newValue);
+ }
+
+ // Clear mgr module of all edits made to it
+ this.navigateEdit(name);
+
+ // Clears the editable fields
+ for (const input of inputs) {
+ if (input.oldValue) {
+ const id = `#${input.id}`;
+ cy.get(id).clear();
+ if (input.oldValue) {
+ cy.get(id).type(input.oldValue);
+ }
+ }
+ }
+
+ // Checks that clearing represents in details tab of module
+ cy.contains('button', 'Update').click();
+ this.getExpandCollapseElement(name).should('be.visible').click();
+ for (const input of inputs) {
+ if (input.oldValue) {
+ cy.get('.datatable-body')
+ .eq(1)
+ .should('contain', input.id)
+ .and('not.contain', input.newValue);
+ }
+ }
+ }
+}
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
index 000000000..a23d071e6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.e2e-spec.ts
@@ -0,0 +1,62 @@
+import { MonitorsPageHelper } from './monitors.po';
+
+describe('Monitors page', () => {
+ const monitors = new MonitorsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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
index 000000000..4113b9928
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.po.ts
@@ -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
index 000000000..e68d03bd5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts
@@ -0,0 +1,57 @@
+import { OSDsPageHelper } from './osds.po';
+
+describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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.getTableCount('total').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 show the correct text for the tab labels', () => {
+ cy.get('#tabset-osd-details > li > a').then(($tabs) => {
+ const tabHeadings = $tabs.map((_i, e) => e.textContent).get();
+
+ expect(tabHeadings).to.eql([
+ 'Devices',
+ 'Attributes (OSD map)',
+ 'Metadata',
+ 'Device health',
+ 'Performance counter',
+ '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
index 000000000..cd812f474
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.po.ts
@@ -0,0 +1,84 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/osd', id: 'cd-osd-list' },
+ create: { url: '#/osd/create', id: 'cd-osd-form' }
+};
+
+export class OSDsPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ id: 3,
+ status: 5
+ };
+
+ create(deviceType: 'hdd' | 'ssd', hostname?: string, expandCluster = false) {
+ cy.get('[aria-label="toggle advanced mode"]').click();
+ // Click Primary devices Add button
+ cy.get('cd-osd-devices-selection-groups[name="Primary"]').as('primaryGroups');
+ cy.get('@primaryGroups').find('button').click();
+
+ // Select all devices with `deviceType`
+ cy.get('cd-osd-devices-selection-modal').within(() => {
+ cy.get('.modal-footer .tc_submitButton').as('addButton').should('be.disabled');
+ this.filterTable('Type', deviceType);
+ if (hostname) {
+ this.filterTable('Hostname', hostname);
+ }
+
+ if (expandCluster) {
+ this.getTableCount('total').should('be.gte', 1);
+ }
+ cy.get('@addButton').click();
+ });
+
+ if (!expandCluster) {
+ cy.get('@primaryGroups').within(() => {
+ this.getTableCount('total').as('newOSDCount');
+ });
+
+ cy.get(`${pages.create.id} .card-footer .tc_submitButton`).click();
+ cy.get(`cd-osd-creation-preview-modal .modal-footer .tc_submitButton`).click();
+ }
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ checkStatus(id: number, status: string[]) {
+ this.searchTable(`id:${id}`);
+ this.expectTableCount('found', 1);
+ cy.get(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`).should(($ele) => {
+ const allStatus = $ele.toArray().map((v) => v.innerText);
+ for (const s of status) {
+ expect(allStatus).to.include(s);
+ }
+ });
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ ensureNoOsd(id: number) {
+ this.searchTable(`id:${id}`);
+ this.expectTableCount('found', 0);
+ this.clearTableSearchInput();
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ deleteByIDs(osdIds: number[], replace?: boolean) {
+ this.getTableRows().each(($el) => {
+ const rowOSD = Number(
+ $el.find('datatable-body-cell .datatable-body-cell-label').get(this.columnIndex.id - 1)
+ .textContent
+ );
+ if (osdIds.includes(rowOSD)) {
+ cy.wrap($el).click();
+ }
+ });
+ this.clickActionButton('delete');
+ if (replace) {
+ cy.get('cd-modal label[for="preserve"]').click();
+ }
+ cy.get('cd-modal label[for="confirmation"]').click();
+ cy.contains('cd-modal button', 'Delete').click();
+ cy.get('cd-modal').should('not.exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts
new file mode 100644
index 000000000..481d6bc9b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/services.po.ts
@@ -0,0 +1,204 @@
+import { PageHelper } from '../page-helper.po';
+
+const pages = {
+ index: { url: '#/services', id: 'cd-services' },
+ create: { url: '#/services/(modal:create)', id: 'cd-service-form' }
+};
+
+export class ServicesPageHelper extends PageHelper {
+ pages = pages;
+
+ columnIndex = {
+ service_name: 2,
+ placement: 3,
+ running: 4,
+ size: 5,
+ last_refresh: 6
+ };
+
+ serviceDetailColumnIndex = {
+ daemonName: 2,
+ status: 4
+ };
+
+ check_for_service() {
+ this.getTableCount('total').should('not.be.eq', 0);
+ }
+
+ private selectServiceType(serviceType: string) {
+ return this.selectOption('service_type', serviceType);
+ }
+
+ clickServiceTab(serviceName: string, tabName: string) {
+ this.getExpandCollapseElement(serviceName).click();
+ cy.get('cd-service-details').within(() => {
+ this.getTab(tabName).click();
+ });
+ }
+
+ addService(
+ serviceType: string,
+ exist?: boolean,
+ count = '1',
+ snmpVersion?: string,
+ snmpPrivProtocol?: boolean,
+ unmanaged = false
+ ) {
+ cy.get(`${this.pages.create.id}`).within(() => {
+ this.selectServiceType(serviceType);
+ switch (serviceType) {
+ case 'rgw':
+ cy.get('#service_id').type('foo');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(count);
+ break;
+
+ case 'ingress':
+ if (unmanaged) {
+ cy.get('label[for=unmanaged]').click();
+ }
+ this.selectOption('backend_service', 'rgw.foo');
+ cy.get('#service_id').should('have.value', 'rgw.foo');
+ cy.get('#virtual_ip').type('192.168.100.1/24');
+ cy.get('#frontend_port').type('8081');
+ cy.get('#monitor_port').type('8082');
+ break;
+
+ case 'nfs':
+ cy.get('#service_id').type('testnfs');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(count);
+ break;
+
+ case 'snmp-gateway':
+ this.selectOption('snmp_version', snmpVersion);
+ cy.get('#snmp_destination').type('192.168.0.1:8443');
+ if (snmpVersion === 'V2c') {
+ cy.get('#snmp_community').type('public');
+ } else {
+ cy.get('#engine_id').type('800C53F00000');
+ this.selectOption('auth_protocol', 'SHA');
+ if (snmpPrivProtocol) {
+ this.selectOption('privacy_protocol', 'DES');
+ cy.get('#snmp_v3_priv_password').type('testencrypt');
+ }
+
+ // Credentials
+ cy.get('#snmp_v3_auth_username').type('test');
+ cy.get('#snmp_v3_auth_password').type('testpass');
+ }
+ break;
+
+ default:
+ cy.get('#service_id').type('test');
+ unmanaged ? cy.get('label[for=unmanaged]').click() : cy.get('#count').type(count);
+ break;
+ }
+ if (serviceType === 'snmp-gateway') {
+ cy.get('cd-submit-button').dblclick();
+ } else {
+ cy.get('cd-submit-button').click();
+ }
+ });
+ if (exist) {
+ cy.get('#service_id').should('have.class', 'ng-invalid');
+ } else {
+ // back to service list
+ cy.get(`${this.pages.index.id}`);
+ }
+ }
+
+ editService(name: string, daemonCount: string) {
+ this.navigateEdit(name, true, false);
+ cy.get(`${this.pages.create.id}`).within(() => {
+ cy.get('#service_type').should('be.disabled');
+ cy.get('#service_id').should('be.disabled');
+ cy.get('#count').clear().type(daemonCount);
+ cy.get('cd-submit-button').click();
+ });
+ }
+
+ checkServiceStatus(daemon: string, expectedStatus = 'running') {
+ let daemonNameIndex = this.serviceDetailColumnIndex.daemonName;
+ let statusIndex = this.serviceDetailColumnIndex.status;
+
+ // since hostname row is hidden from the hosts details table,
+ // we'll need to manually override the indexes when this check is being
+ // done for the daemons in host details page. So we'll get the url and
+ // verify if the current page is not the services index page
+ cy.url().then((url) => {
+ if (!url.includes(pages.index.url)) {
+ daemonNameIndex = 1;
+ statusIndex = 3;
+ }
+
+ cy.get('cd-service-daemon-list').within(() => {
+ this.getTableCell(daemonNameIndex, daemon, true)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${statusIndex}) .badge`)
+ .should(($ele) => {
+ const status = $ele.toArray().map((v) => v.innerText);
+ expect(status).to.include(expectedStatus);
+ });
+ });
+ });
+ }
+
+ expectPlacementCount(serviceName: string, expectedCount: string) {
+ this.getTableCell(this.columnIndex.service_name, serviceName)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+ .should(($ele) => {
+ const running = $ele.text().split(';');
+ expect(running).to.include(`count:${expectedCount}`);
+ });
+ }
+
+ checkExist(serviceName: string, exist: boolean) {
+ this.getTableCell(this.columnIndex.service_name, serviceName).should(($elements) => {
+ const services = $elements.map((_, el) => el.textContent).get();
+ if (exist) {
+ expect(services).to.include(serviceName);
+ } else {
+ expect(services).to.not.include(serviceName);
+ }
+ });
+ }
+
+ isUnmanaged(serviceName: string, unmanaged: boolean) {
+ this.getTableCell(this.columnIndex.service_name, serviceName)
+ .parent()
+ .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+ .should(($ele) => {
+ const placement = $ele.text().split(';');
+ unmanaged
+ ? expect(placement).to.include('unmanaged')
+ : expect(placement).to.not.include('unmanaged');
+ });
+ }
+
+ deleteService(serviceName: string) {
+ const getRow = this.getTableCell.bind(this, this.columnIndex.service_name);
+ getRow(serviceName).click();
+
+ // Clicks on table Delete button
+ this.clickActionButton('delete');
+
+ // Confirms deletion
+ cy.get('cd-modal .custom-control-label').click();
+ cy.contains('cd-modal button', 'Delete').click();
+
+ // Wait for modal to close
+ cy.get('cd-modal').should('not.exist');
+ this.checkExist(serviceName, false);
+ }
+
+ daemonAction(daemon: string, action: string) {
+ cy.get('cd-service-daemon-list').within(() => {
+ this.getTableRow(daemon).click();
+ this.clickActionButton(action);
+
+ // unselect it to avoid colliding with any other selection
+ // in different steps
+ this.getTableRow(daemon).click();
+ });
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/common/01-global.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/common/01-global.feature.po.ts
new file mode 100644
index 000000000..575d4013b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/common/01-global.feature.po.ts
@@ -0,0 +1,188 @@
+import { And, Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
+
+import { UrlsCollection } from './urls.po';
+
+const urlsCollection = new UrlsCollection();
+
+Given('I am logged in', () => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+});
+
+Given('I am on the {string} page', (page: string) => {
+ cy.visit(urlsCollection.pages[page].url);
+ cy.get(urlsCollection.pages[page].id).should('exist');
+});
+
+Then('I should be on the {string} page', (page: string) => {
+ cy.get(urlsCollection.pages[page].id).should('exist');
+});
+
+And('I should see a button to {string}', (button: string) => {
+ cy.get(`[aria-label="${button}"]`).should('be.visible');
+});
+
+When('I click on {string} button', (button: string) => {
+ cy.get(`[aria-label="${button}"]`).first().click();
+});
+
+// When you are clicking on an action in the table actions dropdown button
+When('I click on {string} button from the table actions', (button: string) => {
+ cy.get('.table-actions button.dropdown-toggle').first().click();
+ cy.get(`[aria-label="${button}"]`).first().click();
+});
+
+And('select options {string}', (labels: string) => {
+ if (labels) {
+ cy.get('a[data-testid=select-menu-edit]').click();
+ for (const label of labels.split(', ')) {
+ cy.get('.popover-body div.select-menu-item-content').contains(label).click();
+ }
+ }
+});
+
+And('{string} option {string}', (action: string, labels: string) => {
+ if (labels) {
+ if (action === 'add') {
+ cy.get('cd-modal').find('.select-menu-edit').click();
+ for (const label of labels.split(', ')) {
+ cy.get('.popover-body input').type(`${label}{enter}`);
+ }
+ } else {
+ for (const label of labels.split(', ')) {
+ cy.contains('cd-modal .badge', new RegExp(`^${label}$`))
+ .find('.badge-remove')
+ .click();
+ }
+ }
+ }
+});
+
+/**
+ * Fills in the given field using the value provided
+ * @param field ID of the field that needs to be filled out.
+ * @param value Value that should be filled in the field.
+ */
+And('enter {string} {string}', (field: string, value: string) => {
+ cy.get('cd-modal').within(() => {
+ cy.get(`input[id=${field}]`).type(value);
+ });
+});
+
+And('I click on submit button', () => {
+ cy.get('[data-cy=submitBtn]').click();
+});
+
+/**
+ * Selects any row on the datatable if it matches the given name
+ */
+When('I select a row {string}', (row: string) => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).click();
+});
+
+Then('I should see the modal', () => {
+ cy.get('cd-modal').should('exist');
+});
+
+Then('I should not see the modal', () => {
+ cy.get('cd-modal').should('not.exist');
+});
+
+/**
+ * Some modals have an additional confirmation to be provided
+ * by ticking the 'Are you sure?' box.
+ */
+Then('I check the tick box in modal', () => {
+ cy.get('cd-modal .custom-control-label').click();
+});
+
+And('I confirm to {string}', (action: string) => {
+ cy.contains('cd-modal button', action).click();
+ cy.get('cd-modal').should('not.exist');
+});
+
+Then('I should see an error in {string} field', (field: string) => {
+ cy.get('cd-modal').within(() => {
+ cy.get(`input[id=${field}]`).should('have.class', 'ng-invalid');
+ });
+});
+
+Then('I should see a row with {string}', (row: string) => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'exist'
+ );
+});
+
+Then('I should not see a row with {string}', (row: string) => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
+ 'not.exist'
+ );
+});
+
+Then('I should see rows with following entries', (entries) => {
+ entries.hashes().forEach((entry: any) => {
+ cy.get('cd-table .search input').first().clear().type(entry.hostname);
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label`,
+ entry.hostname
+ ).should('exist');
+ });
+});
+
+And('I should see row {string} have {string}', (row: string, options: string) => {
+ if (options) {
+ cy.get('cd-table .search input').first().clear().type(row);
+ for (const option of options.split(',')) {
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label .badge`,
+ option
+ ).should('exist');
+ }
+ }
+});
+
+And('I should see row {string} does not have {string}', (row: string, options: string) => {
+ if (options) {
+ cy.get('cd-table .search input').first().clear().type(row);
+ for (const option of options.split(',')) {
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label .badge`,
+ option
+ ).should('not.exist');
+ }
+ }
+});
+
+And('I go to the {string} tab', (names: string) => {
+ for (const name of names.split(', ')) {
+ cy.contains('.nav.nav-tabs li', name).click();
+ }
+});
+
+And('select {string} {string}', (selectionName: string, option: string) => {
+ cy.get(`select[name=${selectionName}]`).select(option);
+ cy.get(`select[name=${selectionName}] option:checked`).contains(option);
+});
+
+When('I expand the row {string}', (row: string) => {
+ cy.contains('.datatable-body-row', row).first().find('.tc_expand-collapse').click();
+});
+
+And('I should see row {string} have {string} on this tab', (row: string, options: string) => {
+ if (options) {
+ cy.get('cd-table').should('exist');
+ cy.get('datatable-scroller, .empty-row');
+ cy.get('.datatable-row-detail').within(() => {
+ cy.get('cd-table .search input').first().clear().type(row);
+ for (const option of options.split(',')) {
+ cy.contains(
+ `datatable-body-row datatable-body-cell .datatable-body-cell-label span`,
+ option
+ ).should('exist');
+ }
+ });
+ }
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/common/create-cluster/create-cluster.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/common/create-cluster/create-cluster.feature.po.ts
new file mode 100644
index 000000000..d18c34855
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/common/create-cluster/create-cluster.feature.po.ts
@@ -0,0 +1,12 @@
+import { Given, Then } from 'cypress-cucumber-preprocessor/steps';
+
+Given('I am on the {string} section', (page: string) => {
+ cy.get('cd-wizard').within(() => {
+ cy.get('.nav-link').should('contain.text', page).first().click();
+ cy.get('.nav-link.active').should('contain.text', page);
+ });
+});
+
+Then('I should see a message {string}', () => {
+ cy.get('cd-create-cluster').should('contain.text', 'Please expand your cluster first');
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/common/grafana.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/common/grafana.feature.po.ts
new file mode 100644
index 000000000..7366f8bab
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/common/grafana.feature.po.ts
@@ -0,0 +1,86 @@
+import { e2e } from '@grafana/e2e';
+import { Then, When } from 'cypress-cucumber-preprocessor/steps';
+import 'cypress-iframe';
+
+function getIframe() {
+ cy.frameLoaded('#iframe');
+ return cy.iframe();
+}
+
+Then('I should see the grafana panel {string}', (panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ e2e.components.Panels.Panel.title(panel).should('be.visible');
+ });
+ }
+ });
+});
+
+When('I view the grafana panel {string}', (panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ e2e.components.Panels.Panel.title(panel).should('be.visible').click();
+ e2e.components.Panels.Panel.headerItems('View').should('be.visible').click();
+ });
+ }
+ });
+});
+
+Then('I should not see {string} in the panel {string}', (value: string, panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`)
+ .should('be.visible')
+ .within(() => {
+ cy.get('span').first().should('not.have.text', value);
+ });
+ });
+ }
+ });
+});
+
+Then(
+ 'I should see the legends {string} in the graph {string}',
+ (legends: string, panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`)
+ .should('be.visible')
+ .within(() => {
+ for (const legend of legends.split(', ')) {
+ cy.get('a').contains(legend);
+ }
+ });
+ });
+ }
+ });
+ }
+);
+
+Then('I should not see No Data in the graph {string}', (panels: string) => {
+ getIframe().within(() => {
+ for (const panel of panels.split(', ')) {
+ cy.get('.grafana-app')
+ .wait(100)
+ .within(() => {
+ cy.get(`[aria-label="${panel} panel"]`)
+ .should('be.visible')
+ .within(() => {
+ cy.get('div.datapoints-warning').should('not.exist');
+ });
+ });
+ }
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/common/urls.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/common/urls.po.ts
new file mode 100644
index 000000000..286355085
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/common/urls.po.ts
@@ -0,0 +1,44 @@
+import { PageHelper } from '../page-helper.po';
+
+export class UrlsCollection extends PageHelper {
+ pages = {
+ // Cluster expansion
+ welcome: { url: '#/expand-cluster', id: 'cd-create-cluster' },
+
+ // Landing page
+ dashboard: { url: '#/dashboard', id: 'cd-dashboard' },
+
+ // Hosts
+ hosts: { url: '#/hosts', id: 'cd-hosts' },
+ 'add hosts': { url: '#/hosts/(modal:add)', id: 'cd-host-form' },
+
+ // Services
+ services: { url: '#/services', id: 'cd-services' },
+ 'create services': { url: '#/services/(modal:create)', id: 'cd-service-form' },
+
+ // Physical Disks
+ 'physical disks': { url: '#/inventory', id: 'cd-inventory' },
+
+ // Monitors
+ monitors: { url: '#/monitor', id: 'cd-monitor' },
+
+ // OSDs
+ osds: { url: '#/osd', id: 'cd-osd-list' },
+ 'create osds': { url: '#/osd/create', id: 'cd-osd-form' },
+
+ // Configuration
+ configuration: { url: '#/configuration', id: 'cd-configuration' },
+
+ // Crush Map
+ 'crush map': { url: '#/crush-map', id: 'cd-crushmap' },
+
+ // Mgr modules
+ 'mgr-modules': { url: '#/mgr-modules', id: 'cd-mgr-module-list' },
+
+ // Logs
+ logs: { url: '#/logs', id: 'cd-logs' },
+
+ // RGW Daemons
+ 'rgw daemons': { url: '#/rgw/daemon', id: 'cd-rgw-daemon-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
index 000000000..e623475fd
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.e2e-spec.ts
@@ -0,0 +1,17 @@
+import { FilesystemsPageHelper } from './filesystems.po';
+
+describe('File Systems page', () => {
+ const filesystems = new FilesystemsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ filesystems.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ filesystems.expectBreadcrumbText('File Systems');
+ });
+ });
+});
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
index 000000000..bd6e5b8b7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.po.ts
@@ -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/orchestrator/01-hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts
new file mode 100644
index 000000000..aca36ade1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts
@@ -0,0 +1,86 @@
+import { HostsPageHelper } from '../cluster/hosts.po';
+
+describe('Hosts page', () => {
+ const hosts = new HostsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ hosts.navigateTo();
+ });
+
+ describe('when Orchestrator is available', () => {
+ beforeEach(function () {
+ cy.fixture('orchestrator/inventory.json').as('hosts');
+ cy.fixture('orchestrator/services.json').as('services');
+ });
+
+ it('should not add an exsiting host', function () {
+ const hostname = Cypress._.sample(this.hosts).name;
+ hosts.navigateTo('add');
+ hosts.add(hostname, true);
+ });
+
+ it('should drain and remove a host and then add it back', function () {
+ const hostname = Cypress._.last(this.hosts)['name'];
+
+ // should drain the host first before deleting
+ hosts.drain(hostname);
+ hosts.remove(hostname);
+
+ // add it back
+ hosts.navigateTo('add');
+ hosts.add(hostname);
+ hosts.checkExist(hostname, true);
+ });
+
+ it('should display inventory', function () {
+ for (const host of this.hosts) {
+ hosts.clickTab('cd-host-details', host.name, 'Physical Disks');
+ cy.get('cd-host-details').within(() => {
+ hosts.expectTableCount('total', host.devices.length);
+ });
+ }
+ });
+
+ it('should display daemons', function () {
+ for (const host of this.hosts) {
+ hosts.clickTab('cd-host-details', host.name, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ }
+ });
+
+ it('should edit host labels', function () {
+ const hostname = Cypress._.sample(this.hosts).name;
+ const labels = ['foo', 'bar'];
+ hosts.editLabels(hostname, labels, true);
+ hosts.editLabels(hostname, labels, false);
+ });
+
+ it('should enter host into maintenance', function () {
+ const hostname = Cypress._.sample(this.hosts).name;
+ const serviceList = new Array();
+ this.services.forEach((service: any) => {
+ if (hostname === service.hostname) {
+ serviceList.push(service.daemon_type);
+ }
+ });
+ let enterMaintenance = true;
+ serviceList.forEach((service: string) => {
+ if (service === 'mgr' || service === 'alertmanager') {
+ enterMaintenance = false;
+ }
+ });
+ if (enterMaintenance) {
+ hosts.maintenance(hostname);
+ }
+ });
+
+ it('should exit host from maintenance', function () {
+ const hostname = Cypress._.sample(this.hosts).name;
+ hosts.maintenance(hostname, true);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/03-inventory.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/03-inventory.e2e-spec.ts
new file mode 100644
index 000000000..a64e3bc8c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/03-inventory.e2e-spec.ts
@@ -0,0 +1,26 @@
+import { InventoryPageHelper } from '../cluster/inventory.po';
+
+describe('Physical Disks page', () => {
+ const inventory = new InventoryPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ inventory.navigateTo();
+ });
+
+ it('should have correct devices', () => {
+ cy.fixture('orchestrator/inventory.json').then((hosts) => {
+ const totalDiskCount = Cypress._.sumBy(hosts, 'devices.length');
+ inventory.expectTableCount('total', totalDiskCount);
+ for (const host of hosts) {
+ inventory.filterTable('Hostname', host['name']);
+ inventory.getTableCount('found').should('be.eq', host.devices.length);
+ }
+ });
+ });
+
+ it('should identify device', () => {
+ inventory.identify();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/04-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/04-osds.e2e-spec.ts
new file mode 100644
index 000000000..41f0933b7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/04-osds.e2e-spec.ts
@@ -0,0 +1,50 @@
+import { OSDsPageHelper } from '../cluster/osds.po';
+import { DashboardPageHelper } from '../ui/dashboard.po';
+
+describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+ const dashboard = new DashboardPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ osds.navigateTo();
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create and delete OSDs', () => {
+ osds.getTableCount('total').as('initOSDCount');
+ osds.navigateTo('create');
+ osds.create('hdd');
+
+ cy.get('@newOSDCount').then((newCount) => {
+ cy.get('@initOSDCount').then((oldCount) => {
+ const expectedCount = Number(oldCount) + Number(newCount);
+
+ // check total rows
+ osds.expectTableCount('total', expectedCount);
+
+ // landing page is easier to check OSD status
+ dashboard.navigateTo();
+ dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} total`);
+ dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} up`);
+ dashboard.infoCardBody('OSDs').should('contain.text', `${expectedCount} in`);
+
+ cy.wait(30000);
+ expect(Number(newCount)).to.be.gte(2);
+ // Delete the first OSD we created
+ osds.navigateTo();
+ const deleteOsdId = Number(oldCount);
+ osds.deleteByIDs([deleteOsdId], false);
+ osds.ensureNoOsd(deleteOsdId);
+
+ cy.wait(30000);
+ // Replace the second OSD we created
+ const replaceID = Number(oldCount) + 1;
+ osds.deleteByIDs([replaceID], true);
+ osds.checkStatus(replaceID, ['destroyed']);
+ });
+ });
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/05-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/05-services.e2e-spec.ts
new file mode 100644
index 000000000..fb5e6ac89
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/05-services.e2e-spec.ts
@@ -0,0 +1,36 @@
+import { ServicesPageHelper } from '../cluster/services.po';
+
+describe('Services page', () => {
+ const services = new ServicesPageHelper();
+ const serviceName = 'rgw.foo';
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ services.navigateTo();
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create an rgw service', () => {
+ services.navigateTo('create');
+ services.addService('rgw');
+
+ services.checkExist(serviceName, true);
+ });
+
+ it('should edit a service', () => {
+ const count = '2';
+ services.editService(serviceName, count);
+ services.expectPlacementCount(serviceName, count);
+ });
+
+ it('should create and delete an ingress service', () => {
+ services.navigateTo('create');
+ services.addService('ingress');
+
+ services.checkExist('ingress.rgw.foo', true);
+
+ services.deleteService('ingress.rgw.foo');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/grafana/grafana.feature b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/grafana/grafana.feature
new file mode 100644
index 000000000..62476ad25
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/grafana/grafana.feature
@@ -0,0 +1,63 @@
+Feature: Grafana panels
+
+ Go to some of the grafana performance section and check if
+ panels are populated without any issues
+
+ Background: Log in
+ Given I am logged in
+
+ Scenario Outline: Hosts Overall Performance
+ Given I am on the "hosts" page
+ When I go to the "Overall Performance" tab
+ Then I should see the grafana panel "<panel>"
+ When I view the grafana panel "<panel>"
+ Then I should not see "No Data" in the panel "<panel>"
+
+ Examples:
+ | panel |
+ | OSD Hosts |
+ | AVG CPU Busy |
+ | AVG RAM Utilization |
+ | Physical IOPS |
+ | AVG Disk Utilization |
+ | Network Load |
+ | CPU Busy - Top 10 Hosts |
+ | Network Load - Top 10 Hosts |
+
+ Scenario Outline: RGW Daemon Overall Performance
+ Given I am on the "rgw daemons" page
+ When I go to the "Overall Performance" tab
+ Then I should see the grafana panel "<panel>"
+ When I view the grafana panel "<panel>"
+ Then I should not see No Data in the graph "<panel>"
+ And I should see the legends "<legends>" in the graph "<panel>"
+
+ Examples:
+ | panel | legends |
+ | Total Requests/sec by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | GET Latencies by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | Bandwidth by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | PUT Latencies by RGW Instance | foo.ceph-node-00, foo.ceph-node-01, foo.ceph-node-02 |
+ | Average GET/PUT Latencies | GET AVG, PUT AVG |
+ | Bandwidth Consumed by Type | GETs, PUTs |
+
+ Scenario Outline: RGW per Daemon Performance
+ Given I am on the "rgw daemons" page
+ When I expand the row "<name>"
+ And I go to the "Performance Details" tab
+ Then I should see the grafana panel "<panel>"
+ When I view the grafana panel "<panel>"
+ Then I should not see No Data in the graph "<panel>"
+ And I should see the legends "<name>" in the graph "<panel>"
+
+ Examples:
+ | name | panel |
+ | foo.ceph-node-00 | Bandwidth by HTTP Operation |
+ | foo.ceph-node-00 | HTTP Request Breakdown |
+ | foo.ceph-node-00 | Workload Breakdown |
+ | foo.ceph-node-01 | Bandwidth by HTTP Operation |
+ | foo.ceph-node-01 | HTTP Request Breakdown |
+ | foo.ceph-node-01 | Workload Breakdown |
+ | foo.ceph-node-02 | Bandwidth by HTTP Operation |
+ | foo.ceph-node-02 | HTTP Request Breakdown |
+ | foo.ceph-node-02 | Workload Breakdown |
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/01-create-cluster-welcome.feature b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/01-create-cluster-welcome.feature
new file mode 100644
index 000000000..6ba2fc4fc
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/01-create-cluster-welcome.feature
@@ -0,0 +1,26 @@
+Feature: Cluster expansion welcome screen
+
+ Go to the welcome screen and decide whether
+ to proceed to wizard or skips to landing page
+
+ Background: Login
+ Given I am logged in
+
+ Scenario: Cluster expansion welcome screen
+ Given I am on the "welcome" page
+ And I should see a button to "Expand Cluster"
+ And I should see a button to "Skip"
+ And I should see a message "Please expand your cluster first"
+
+ Scenario: Go to the Cluster expansion wizard
+ Given I am on the "welcome" page
+ And I should see a button to "Expand Cluster"
+ When I click on "Expand Cluster" button
+ Then I am on the "Add Hosts" section
+
+ Scenario: Skips the process and go to the landing page
+ Given I am on the "welcome" page
+ And I should see a button to "Skip"
+ When I click on "Skip" button
+ And I confirm to "Continue"
+ Then I should be on the "dashboard" page
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/02-create-cluster-add-host.feature b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/02-create-cluster-add-host.feature
new file mode 100644
index 000000000..93c10833d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/02-create-cluster-add-host.feature
@@ -0,0 +1,74 @@
+Feature: Cluster expansion host addition
+
+ Add some hosts and perform some host related actions like editing the labels
+ and removing the hosts from the cluster and verify all of the actions are performed
+ as expected
+
+ Background: Cluster expansion wizard
+ Given I am logged in
+ And I am on the "welcome" page
+ And I click on "Expand Cluster" button
+
+ Scenario Outline: Add hosts
+ Given I am on the "Add Hosts" section
+ When I click on "Add" button
+ And enter "hostname" "<hostname>"
+ And select options "<labels>"
+ And I click on submit button
+ Then I should see a row with "<hostname>"
+ And I should see row "<hostname>" have "<labels>"
+
+ Examples:
+ | hostname | labels |
+ | ceph-node-01 | mon, mgr |
+ | ceph-node-02 ||
+
+ Scenario Outline: Remove hosts
+ Given I am on the "Add Hosts" section
+ And I should see a row with "<hostname>"
+ When I select a row "<hostname>"
+ And I click on "Remove" button from the table actions
+ Then I should see the modal
+ And I check the tick box in modal
+ And I click on "Remove Host" button
+ Then I should not see the modal
+ And I should not see a row with "<hostname>"
+
+ Examples:
+ | hostname |
+ | ceph-node-01 |
+ | ceph-node-02 |
+
+ Scenario: Add hosts using pattern 'ceph-node-[01-02]'
+ Given I am on the "Add Hosts" section
+ When I click on "Add" button
+ And enter "hostname" "ceph-node-[01-02]"
+ And I click on submit button
+ Then I should see rows with following entries
+ | hostname |
+ | ceph-node-01 |
+ | ceph-node-02 |
+
+ Scenario: Add exisiting host and verify it failed
+ Given I am on the "Add Hosts" section
+ And I should see a row with "ceph-node-00"
+ When I click on "Add" button
+ And enter "hostname" "ceph-node-00"
+ Then I should see an error in "hostname" field
+
+ Scenario Outline: Add and remove labels on host
+ Given I am on the "Add Hosts" section
+ When I select a row "<hostname>"
+ And I click on "Edit" button from the table actions
+ And "add" option "<labels>"
+ And I click on submit button
+ Then I should see row "<hostname>" have "<labels>"
+ When I select a row "<hostname>"
+ And I click on "Edit" button from the table actions
+ And "remove" option "<labels>"
+ And I click on submit button
+ Then I should see row "<hostname>" does not have "<labels>"
+
+ Examples:
+ | hostname | labels |
+ | ceph-node-01 | foo |
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts
new file mode 100644
index 000000000..7668cafcf
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts
@@ -0,0 +1,47 @@
+/* tslint:disable*/
+import {
+ CreateClusterServicePageHelper,
+ CreateClusterWizardHelper
+} from '../../cluster/create-cluster.po';
+/* tslint:enable*/
+
+describe('Create cluster create services page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+ const createClusterServicePage = new CreateClusterServicePageHelper();
+
+ const createService = (serviceType: string, serviceName: string, count = '1') => {
+ cy.get('[aria-label=Create]').first().click();
+ createClusterServicePage.addService(serviceType, false, count);
+ createClusterServicePage.checkExist(serviceName, true);
+ };
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('.nav-link').contains('Create Services').click();
+ });
+
+ it('should check if title contains Create Services', () => {
+ cy.get('.title').should('contain.text', 'Create Services');
+ });
+
+ describe('when Orchestrator is available', () => {
+ const serviceName = 'mds.test';
+
+ it('should create an mds service', () => {
+ createService('mds', serviceName, '1');
+ });
+
+ it('should edit a service', () => {
+ const daemonCount = '2';
+ createClusterServicePage.editService(serviceName, daemonCount);
+ createClusterServicePage.expectPlacementCount(serviceName, daemonCount);
+ });
+
+ it('should delete mds service', () => {
+ createClusterServicePage.deleteService('mds.test');
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts
new file mode 100644
index 000000000..a82be9855
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts
@@ -0,0 +1,41 @@
+/* tslint:disable*/
+import { CreateClusterWizardHelper } from '../../cluster/create-cluster.po';
+import { OSDsPageHelper } from '../../cluster/osds.po';
+/* tslint:enable*/
+
+const osds = new OSDsPageHelper();
+
+describe('Create cluster create osds page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('.nav-link').contains('Create OSDs').click();
+ });
+
+ it('should check if title contains Create OSDs', () => {
+ cy.get('.title').should('contain.text', 'Create OSDs');
+ });
+
+ describe('when Orchestrator is available', () => {
+ it('should create OSDs', () => {
+ const hostnames = ['ceph-node-00', 'ceph-node-01', 'ceph-node-02'];
+ for (const hostname of hostnames) {
+ osds.create('hdd', hostname, true);
+
+ // Go to the Review section and Expand the cluster
+ // because the drive group spec is only stored
+ // in frontend and will be lost when refreshed
+ cy.get('.nav-link').contains('Review').click();
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('cd-dashboard').should('exist');
+ createCluster.navigateTo();
+ createCluster.createCluster();
+ cy.get('.nav-link').contains('Create OSDs').click();
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts
new file mode 100644
index 000000000..f93ad7a97
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts
@@ -0,0 +1,67 @@
+/* tslint:disable*/
+import {
+ CreateClusterHostPageHelper,
+ CreateClusterWizardHelper
+} from '../../cluster/create-cluster.po';
+/* tslint:enable*/
+
+describe('Create Cluster Review page', () => {
+ const createCluster = new CreateClusterWizardHelper();
+ const createClusterHostPage = new CreateClusterHostPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ createCluster.navigateTo();
+ createCluster.createCluster();
+
+ cy.get('.nav-link').contains('Review').click();
+ });
+
+ describe('navigation link test', () => {
+ it('should check if active nav-link is of Review section', () => {
+ cy.get('.nav-link.active').should('contain.text', 'Review');
+ });
+ });
+
+ describe('fields check', () => {
+ it('should check cluster resources table is present', () => {
+ // check for table header 'Cluster Resources'
+ createCluster.getLegends().its(0).should('have.text', 'Cluster Resources');
+
+ // check for fields in table
+ createCluster.getStatusTables().should('contain.text', 'Hosts');
+ createCluster.getStatusTables().should('contain.text', 'Storage Capacity');
+ createCluster.getStatusTables().should('contain.text', 'CPUs');
+ createCluster.getStatusTables().should('contain.text', 'Memory');
+ });
+
+ it('should check Host Details table is present', () => {
+ // check for there to be two tables
+ createCluster.getDataTables().should('have.length', 1);
+
+ // verify correct columns on Host Details table
+ createCluster.getDataTableHeaders(0).contains('Hostname');
+
+ createCluster.getDataTableHeaders(0).contains('Labels');
+
+ createCluster.getDataTableHeaders(0).contains('CPUs');
+
+ createCluster.getDataTableHeaders(0).contains('Cores');
+
+ createCluster.getDataTableHeaders(0).contains('Total Memory');
+
+ createCluster.getDataTableHeaders(0).contains('Raw Capacity');
+
+ createCluster.getDataTableHeaders(0).contains('HDDs');
+
+ createCluster.getDataTableHeaders(0).contains('Flash');
+
+ createCluster.getDataTableHeaders(0).contains('NICs');
+ });
+
+ it('should check default host name is present', () => {
+ createClusterHostPage.check_for_host();
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/06-cluster-check.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/06-cluster-check.e2e-spec.ts
new file mode 100644
index 000000000..589cbaa90
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/06-cluster-check.e2e-spec.ts
@@ -0,0 +1,99 @@
+/* tslint:disable*/
+import { Input, ManagerModulesPageHelper } from '../../cluster/mgr-modules.po';
+import { CreateClusterWizardHelper } from '../../cluster/create-cluster.po';
+import { HostsPageHelper } from '../../cluster/hosts.po';
+import { ServicesPageHelper } from '../../cluster/services.po';
+/* tslint:enable*/
+
+describe('when cluster creation is completed', () => {
+ const createCluster = new CreateClusterWizardHelper();
+ const services = new ServicesPageHelper();
+ const hosts = new HostsPageHelper();
+ const mgrmodules = new ManagerModulesPageHelper();
+
+ const hostnames = ['ceph-node-00', 'ceph-node-01', 'ceph-node-02', 'ceph-node-03'];
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ });
+
+ it('should redirect to dashboard landing page after cluster creation', () => {
+ createCluster.navigateTo();
+ createCluster.createCluster();
+
+ cy.get('.nav-link').contains('Review').click();
+ cy.get('button[aria-label="Next"]').click();
+ cy.get('cd-dashboard').should('exist');
+ });
+
+ describe('Hosts page', () => {
+ beforeEach(() => {
+ hosts.navigateTo();
+ });
+
+ it('should check if monitoring stacks are running on the root host', () => {
+ const monitoringStack = ['alertmanager', 'grafana', 'node-exporter', 'prometheus'];
+ hosts.clickTab('cd-host-details', 'ceph-node-00', 'Daemons');
+ for (const daemon of monitoringStack) {
+ cy.get('cd-host-details').within(() => {
+ services.checkServiceStatus(daemon);
+ });
+ }
+ });
+
+ // avoid creating node-exporter on the newly added host
+ // to favour the host draining process
+ it('should reduce the count for node-exporter', () => {
+ services.editService('node-exporter', '3');
+ });
+
+ // grafana ip address is set to the fqdn by default.
+ // kcli is not working with that, so setting the IP manually.
+ it('should change ip address of grafana', { retries: 2 }, () => {
+ const dashboardArr: Input[] = [
+ {
+ id: 'GRAFANA_API_URL',
+ newValue: 'https://192.168.100.100:3000',
+ oldValue: ''
+ }
+ ];
+ mgrmodules.editMgrModule('dashboard', dashboardArr);
+ });
+
+ it('should add one more host', () => {
+ hosts.navigateTo('add');
+ hosts.add(hostnames[3]);
+ hosts.checkExist(hostnames[3], true);
+ });
+
+ it('should have removed "_no_schedule" label', () => {
+ for (const hostname of hostnames) {
+ hosts.checkLabelExists(hostname, ['_no_schedule'], false);
+ }
+ });
+
+ it('should display inventory', () => {
+ hosts.clickTab('cd-host-details', hostnames[1], 'Physical Disks');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ });
+
+ it('should display daemons', () => {
+ hosts.clickTab('cd-host-details', hostnames[1], 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ hosts.getTableCount('total').should('be.gte', 0);
+ });
+ });
+
+ it('should check if mon daemon is running on all hosts', () => {
+ for (const hostname of hostnames) {
+ hosts.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ services.checkServiceStatus('mon');
+ });
+ }
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/07-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/07-osds.e2e-spec.ts
new file mode 100644
index 000000000..a0a1dd032
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/07-osds.e2e-spec.ts
@@ -0,0 +1,24 @@
+/* tslint:disable*/
+import { OSDsPageHelper } from '../../cluster/osds.po';
+/* tslint:enable*/
+
+describe('OSDs page', () => {
+ const osds = new OSDsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ osds.navigateTo();
+ });
+
+ it('should check if atleast 3 osds are created', { retries: 3 }, () => {
+ // we have created a total of more than 3 osds throughout
+ // the whole tests so ensuring that atleast
+ // 3 osds are listed in the table. Since the OSD
+ // creation can take more time going with
+ // retry of 3
+ for (let id = 0; id < 3; id++) {
+ osds.checkStatus(id, ['in', 'up']);
+ }
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/08-hosts.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/08-hosts.e2e-spec.ts
new file mode 100644
index 000000000..374ecdb0c
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/08-hosts.e2e-spec.ts
@@ -0,0 +1,49 @@
+/* tslint:disable*/
+import { HostsPageHelper } from '../../cluster/hosts.po';
+import { ServicesPageHelper } from '../../cluster/services.po';
+/* tslint:enable*/
+
+describe('Host Page', () => {
+ const hosts = new HostsPageHelper();
+ const services = new ServicesPageHelper();
+
+ const hostnames = ['ceph-node-00', 'ceph-node-01', 'ceph-node-02', 'ceph-node-03'];
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ hosts.navigateTo();
+ });
+
+ // rgw is needed for testing the force maintenance
+ it('should create rgw services', () => {
+ services.navigateTo('create');
+ services.addService('rgw', false, '4');
+ services.checkExist('rgw.foo', true);
+ });
+
+ it('should check if rgw daemon is running on all hosts', () => {
+ for (const hostname of hostnames) {
+ hosts.clickTab('cd-host-details', hostname, 'Daemons');
+ cy.get('cd-host-details').within(() => {
+ services.checkServiceStatus('rgw');
+ });
+ }
+ });
+
+ it('should force maintenance and exit', () => {
+ hosts.maintenance(hostnames[3], true, true);
+ });
+
+ it('should drain, remove and add the host back', () => {
+ hosts.drain(hostnames[3]);
+ hosts.remove(hostnames[3]);
+ hosts.navigateTo('add');
+ hosts.add(hostnames[3]);
+ hosts.checkExist(hostnames[3], true);
+ });
+
+ it('should show the exact count of daemons', () => {
+ hosts.checkServiceInstancesExist(hostnames[0], ['mgr: 1', 'prometheus: 1']);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/09-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/09-services.e2e-spec.ts
new file mode 100644
index 000000000..ed9ffb989
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/09-services.e2e-spec.ts
@@ -0,0 +1,114 @@
+/* tslint:disable*/
+import { ServicesPageHelper } from '../../cluster/services.po';
+/* tslint:enable*/
+
+describe('Services page', () => {
+ const services = new ServicesPageHelper();
+ const mdsDaemonName = 'mds.test';
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ services.navigateTo();
+ });
+
+ it('should check if rgw service is created', () => {
+ services.checkExist('rgw.foo', true);
+ });
+
+ it('should create an mds service', () => {
+ services.navigateTo('create');
+ services.addService('mds', false);
+ services.checkExist(mdsDaemonName, true);
+
+ services.clickServiceTab(mdsDaemonName, 'Details');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus(mdsDaemonName);
+ });
+ });
+
+ it('should stop a daemon', () => {
+ services.clickServiceTab(mdsDaemonName, 'Details');
+ services.checkServiceStatus(mdsDaemonName);
+
+ services.daemonAction('mds', 'stop');
+ services.checkServiceStatus(mdsDaemonName, 'stopped');
+ });
+
+ it('should restart a daemon', () => {
+ services.checkExist(mdsDaemonName, true);
+ services.clickServiceTab(mdsDaemonName, 'Details');
+ services.daemonAction('mds', 'restart');
+ services.checkServiceStatus(mdsDaemonName, 'running');
+ });
+
+ it('should redeploy a daemon', () => {
+ services.checkExist(mdsDaemonName, true);
+ services.clickServiceTab(mdsDaemonName, 'Details');
+
+ services.daemonAction('mds', 'stop');
+ services.checkServiceStatus(mdsDaemonName, 'stopped');
+ services.daemonAction('mds', 'redeploy');
+ services.checkServiceStatus(mdsDaemonName, 'running');
+ });
+
+ it('should start a daemon', () => {
+ services.checkExist(mdsDaemonName, true);
+ services.clickServiceTab(mdsDaemonName, 'Details');
+
+ services.daemonAction('mds', 'stop');
+ services.checkServiceStatus(mdsDaemonName, 'stopped');
+ services.daemonAction('mds', 'start');
+ services.checkServiceStatus(mdsDaemonName, 'running');
+ });
+
+ it('should delete an mds service', () => {
+ services.deleteService(mdsDaemonName);
+ });
+
+ it('should create and delete snmp-gateway service with version V2c', () => {
+ services.navigateTo('create');
+ services.addService('snmp-gateway', false, '1', 'V2c');
+ services.checkExist('snmp-gateway', true);
+
+ services.clickServiceTab('snmp-gateway', 'Details');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('snmp-gateway');
+ });
+
+ services.deleteService('snmp-gateway');
+ });
+
+ it('should create and delete snmp-gateway service with version V3', () => {
+ services.navigateTo('create');
+ services.addService('snmp-gateway', false, '1', 'V3', true);
+ services.checkExist('snmp-gateway', true);
+
+ services.clickServiceTab('snmp-gateway', 'Details');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('snmp-gateway');
+ });
+
+ services.deleteService('snmp-gateway');
+ });
+
+ it('should create and delete snmp-gateway service with version V3 and w/o privacy protocol', () => {
+ services.navigateTo('create');
+ services.addService('snmp-gateway', false, '1', 'V3', false);
+ services.checkExist('snmp-gateway', true);
+
+ services.clickServiceTab('snmp-gateway', 'Details');
+ cy.get('cd-service-details').within(() => {
+ services.checkServiceStatus('snmp-gateway');
+ });
+
+ services.deleteService('snmp-gateway');
+ });
+
+ it('should create ingress as unmanaged', () => {
+ services.navigateTo('create');
+ services.addService('ingress', false, undefined, undefined, undefined, true);
+ services.checkExist('ingress.rgw.foo', true);
+ services.isUnmanaged('ingress.rgw.foo', true);
+ services.deleteService('ingress.rgw.foo');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/10-nfs-exports.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/10-nfs-exports.e2e-spec.ts
new file mode 100644
index 000000000..f4b5499f0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/10-nfs-exports.e2e-spec.ts
@@ -0,0 +1,83 @@
+/* tslint:disable*/
+import { ServicesPageHelper } from '../../cluster/services.po';
+import { NFSPageHelper } from '../../orchestrator/workflow/nfs/nfs-export.po';
+import { BucketsPageHelper } from '../../rgw/buckets.po';
+/* tslint:enable*/
+
+describe('nfsExport page', () => {
+ const nfsExport = new NFSPageHelper();
+ const services = new ServicesPageHelper();
+ const buckets = new BucketsPageHelper();
+ const bucketName = 'e2e.nfs.bucket';
+ // @TODO: uncomment this when a CephFS volume can be created through Dashboard.
+ // const fsPseudo = '/fsPseudo';
+ const rgwPseudo = '/rgwPseudo';
+ const editPseudo = '/editPseudo';
+ const backends = ['CephFS', 'Object Gateway'];
+ const squash = 'no_root_squash';
+ const client: object = { addresses: '192.168.0.10' };
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ nfsExport.navigateTo();
+ });
+
+ describe('breadcrumb test', () => {
+ it('should open and show breadcrumb', () => {
+ nfsExport.expectBreadcrumbText('NFS');
+ });
+ });
+
+ describe('Create, edit and delete', () => {
+ it('should create an NFS cluster', () => {
+ services.navigateTo('create');
+
+ services.addService('nfs');
+
+ services.checkExist('nfs.testnfs', true);
+ services.getExpandCollapseElement().click();
+ services.checkServiceStatus('nfs');
+ });
+
+ it('should create a nfs-export with RGW backend', () => {
+ buckets.navigateTo('create');
+ buckets.create(bucketName, 'dashboard', 'default-placement');
+
+ nfsExport.navigateTo();
+ nfsExport.existTableCell(rgwPseudo, false);
+ nfsExport.navigateTo('create');
+ nfsExport.create(backends[1], squash, client, rgwPseudo, bucketName);
+ nfsExport.existTableCell(rgwPseudo);
+ });
+
+ // @TODO: uncomment this when a CephFS volume can be created through Dashboard.
+ // it('should create a nfs-export with CephFS backend', () => {
+ // nfsExport.navigateTo();
+ // nfsExport.existTableCell(fsPseudo, false);
+ // nfsExport.navigateTo('create');
+ // nfsExport.create(backends[0], squash, client, fsPseudo);
+ // nfsExport.existTableCell(fsPseudo);
+ // });
+
+ it('should show Clients', () => {
+ nfsExport.clickTab('cd-nfs-details', rgwPseudo, 'Clients (1)');
+ cy.get('cd-nfs-details').within(() => {
+ nfsExport.getTableCount('total').should('be.gte', 0);
+ });
+ });
+
+ it('should edit an export', () => {
+ nfsExport.editExport(rgwPseudo, editPseudo);
+
+ nfsExport.existTableCell(editPseudo);
+ });
+
+ it('should delete exports and bucket', () => {
+ nfsExport.delete(editPseudo);
+
+ buckets.navigateTo();
+ buckets.delete(bucketName);
+ });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/nfs/nfs-export.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/nfs/nfs-export.po.ts
new file mode 100644
index 000000000..c700ef058
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/workflow/nfs/nfs-export.po.ts
@@ -0,0 +1,52 @@
+/* tslint:disable*/
+import { PageHelper } from '../../../page-helper.po';
+/* tslint:enable*/
+
+const pages = {
+ index: { url: '#/nfs', id: 'cd-nfs-list' },
+ create: { url: '#/nfs/create', id: 'cd-nfs-form' }
+};
+
+export class NFSPageHelper extends PageHelper {
+ pages = pages;
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(backend: string, squash: string, client: object, pseudo: string, rgwPath?: string) {
+ this.selectOption('cluster_id', 'testnfs');
+ // select a storage backend
+ this.selectOption('name', backend);
+ if (backend === 'CephFS') {
+ this.selectOption('fs_name', 'myfs');
+
+ cy.get('#security_label').click({ force: true });
+ } else {
+ cy.get('input[data-testid=rgw_path]').type(rgwPath);
+ }
+
+ cy.get('input[name=pseudo]').type(pseudo);
+ this.selectOption('squash', squash);
+
+ // Add clients
+ cy.get('button[name=add_client]').click({ force: true });
+ cy.get('input[name=addresses]').type(client['addresses']);
+
+ // Check if we can remove clients and add it again
+ cy.get('span[name=remove_client]').click({ force: true });
+ cy.get('button[name=add_client]').click({ force: true });
+ cy.get('input[name=addresses]').type(client['addresses']);
+
+ cy.get('cd-submit-button').click();
+ }
+
+ editExport(pseudo: string, editPseudo: string) {
+ this.navigateEdit(pseudo);
+
+ cy.get('input[name=pseudo]').clear().type(editPseudo);
+
+ cy.get('cd-submit-button').click();
+
+ // Click the export and check its details table for updated content
+ this.getExpandCollapseElement(editPseudo).click();
+ cy.get('.active.tab-pane').should('contain.text', editPseudo);
+ }
+}
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
index 000000000..4531a70bb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts
@@ -0,0 +1,309 @@
+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);
+ });
+ }
+
+ /**
+ * Navigates to the edit page
+ */
+ navigateEdit(name: string, select = true, breadcrumb = true) {
+ if (select) {
+ this.navigateTo();
+ this.getFirstTableCell(name).click();
+ }
+ cy.contains('Creating...').should('not.exist');
+ cy.contains('button', 'Edit').click();
+ if (breadcrumb) {
+ this.expectBreadcrumbText('Edit');
+ }
+ }
+
+ /**
+ * Checks the active breadcrumb value.
+ */
+ expectBreadcrumbText(text: string) {
+ cy.get('.breadcrumb-item.active').should('have.text', text);
+ }
+
+ getTabs() {
+ return cy.get('.nav.nav-tabs li');
+ }
+
+ getTab(tabName: string) {
+ return cy.contains('.nav.nav-tabs li', tabName);
+ }
+
+ getTabText(index: number) {
+ return this.getTabs().its(index).text();
+ }
+
+ getTabsCount(): any {
+ return this.getTabs().its('length');
+ }
+
+ /**
+ * Helper method to navigate/click a tab inside the expanded table row.
+ * @param selector The selector of the expanded table row.
+ * @param name The name of the row which should expand.
+ * @param tabName Name of the tab to be navigated/clicked.
+ */
+ clickTab(selector: string, name: string, tabName: string) {
+ this.getExpandCollapseElement(name).click();
+ cy.get(selector).within(() => {
+ this.getTab(tabName).click();
+ });
+ }
+
+ /**
+ * 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');
+ }
+
+ private getTableCountSpan(spanType: 'selected' | 'found' | 'total') {
+ return cy.contains('.datatable-footer-inner .page-count span', spanType);
+ }
+
+ // Get 'selected', 'found', or 'total' row count of a table.
+ getTableCount(spanType: 'selected' | 'found' | 'total') {
+ this.waitDataTableToLoad();
+ return this.getTableCountSpan(spanType).then(($elem) => {
+ const text = $elem
+ .filter((_i, e) => e.innerText.includes(spanType))
+ .first()
+ .text();
+
+ return Number(text.match(/(\d+)\s+\w*/)[1]);
+ });
+ }
+
+ // Wait until selected', 'found', or 'total' row count of a table equal to a number.
+ expectTableCount(spanType: 'selected' | 'found' | 'total', count: number) {
+ this.waitDataTableToLoad();
+ this.getTableCountSpan(spanType).should(($elem) => {
+ const text = $elem.first().text();
+ expect(Number(text.match(/(\d+)\s+\w*/)[1])).to.equal(count);
+ });
+ }
+
+ getTableRow(content: string) {
+ this.waitDataTableToLoad();
+
+ this.searchTable(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.searchTable(content);
+ return cy.contains('.datatable-body-cell-label', content);
+ } else {
+ return cy.get('.datatable-body-cell-label').first();
+ }
+ }
+
+ getTableCell(columnIndex: number, exactContent: string, partialMatch = false) {
+ this.waitDataTableToLoad();
+ this.clearTableSearchInput();
+ this.searchTable(exactContent);
+ if (partialMatch) {
+ return cy.contains(
+ `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
+ exactContent
+ );
+ }
+ return cy.contains(
+ `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
+ new RegExp(`^${exactContent}$`)
+ );
+ }
+
+ existTableCell(name: string, oughtToBePresent = true) {
+ const waitRule = oughtToBePresent ? 'be.visible' : 'not.exist';
+ this.getFirstTableCell(name).should(waitRule);
+ }
+
+ 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');
+ }
+
+ /**
+ * Grabs striped tables
+ */
+ getStatusTables() {
+ return cy.get('.table.table-striped');
+ }
+
+ filterTable(name: string, option: string) {
+ this.waitDataTableToLoad();
+
+ cy.get('.tc_filter_name > button').click();
+ cy.contains(`.tc_filter_name .dropdown-item`, name).click();
+
+ cy.get('.tc_filter_option > button').click();
+ cy.contains(`.tc_filter_option .dropdown-item`, option).click();
+ }
+
+ setPageSize(size: string) {
+ cy.get('cd-table .dataTables_paginate input').first().clear({ force: true }).type(size);
+ }
+
+ searchTable(text: string) {
+ this.waitDataTableToLoad();
+
+ this.setPageSize('10');
+ cy.get('cd-table .search input').first().clear().type(text);
+ }
+
+ clearTableSearchInput() {
+ this.waitDataTableToLoad();
+
+ return cy.get('cd-table .search button').first().click();
+ }
+
+ // Click the action button
+ clickActionButton(action: string) {
+ cy.get('.table-actions button.dropdown-toggle').first().click(); // open submenu
+ cy.get(`button.${action}`).click(); // click on "action" menu item
+ }
+
+ /**
+ * 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.
+ * @param name The string to search in table cells.
+ * @param columnIndex If provided, search string in columnIndex column.
+ */
+ delete(name: string, columnIndex?: number, section?: string) {
+ // Selects row
+ const getRow = columnIndex
+ ? this.getTableCell.bind(this, columnIndex)
+ : this.getFirstTableCell.bind(this);
+ getRow(name).click();
+ let action: string;
+ section === 'hosts' ? (action = 'remove') : (action = 'delete');
+
+ // Clicks on table Delete/Remove button
+ this.clickActionButton(action);
+
+ // Convert action to SentenceCase and Confirms deletion
+ const actionUpperCase = action.charAt(0).toUpperCase() + action.slice(1);
+ cy.get('cd-modal .custom-control-label').click();
+ cy.contains('cd-modal button', actionUpperCase).click();
+
+ // Wait for modal to close
+ cy.get('cd-modal').should('not.exist');
+
+ // Waits for item to be removed from table
+ getRow(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
index 000000000..b4c3c75ac
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.e2e-spec.ts
@@ -0,0 +1,54 @@
+import { PoolPageHelper } from './pools.po';
+
+describe('Pools page', () => {
+ const pools = new PoolPageHelper();
+ const poolName = 'pool_e2e_pool-test';
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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.existTableCell(poolName, false);
+ pools.navigateTo('create');
+ pools.create(poolName, 8, 'rbd');
+ pools.existTableCell(poolName);
+ });
+
+ it('should edit a pools placement group', () => {
+ pools.existTableCell(poolName);
+ pools.edit_pool_pg(poolName, 32);
+ });
+
+ it('should show updated configuration field values', () => {
+ pools.existTableCell(poolName);
+ const bpsLimit = '4 B/s';
+ pools.edit_pool_configuration(poolName, bpsLimit);
+ });
+
+ 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
index 000000000..98cee470e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.po.ts
@@ -0,0 +1,70 @@
+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.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.navigateEdit(name);
+
+ 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);
+ }
+ }
+
+ edit_pool_configuration(name: string, bpsLimit: string) {
+ this.navigateEdit(name);
+
+ cy.get('.collapsible').click();
+ cy.get('cd-rbd-configuration-form')
+ .get('input[name=rbd_qos_bps_limit]')
+ .clear()
+ .type(`${bpsLimit}`);
+ cy.get('cd-submit-button').click();
+
+ this.navigateEdit(name);
+
+ cy.get('.collapsible').click();
+ cy.get('cd-rbd-configuration-form')
+ .get('input[name=rbd_qos_bps_limit]')
+ .should('have.value', bpsLimit);
+ }
+
+ private setApplications(apps: string[]) {
+ if (!apps || apps.length === 0) {
+ return;
+ }
+ cy.get('.float-left.mr-2.select-menu-edit').click();
+ cy.get('.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
index 000000000..e5ffdeee9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.e2e-spec.ts
@@ -0,0 +1,62 @@
+import { BucketsPageHelper } from './buckets.po';
+
+describe('RGW buckets page', () => {
+ const buckets = new BucketsPageHelper();
+ const bucket_name = 'e2ebucket';
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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, BucketsPageHelper.USERS[0], 'default-placement');
+ buckets.getFirstTableCell(bucket_name).should('exist');
+ });
+
+ it('should edit bucket', () => {
+ buckets.edit(bucket_name, BucketsPageHelper.USERS[1]);
+ buckets.getDataTables().should('contain.text', BucketsPageHelper.USERS[1]);
+ });
+
+ it('should delete bucket', () => {
+ buckets.delete(bucket_name);
+ });
+
+ it('should create bucket with object locking enabled', () => {
+ buckets.navigateTo('create');
+ buckets.create(bucket_name, BucketsPageHelper.USERS[0], 'default-placement', true);
+ buckets.getFirstTableCell(bucket_name).should('exist');
+ });
+
+ it('should not allow to edit versioning if object locking is enabled', () => {
+ buckets.edit(bucket_name, BucketsPageHelper.USERS[1], true);
+ buckets.getDataTables().should('contain.text', BucketsPageHelper.USERS[1]);
+
+ 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, BucketsPageHelper.USERS[0], '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
index 000000000..4804753d1
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.po.ts
@@ -0,0 +1,193 @@
+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 {
+ static readonly USERS = ['dashboard', 'testid'];
+
+ 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);
+ }
+
+ private selectLockMode(lockMode: string) {
+ return this.selectOption('lock_mode', lockMode);
+ }
+
+ @PageHelper.restrictTo(pages.create.url)
+ create(name: string, owner: string, placementTarget: string, isLocking = false) {
+ // 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');
+
+ if (isLocking) {
+ cy.get('#lock_enabled').click({ force: true });
+ // Select lock mode:
+ this.selectLockMode('Compliance');
+ cy.get('#lock_mode').should('have.class', 'ng-valid');
+ cy.get('#lock_retention_period_days').type('3');
+ }
+
+ // 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, isLocking = false) {
+ this.navigateEdit(name);
+
+ cy.get('input[name=placement-target]').should('have.value', 'default-placement');
+ this.selectOwner(new_owner);
+
+ // If object locking is enabled versioning shouldn't be visible
+ if (isLocking) {
+ cy.get('input[id=versioning]').should('be.disabled');
+ 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');
+
+ return cy.get('@versioningValueCell').should('have.text', this.versioningStateEnabled);
+ }
+ // 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:
+ this.navigateEdit(name);
+
+ 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',
+ 'Bucket names must be 3 to 63 characters long.'
+ );
+
+ // 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(BucketsPageHelper.USERS[1]);
+
+ // 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(BucketsPageHelper.USERS[1]);
+ // 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.navigateEdit(name);
+
+ 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
index 000000000..4cad786c6
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.e2e-spec.ts
@@ -0,0 +1,35 @@
+import { DaemonsPageHelper } from './daemons.po';
+
+describe('RGW daemons page', () => {
+ const daemons = new DaemonsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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
index 000000000..82a179463
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.po.ts
@@ -0,0 +1,34 @@
+import { PageHelper } from '../page-helper.po';
+
+export class DaemonsPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/rgw/daemon', id: 'cd-rgw-daemon-list' }
+ };
+
+ getTableCell() {
+ return cy
+ .get('.tab-content')
+ .its(1)
+ .find('cd-table')
+ .should('have.length', 1) // Only 1 table should be renderer
+ .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().should('be.visible').should('contain.text', 'ceph_version');
+
+ // 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().should('be.visible').should('contain.text', 'objecter.op_r');
+
+ // click on performance details tab
+ cy.contains('.nav-link', 'Performance Details').click();
+ }
+}
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
index 000000000..b5f366a09
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.e2e-spec.ts
@@ -0,0 +1,46 @@
+import { UsersPageHelper } from './users.po';
+
+describe('RGW users page', () => {
+ const users = new UsersPageHelper();
+ const tenant = 'e2e_000tenant';
+ const user_id = 'e2e_000user_create_edit_delete';
+ const user_name = tenant + '$' + user_id;
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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(tenant, user_id, 'Some Name', 'original@website.com', '1200');
+ users.getFirstTableCell(user_id).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
index 000000000..a4266f989
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.po.ts
@@ -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(tenant: string, user_id: string, fullname: string, email: string, maxbuckets: string) {
+ // Enter in user_id
+ cy.get('#user_id').type(user_id);
+ // Show Tenanat
+ cy.get('#show_tenant').click({ force: true });
+ // Enter in tenant
+ cy.get('#tenant').type(tenant);
+ // 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').should('exist').should('have.value', '1000');
+ 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(tenant + '$' + user_id).should('exist');
+ }
+
+ @PageHelper.restrictTo(pages.index.url)
+ edit(name: string, new_fullname: string, new_email: string, new_maxbuckets: string) {
+ this.navigateEdit(name);
+
+ // 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 tenant = '000invalid_tenant';
+ 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(tenant, uname, 'xxx', 'xxx@xxx', '1');
+
+ this.navigateTo('create');
+
+ // Username
+ cy.get('#user_id')
+ // 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);
+ cy.get('#show_tenant').click({ force: true });
+ cy.get('#tenant').type(tenant).should('have.class', 'ng-invalid');
+ cy.contains('#tenant + .invalid-feedback', 'The chosen user ID exists in this tenant.');
+
+ // check that username field is marked invalid if username has been cleared off
+ cy.get('#user_id').clear().blur().should('have.class', 'ng-invalid');
+ cy.contains('#user_id + .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(tenant + '$' + uname);
+ }
+
+ invalidEdit() {
+ const tenant = '000invalid_tenant';
+ const uname = '000invalid_edit_user';
+ // creating this user to edit for the test
+ this.navigateTo('create');
+ this.create(tenant, uname, 'xxx', 'xxx@xxx', '50');
+ const name = tenant + '$' + uname;
+ this.navigateEdit(name);
+
+ // 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.selectOption('max_buckets_mode', 'Disabled');
+ cy.get('#max_buckets').should('not.exist');
+ this.selectOption('max_buckets_mode', 'Custom');
+ cy.get('#max_buckets').should('exist').should('have.value', '50');
+ 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(tenant + '$' + uname);
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/api-docs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/api-docs.e2e-spec.ts
new file mode 100644
index 000000000..52994859e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/api-docs.e2e-spec.ts
@@ -0,0 +1,15 @@
+import { ApiDocsPageHelper } from '../ui/api-docs.po';
+
+describe('Api Docs Page', () => {
+ const apiDocs = new ApiDocsPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ apiDocs.navigateTo();
+ });
+
+ it('should show the API Docs description', () => {
+ cy.get('.renderedMarkdown').first().contains('This is the official Ceph REST API');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/api-docs.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/api-docs.po.ts
new file mode 100644
index 000000000..c7a8d222d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/api-docs.po.ts
@@ -0,0 +1,5 @@
+import { PageHelper } from '../page-helper.po';
+
+export class ApiDocsPageHelper extends PageHelper {
+ pages = { index: { url: '#/api-docs', id: 'cd-api-docs' } };
+}
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
index 000000000..9cb84480b
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.e2e-spec.ts
@@ -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();
+ Cypress.Cookies.preserveOnce('token');
+ 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',
+ 'Hosts',
+ 'Monitors',
+ 'OSDs',
+ 'Managers',
+ 'Object Gateways',
+ 'Metadata Servers',
+ 'iSCSI Gateways',
+ 'Raw Capacity',
+ 'Objects',
+ 'PG Status',
+ 'Pools',
+ 'PGs per OSD',
+ 'Client Read/Write',
+ 'Client Throughput',
+ 'Recovery Throughput',
+ 'Scrubbing'
+ ];
+
+ 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', 'Capacity');
+ dashboard.infoGroupTitle(2).should('eq', 'Performance');
+ });
+ });
+
+ 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.getTableCount('total').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
index 000000000..42d63ef44
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.po.ts
@@ -0,0 +1,31 @@
+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();
+ }
+
+ infoCardBody(infoCard: string) {
+ return this.infoCard(infoCard).find('.card-text');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/language.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/language.e2e-spec.ts
new file mode 100644
index 000000000..ccf16c2b5
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/language.e2e-spec.ts
@@ -0,0 +1,20 @@
+import { LanguagePageHelper } from './language.po';
+
+describe('Shared pages', () => {
+ const language = new LanguagePageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ language.navigateTo();
+ });
+
+ it('should check default language', () => {
+ language.getLanguageBtn().should('contain.text', 'English');
+ });
+
+ it('should check all available languages', () => {
+ language.getLanguageBtn().click();
+ language.getAllLanguages().should('have.length', 1).should('contain.text', 'English');
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/language.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/language.po.ts
new file mode 100644
index 000000000..80e21ba1e
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/language.po.ts
@@ -0,0 +1,15 @@
+import { PageHelper } from '../page-helper.po';
+
+export class LanguagePageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/dashboard', id: 'cd-dashboard' }
+ };
+
+ getLanguageBtn() {
+ return cy.get('cd-language-selector a').first();
+ }
+
+ getAllLanguages() {
+ return cy.get('cd-language-selector button');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/login.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/login.e2e-spec.ts
new file mode 100644
index 000000000..29c9e9e10
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/login.e2e-spec.ts
@@ -0,0 +1,17 @@
+import { LoginPageHelper } from './login.po';
+
+describe('Login page', () => {
+ const login = new LoginPageHelper();
+
+ it('should login and navigate to dashboard page', () => {
+ login.navigateTo();
+ login.doLogin();
+ });
+
+ it('should logout when clicking the button', () => {
+ login.navigateTo();
+ login.doLogin();
+
+ login.doLogout();
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/login.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/login.po.ts
new file mode 100644
index 000000000..d4d2c6921
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/login.po.ts
@@ -0,0 +1,22 @@
+import { PageHelper } from '../page-helper.po';
+
+export class LoginPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/login', id: 'cd-login' },
+ dashboard: { url: '#/dashboard', id: 'cd-dashboard' }
+ };
+
+ doLogin() {
+ cy.get('[name=username]').type('admin');
+ cy.get('#password').type('admin');
+ cy.get('[type=submit]').click();
+ cy.get('cd-dashboard').should('exist');
+ }
+
+ doLogout() {
+ cy.get('cd-identity a').click();
+ cy.contains('cd-identity span', 'Sign out').click();
+ cy.get('cd-login').should('exist');
+ cy.location('hash').should('eq', '#/login');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.e2e-spec.ts
new file mode 100644
index 000000000..fee2d2db9
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.e2e-spec.ts
@@ -0,0 +1,24 @@
+import { NavigationPageHelper } from './navigation.po';
+
+describe('Shared pages', () => {
+ const shared = new NavigationPageHelper();
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ shared.navigateTo();
+ });
+
+ it('should display the vertical menu by default', () => {
+ shared.getVerticalMenu().should('not.have.class', 'active');
+ });
+
+ it('should hide the vertical menu', () => {
+ shared.getMenuToggler().click();
+ shared.getVerticalMenu().should('have.class', 'active');
+ });
+
+ it('should navigate to the correct page', () => {
+ shared.checkNavigations(shared.navigations);
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.po.ts
new file mode 100644
index 000000000..a7ecf3af0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.po.ts
@@ -0,0 +1,69 @@
+import { PageHelper } from '../page-helper.po';
+
+export class NavigationPageHelper extends PageHelper {
+ pages = {
+ index: { url: '#/dashboard', id: 'cd-dashboard' }
+ };
+
+ navigations = [
+ { menu: 'NFS', component: 'cd-error' },
+ {
+ menu: 'Object Gateway',
+ submenus: [
+ { menu: 'Daemons', component: 'cd-rgw-daemon-list' },
+ { menu: 'Users', component: 'cd-rgw-user-list' },
+ { menu: 'Buckets', component: 'cd-rgw-bucket-list' }
+ ]
+ },
+ { menu: 'Dashboard', component: 'cd-dashboard' },
+ {
+ menu: 'Cluster',
+ submenus: [
+ { menu: 'Hosts', component: 'cd-hosts' },
+ { menu: 'Physical Disks', component: 'cd-error' },
+ { menu: 'Monitors', component: 'cd-monitor' },
+ { menu: 'Services', component: 'cd-error' },
+ { menu: 'OSDs', component: 'cd-osd-list' },
+ { menu: 'Configuration', component: 'cd-configuration' },
+ { menu: 'CRUSH map', component: 'cd-crushmap' },
+ { menu: 'Manager Modules', component: 'cd-mgr-module-list' },
+ { menu: 'Logs', component: 'cd-logs' },
+ { menu: 'Monitoring', component: 'cd-prometheus-tabs' }
+ ]
+ },
+ { menu: 'Pools', component: 'cd-pool-list' },
+ {
+ menu: 'Block',
+ submenus: [
+ { menu: 'Images', component: 'cd-error' },
+ { menu: 'Mirroring', component: 'cd-mirroring' },
+ { menu: 'iSCSI', component: 'cd-iscsi' }
+ ]
+ },
+ { menu: 'File Systems', component: 'cd-cephfs-list' }
+ ];
+
+ getVerticalMenu() {
+ return cy.get('nav[id=sidebar]');
+ }
+
+ getMenuToggler() {
+ return cy.get('[aria-label="toggle sidebar visibility"]');
+ }
+
+ checkNavigations(navs: any) {
+ // The nfs-ganesha, RGW, and block/rbd status requests are mocked to ensure that this method runs in time
+ cy.intercept('/ui-api/nfs-ganesha/status', { fixture: 'nfs-ganesha-status.json' });
+ cy.intercept('/ui-api/rgw/status', { fixture: 'rgw-status.json' });
+ cy.intercept('/ui-api/block/rbd/status', { fixture: 'block-rbd-status.json' });
+
+ navs.forEach((nav: any) => {
+ cy.contains('.simplebar-content li.nav-item a', nav.menu).click();
+ if (nav.submenus) {
+ this.checkNavigations(nav.submenus);
+ } else {
+ cy.get(nav.component).should('exist');
+ }
+ });
+ }
+}
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
index 000000000..2ee73a706
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.e2e-spec.ts
@@ -0,0 +1,59 @@
+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();
+ Cypress.Cookies.preserveOnce('token');
+ pools.navigateTo('create');
+ pools.create(poolName, 8);
+ pools.edit_pool_pg(poolName, 4, false);
+ });
+
+ after(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ pools.navigateTo();
+ pools.delete(poolName);
+ });
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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
index 000000000..12c424e35
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.po.ts
@@ -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
index 000000000..c3f325dbb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.e2e-spec.ts
@@ -0,0 +1,37 @@
+import { RoleMgmtPageHelper } from './role-mgmt.po';
+
+describe('Role Management page', () => {
+ const roleMgmt = new RoleMgmtPageHelper();
+ const role_name = 'e2e_role_mgmt_role';
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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
index 000000000..1cc3630a4
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.po.ts
@@ -0,0 +1,40 @@
+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');
+ // Waits for data to load
+ cy.contains('grafana');
+
+ // 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.get('[data-cy=submitBtn]').click();
+ cy.get('.breadcrumb-item.active').should('not.have.text', 'Create');
+
+ this.getFirstTableCell(name).should('exist');
+ }
+
+ edit(name: string, description: string) {
+ this.navigateEdit(name);
+ // Waits for data to load
+ cy.contains('grafana');
+
+ // 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.get('[data-cy=submitBtn]').click();
+ cy.get('.breadcrumb-item.active').should('not.have.text', 'Edit');
+
+ 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
index 000000000..92dc77212
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.e2e-spec.ts
@@ -0,0 +1,37 @@
+import { UserMgmtPageHelper } from './user-mgmt.po';
+
+describe('User Management page', () => {
+ const userMgmt = new UserMgmtPageHelper();
+ const user_name = 'e2e_user_mgmt_user';
+
+ beforeEach(() => {
+ cy.login();
+ Cypress.Cookies.preserveOnce('token');
+ 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
index 000000000..fb2b79129
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.po.ts
@@ -0,0 +1,39 @@
+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.get('[data-cy=submitBtn]').click();
+ this.getFirstTableCell(username).should('exist');
+ }
+
+ edit(username: string, password: string, name: string, email: string) {
+ this.navigateEdit(username);
+
+ // 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.get('[data-cy=submitBtn]');
+ editButton.click();
+ this.getFirstTableCell(email).should('exist');
+ this.getFirstTableCell(name).should('exist');
+ }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/visualTests/dashboard.vrt-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/visualTests/dashboard.vrt-spec.ts
new file mode 100644
index 000000000..b83d16d3d
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/visualTests/dashboard.vrt-spec.ts
@@ -0,0 +1,22 @@
+import { LoginPageHelper } from '../ui/login.po';
+
+describe('Dashboard Landing Page', () => {
+ const login = new LoginPageHelper();
+
+ beforeEach(() => {
+ cy.eyesOpen({
+ testName: 'Dashboard Component'
+ });
+ });
+
+ afterEach(() => {
+ cy.eyesClose();
+ });
+
+ it('should take screenshot of dashboard landing page', () => {
+ login.navigateTo();
+ login.doLogin();
+ cy.get('.card-text').should('be.visible');
+ cy.eyesCheckWindow({ tag: 'Dashboard landing page', ignore: { selector: '.card-text' } });
+ });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/visualTests/login.vrt-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/visualTests/login.vrt-spec.ts
new file mode 100644
index 000000000..ea74f1d0f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/visualTests/login.vrt-spec.ts
@@ -0,0 +1,19 @@
+describe('Login Page', () => {
+ beforeEach(() => {
+ cy.visit('#/login');
+ cy.eyesOpen({
+ appName: 'Ceph',
+ testName: 'Login Component Check'
+ });
+ });
+
+ afterEach(() => {
+ cy.eyesClose();
+ });
+
+ it('types login credentials and takes screenshot', () => {
+ cy.get('[name=username]').type('admin');
+ cy.get('#password').type('admin');
+ cy.eyesCheckWindow({ tag: 'Login Screen with credentials typed' });
+ });
+});