diff options
Diffstat (limited to '')
17 files changed, 1177 insertions, 0 deletions
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(); + }); + } +} |