diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:45:59 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:45:59 +0000 |
commit | 19fcec84d8d7d21e796c7624e521b60d28ee21ed (patch) | |
tree | 42d26aa27d1e3f7c0b8bd3fd14e7d7082f5008dc /src/pybind/mgr/dashboard/frontend/cypress/integration | |
parent | Initial commit. (diff) | |
download | ceph-6d07fdb6bb33b1af39833b850bb6cf8af79fe293.tar.xz ceph-6d07fdb6bb33b1af39833b850bb6cf8af79fe293.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 '')
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' }); + }); +}); |