diff --git a/projects/frontend/data-pipelines/gui/e2e/fixtures/e2e-cy-dp-failing.job.json b/projects/frontend/data-pipelines/gui/e2e/fixtures/e2e-cy-dp-failing.job.json new file mode 100644 index 0000000000..b1019e21ee --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/fixtures/e2e-cy-dp-failing.job.json @@ -0,0 +1,19 @@ +{ + "job_name": "e2e-cp-$env-placeholder$-failing-job", + "description": "Testing failing job, that is used in data-pipelines CI/CD for UI e2e tests. It is intended to be always available in order to speed up deployment and if deleted will be automatically re-created by the e2e tests.", + "config": { + "db_default_type": "IMPALA", + "contacts": { + "notified_on_job_failure_user_error": [], + "notified_on_job_failure_platform_error": [], + "notified_on_job_success": [], + "notified_on_job_deploy": [] + }, + "schedule": { + "schedule_cron": "0 */12 * * *" + }, + "generate_keytab": true, + "enable_execution_notifications": true + }, + "team": "taurus" +} diff --git a/projects/frontend/data-pipelines/gui/e2e/fixtures/e2e-cy-dp-failing.job.zip b/projects/frontend/data-pipelines/gui/e2e/fixtures/e2e-cy-dp-failing.job.zip new file mode 100644 index 0000000000..a6c08016ee Binary files /dev/null and b/projects/frontend/data-pipelines/gui/e2e/fixtures/e2e-cy-dp-failing.job.zip differ diff --git a/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/explore/additional-test-job.json b/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/explore/additional-test-job.json new file mode 100644 index 0000000000..198165b0ee --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/explore/additional-test-job.json @@ -0,0 +1,27 @@ +{ + "config": { + "contacts": { + "notified_on_job_deploy": [ + "auserov3@vmware.com" + ], + "notified_on_job_failure_platform_error": [ + "buserov3@vmware.com" + ], + "notified_on_job_failure_user_error": [ + "cuserov3@vmware.com" + ], + "notified_on_job_success": [ + "duserov3@vmware.com" + ] + }, + "db_default_type": "IMPALA", + "generate_keytab": false, + "schedule": { + "schedule_cron": "0 0 03 * 5" + } + }, + "description": "Test description 3 with a long text on the line", + "job_name": "e2e-cp-$env-placeholder$-a-test-job-3", + "status": "disabled", + "team": "e2e-cp-$env-placeholder$-a-test-team-3" +} diff --git a/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/explore/test-jobs.json b/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/explore/test-jobs.json new file mode 100644 index 0000000000..5ce9ba5d2b --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/explore/test-jobs.json @@ -0,0 +1,56 @@ +[ + { + "config": { + "contacts": { + "notified_on_job_deploy": [ + "auserov1@vmware.com" + ], + "notified_on_job_failure_platform_error": [ + "buserov1@vmware.com" + ], + "notified_on_job_failure_user_error": [ + "cuserov1@vmware.com" + ], + "notified_on_job_success": [ + "duserov1@vmware.com" + ] + }, + "db_default_type": "IMPALA", + "generate_keytab": false, + "schedule": { + "schedule_cron": "0 0 01 * 5" + } + }, + "description": "Test description 1 with a long text on the line", + "job_name": "e2e-cp-$env-placeholder$-a-test-job-1", + "status": "disabled", + "team": "e2e-cp-$env-placeholder$-a-test-team-1" + }, + { + "config": { + "contacts": { + "notified_on_job_deploy": [ + "auserov2@vmware.com" + ], + "notified_on_job_failure_platform_error": [ + "buserov2@vmware.com" + ], + "notified_on_job_failure_user_error": [ + "cuserov2@vmware.com" + ], + "notified_on_job_success": [ + "duserov2@vmware.com" + ] + }, + "db_default_type": "IMPALA", + "generate_keytab": false, + "schedule": { + "schedule_cron": "0 0 02 * 5" + } + }, + "description": "Test description 2 with a long text on the line", + "job_name": "e2e-cp-$env-placeholder$-a-test-job-2", + "status": "disabled", + "team": "e2e-cp-$env-placeholder$-a-test-team-2" + } +] diff --git a/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/manage/e2e-cypress-dp-test.json b/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/manage/e2e-cypress-dp-test.json new file mode 100644 index 0000000000..fd38f2664f --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/manage/e2e-cypress-dp-test.json @@ -0,0 +1,19 @@ +{ + "job_name": "e2e-cp-$env-placeholder$-test-job", + "description": "Testing job, that is used in data-pipelines CI/CD for UI e2e tests. It is intended to be always available in order to speed up deployment and if deleted will be automatically re-created by the e2e tests." , + "config": { + "db_default_type": "IMPALA" , + "contacts": { + "notified_on_job_failure_user_error": [] , + "notified_on_job_failure_platform_error": [] , + "notified_on_job_success": [] , + "notified_on_job_deploy": [] + } , + "schedule": { + "schedule_cron": "11 23 5 8 1" + } , + "generate_keytab": true , + "enable_execution_notifications": true + } , + "team": "taurus" +} diff --git a/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/manage/e2e-cypress-dp-test.zip b/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/manage/e2e-cypress-dp-test.zip new file mode 100644 index 0000000000..a772e15b56 Binary files /dev/null and b/projects/frontend/data-pipelines/gui/e2e/fixtures/lib/manage/e2e-cypress-dp-test.zip differ diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/app/app.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/app/app.spec.js new file mode 100644 index 0000000000..fa088eef37 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/integration/app/app.spec.js @@ -0,0 +1,38 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// + +import { AppPage } from '../../support/pages/app/app.po'; + +describe('App Page', { tags: ['@dataPipelines'] }, () => { + before(() => { + return AppPage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot('app')) + .then(() => AppPage.login()) + .then(() => cy.saveLocalStorage('app')); + }); + + after(() => { + AppPage.saveHarIfSupported(); + }); + + beforeEach(() => { + cy.restoreLocalStorage('app'); + }); + + it('App Page - Main Title Component have text: Data Pipelines', () => { + AppPage.navigateTo(); + + const page = AppPage.getPage(); + + page + .waitForInitialPageLoad(); + + page + .getMainTitle() + .should('have.text', 'Data Pipelines'); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/app/getting-started/getting-started.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/app/getting-started/getting-started.spec.js new file mode 100644 index 0000000000..6a3d354a5f --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/integration/app/getting-started/getting-started.spec.js @@ -0,0 +1,142 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// +import { DataPipelinesBasePO } from '../../../support/application/data-pipelines-base.po'; +import { GettingStartedPage } from '../../../support/pages/app/getting-started/getting-started.po'; +import { DataJobsHealthPanelComponentPO } from '../../../support/pages/app/getting-started/data-jobs-health-panel-component.po'; +import { DataJobManageDetailsPage } from '../../../support/pages/app/lib/manage/data-job-details.po'; +import { DataJobManageExecutionsPage } from '../../../support/pages/app/lib/manage/data-job-executions.po'; +import { applyGlobalEnvSettings } from '../../../support/helpers/commands.helpers'; + +describe('Getting Started Page', { tags: ['@dataPipelines'] }, () => { + let testJob; + + before(() => { + return DataPipelinesBasePO.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot('getting-started')) + .then(() => DataPipelinesBasePO.login()) + .then(() => cy.saveLocalStorage('getting-started')) + .then(() => cy.prepareLongLivedFailingTestJob()) + .then(() => cy.createExecutionsLongLivedFailingTestJob()) + .then(() => cy.fixture('e2e-cy-dp-failing.job.json')) + .then((failingTestJob) => { + testJob = applyGlobalEnvSettings(failingTestJob); + + return cy.wrap({ context: 'getting-started.spec::before()', action: 'continue' }); + }); + }); + + after(() => { + DataPipelinesBasePO.saveHarIfSupported(); + }); + + beforeEach(() => { + cy.restoreLocalStorage('getting-started'); + + DataPipelinesBasePO.initBackendRequestInterceptor(); + }); + + it('Main Title Component have text: Get Started with Data Pipelines', () => { + GettingStartedPage + .navigateTo() + .getMainTitle() + .should('have.text', 'Get Started with Data Pipelines'); + }); + + describe('Data Jobs Health Overview Panel', () => { + it('Verify Widgets rendered correct data and failing jobs navigates correctly', () => { + GettingStartedPage + .navigateTo(); + + let dataJobsHealthPanel = DataJobsHealthPanelComponentPO + .getComponent(); + dataJobsHealthPanel + .waitForViewToRender(); + dataJobsHealthPanel + .getDataJobsHealthPanel() + .scrollIntoView(); + + dataJobsHealthPanel + .getExecutionsSuccessPercentage() + .should('be.gte', 0) + .should('be.lte', 100); + dataJobsHealthPanel + .getNumberOfFailedExecutions() + .should('be.gte', 2); + dataJobsHealthPanel + .getExecutionsTotal() + .should('be.gte', 2); + + dataJobsHealthPanel + .getAllFailingJobs() + .should('have.length.gte', 1); + + dataJobsHealthPanel + .getAllMostRecentFailingJobsExecutions() + .should('have.length.gte', 1); + + // navigate to failing job details + dataJobsHealthPanel + .navigateToFailingJobDetails(testJob.job_name); + + const dataJobManageDetailsPage = DataJobManageDetailsPage + .getPage(); + dataJobManageDetailsPage + .getMainTitle() + .should('contain.text', testJob.job_name); + dataJobManageDetailsPage + .getDetailsTab() + .should('be.visible') + .should('have.class', 'active'); + dataJobManageDetailsPage + .getExecutionsTab() + .should('exist') + .should('not.have.class', 'active'); + dataJobManageDetailsPage + .showMoreDescription() + .getDescriptionFull() + .should('contain.text', testJob.description); + }); + + it('Verify most recent failing executions Widget navigates correctly', () => { + GettingStartedPage + .navigateTo(); + + let dataJobsHealthPanel = DataJobsHealthPanelComponentPO + .getComponent(); + dataJobsHealthPanel + .waitForViewToRender(); + dataJobsHealthPanel + .getDataJobsHealthPanel() + .scrollIntoView(); + + dataJobsHealthPanel + .getAllMostRecentFailingJobsExecutions() + .should('have.length.gte', 1); + + // navigate to most recent failing job executions + dataJobsHealthPanel + .navigateToMostRecentFailingJobExecutions(testJob.job_name); + + const dataJobManageExecutionsPage = DataJobManageExecutionsPage + .getPage(); + dataJobManageExecutionsPage + .getMainTitle() + .should('contain.text', testJob.job_name); + dataJobManageExecutionsPage + .getDetailsTab() + .should('be.visible') + .should('not.have.class', 'active'); + dataJobManageExecutionsPage + .getExecutionsTab() + .should('be.visible') + .should('have.class', 'active'); + dataJobManageExecutionsPage + .getDataGridRows() + .should('have.length.gte', 1); + }); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/lib/explore/data-job-details.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/lib/explore/data-job-details.spec.js new file mode 100644 index 0000000000..e876c43134 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/integration/lib/explore/data-job-details.spec.js @@ -0,0 +1,156 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// +import { DataJobsExplorePage } from '../../../support/pages/app/lib/explore/data-jobs.po'; +import { DataJobExploreDetailsPage } from '../../../support/pages/app/lib/explore/data-job-details.po'; +import { applyGlobalEnvSettings } from '../../../support/helpers/commands.helpers'; + +describe('Data Job Explore Details Page', { tags: ['@dataPipelines', '@exploreDataJobDetails'] }, () => { + let dataJobExploreDetailsPage; + let testJobs; + + before(() => { + return DataJobExploreDetailsPage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot('data-job-explore-details')) + .then(() => DataJobExploreDetailsPage.login()) + .then(() => cy.saveLocalStorage('data-job-explore-details')) + .then(() => cy.cleanTestJobs()) + .then(() => cy.prepareBaseTestJobs()) + .then(() => cy.fixture('lib/explore/test-jobs.json')) + .then((loadedTestJobs) => { + testJobs = applyGlobalEnvSettings(loadedTestJobs); + + return cy.wrap({ context: 'explore::data-job-details.spec::before()', action: 'continue' }); + }); + }); + + after(() => { + cy.cleanTestJobs(); + + DataJobExploreDetailsPage.saveHarIfSupported(); + }); + + beforeEach(() => { + cy.restoreLocalStorage('data-job-explore-details'); + + DataJobExploreDetailsPage.initBackendRequestInterceptor(); + }); + + it('Data Job Explore Details Page - should load and show job details', () => { + cy.log('Fixture for name: ' + testJobs[0].job_name); + + const dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + dataJobsExplorePage + .openJobDetails(testJobs[0].team, testJobs[0].job_name); + + dataJobExploreDetailsPage = DataJobExploreDetailsPage + .getPage(); + + dataJobExploreDetailsPage + .getDetailsTab() + .should('be.visible'); + + dataJobExploreDetailsPage + .getMainTitle() + .should('be.visible') + .should('contains.text', testJobs[0].job_name); + + dataJobExploreDetailsPage + .getStatusField() + .should('be.visible') + .should('have.text', 'Not Deployed'); + + dataJobExploreDetailsPage + .getDescriptionField() + .should('be.visible') + .should('contain.text', testJobs[0].description); + + dataJobExploreDetailsPage + .getTeamField() + .should('be.visible') + .should('have.text', testJobs[0].team); + + dataJobExploreDetailsPage + .getScheduleField() + .should('be.visible') + .should('contains.text', 'At 12:00 AM, on day 01 of the month, and on Friday'); + + // DISABLED Because there is no Source at the moment. + // dataJobExploreDetailsPage + // .getSourceField() + // .should('be.visible') + // .should('contains.text', 'http://host/data-jobs/' + testJobs[0].job_name) + // .should("have.attr", "href", 'http://host/data-jobs/' + testJobs[0].job_name); + + dataJobExploreDetailsPage + .getOnDeployedField() + .should('be.visible') + .should('contains.text', testJobs[0].config.contacts.notified_on_job_deploy); + + dataJobExploreDetailsPage + .getOnPlatformErrorField() + .should('be.visible') + .should('contains.text', testJobs[0].config.contacts.notified_on_job_failure_platform_error); + + dataJobExploreDetailsPage + .getOnUserErrorField() + .should('be.visible') + .should('contains.text', testJobs[0].config.contacts.notified_on_job_failure_user_error); + + dataJobExploreDetailsPage + .getOnSuccessField() + .should('be.visible') + .should('contains.text', testJobs[0].config.contacts.notified_on_job_success); + }); + + it('Data Job Explore Details Page - should verify Details tab is visible and active', () => { + cy.log('Fixture for name: ' + testJobs[0].job_name); + + const dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + dataJobsExplorePage + .openJobDetails(testJobs[0].team, testJobs[0].job_name); + + const dataJobExploreDetailsPage = DataJobExploreDetailsPage + .getPage(); + + dataJobExploreDetailsPage + .getMainTitle() + .should('be.visible') + .should('contains.text', testJobs[0].job_name); + + dataJobExploreDetailsPage + .getDetailsTab() + .should('be.visible') + .should('have.class', 'active'); + }); + + it('Data Job Explore Details Page - should verify Action buttons are not displayed', () => { + cy.log('Fixture for name: ' + testJobs[0].job_name); + + const dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + dataJobsExplorePage + .openJobDetails(testJobs[0].team, testJobs[0].job_name); + + const dataJobExploreDetailsPage = DataJobExploreDetailsPage + .getPage(); + + dataJobExploreDetailsPage + .getMainTitle() + .should('be.visible') + .should('contains.text', testJobs[0].job_name); + + dataJobExploreDetailsPage + .getExecuteNowButton() + .should('not.exist'); + + dataJobExploreDetailsPage + .getActionDropdownBtn() + .should('not.exist'); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/lib/explore/data-job-executions.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/lib/explore/data-job-executions.spec.js new file mode 100644 index 0000000000..b346dfb456 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/integration/lib/explore/data-job-executions.spec.js @@ -0,0 +1,84 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// + +import { DataJobsExplorePage } from '../../../support/pages/app/lib/explore/data-jobs.po'; +import { DataJobBasePO } from '../../../support/application/data-job-base.po'; +import { applyGlobalEnvSettings } from '../../../support/helpers/commands.helpers'; + +describe('Data Job Explore Executions Page', { tags: ['@dataPipelines', '@exploreDataJobExecutions'] }, () => { + let testJobs; + + before(() => { + return DataJobBasePO.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot('data-job-explore-executions')) + .then(() => DataJobBasePO.login()) + .then(() => cy.saveLocalStorage('data-job-explore-executions')) + .then(() => cy.cleanTestJobs()) + .then(() => cy.prepareBaseTestJobs()) + .then(() => cy.fixture('lib/explore/test-jobs.json')) + .then((loadedTestJobs) => { + testJobs = applyGlobalEnvSettings(loadedTestJobs); + + return cy.wrap({ context: 'explore::data-job-executions.spec::before()', action: 'continue' }); + }); + }); + + after(() => { + cy.cleanTestJobs(); + + DataJobBasePO.saveHarIfSupported(); + }); + + beforeEach(() => { + cy.restoreLocalStorage('data-job-explore-executions'); + + DataJobBasePO.initBackendRequestInterceptor(); + }); + + it(`Data Job Explore Executions Page - should open Details and verify Executions tab is not displayed`, () => { + cy.log('Fixture for name: ' + testJobs[0].job_name); + + const dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + dataJobsExplorePage + .openJobDetails(testJobs[0].team, testJobs[0].job_name); + + const dataJobBasePage = DataJobBasePO + .getPage(); + + dataJobBasePage + .getMainTitle() + .should('be.visible') + .should('contains.text', testJobs[0].job_name); + + dataJobBasePage + .getDetailsTab() + .should('have.class', 'active'); + + dataJobBasePage + .getExecutionsTab() + .should('not.exist'); + }); + + it('Data Job Explore Executions Page - should verify on URL navigate to Executions will redirect to Details', () => { + const dataJobBasePage = DataJobBasePO + .navigateToUrl(`/explore/data-jobs/${ testJobs[0].team }/${ testJobs[0].job_name }/executions`); + + dataJobBasePage + .getMainTitle() + .should('be.visible') + .should('contains.text', testJobs[0].job_name); + + dataJobBasePage + .getCurrentUrl() + .should('match', new RegExp(`\\/explore\\/data-jobs\\/${testJobs[0].team}\\/${testJobs[0].job_name}\\/details$`)); + + dataJobBasePage + .getDetailsTab() + .should('have.class', 'active'); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/lib/explore/data-jobs.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/lib/explore/data-jobs.spec.js new file mode 100644 index 0000000000..9877cef132 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/integration/lib/explore/data-jobs.spec.js @@ -0,0 +1,306 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// +import { DataJobsExplorePage } from '../../../support/pages/app/lib/explore/data-jobs.po'; +import { DataJobExploreDetailsPage } from '../../../support/pages/app/lib/explore/data-job-details.po'; +import { applyGlobalEnvSettings } from '../../../support/helpers/commands.helpers'; + +describe('Data Jobs Explore Page', { tags: ['@dataPipelines', '@exploreDataJobs'] }, () => { + let dataJobsExplorePage; + let testJobs; + + before(() => { + return DataJobsExplorePage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot('data-jobs-explore')) + .then(() => DataJobsExplorePage.login()) + .then(() => cy.saveLocalStorage('data-jobs-explore')) + .then(() => cy.cleanTestJobs()) + .then(() => cy.prepareBaseTestJobs()) + .then(() => cy.fixture('lib/explore/test-jobs.json')) + .then((loadedTestJobs) => { + testJobs = applyGlobalEnvSettings(loadedTestJobs); + + return cy.wrap({ context: 'explore::data-jobs.spec::before()', action: 'continue' }); + }); + }); + + after(() => { + cy.cleanTestJobs(); + + DataJobsExplorePage.saveHarIfSupported(); + }); + + beforeEach(() => { + cy.restoreLocalStorage('data-jobs-explore'); + + DataJobsExplorePage.initBackendRequestInterceptor(); + }); + + it('Main Title Component have text: Explore Data Jobs', () => { + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + dataJobsExplorePage + .getMainTitle() + .should('be.visible') + .should('have.text', 'Explore Data Jobs'); + }); + + it('Data Jobs Explore Page - loaded and shows data jobs', () => { + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + dataJobsExplorePage + .getDataGrid() + .should('be.visible'); + + testJobs.forEach((testJob) => { + cy.log('Fixture for name: ' + testJob.job_name); + + dataJobsExplorePage + .getDataGridCell(testJob.job_name) + .scrollIntoView() + .should('be.visible'); + + dataJobsExplorePage + .getDataGridCell(testJob.team) + .should('be.visible'); + }) + }); + + it('Data Jobs Explore Page - filters data jobs', () => { + cy.log('Fixture for name: ' + testJobs[0].job_name); + + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + dataJobsExplorePage + .filterByJobName(testJobs[0].job_name); + + dataJobsExplorePage + .getDataGridCell(testJobs[0].job_name) + .should('be.visible'); + + dataJobsExplorePage + .getDataGridCell(testJobs[1].job_name) + .should('not.exist'); + }); + + it('Data Jobs Explore Page - refreshes data jobs', () => { + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + cy.fixture('lib/explore/additional-test-job.json').then((additionalTestJob) => { + const normalizedTestJob = applyGlobalEnvSettings(additionalTestJob); + + cy.log('Fixture for name: ' + normalizedTestJob.job_name); + + dataJobsExplorePage + .getDataGridCell(testJobs[0].job_name) + .should('have.text', testJobs[0].job_name); + + dataJobsExplorePage + .getDataGridCell(normalizedTestJob.job_name) + .should('not.exist'); + + cy.prepareAdditionalTestJobs(); + + dataJobsExplorePage + .refreshDataGrid(); + + dataJobsExplorePage + .filterByJobName(normalizedTestJob.job_name); + + dataJobsExplorePage + .getDataGridCell(normalizedTestJob.job_name) + .should('have.text', normalizedTestJob.job_name); + }); + }); + + it('Data Jobs Explore Page - searches data jobs', () => { + cy.log('Fixture for name: ' + testJobs[0].job_name); + + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + dataJobsExplorePage + .getDataGridCell(testJobs[1].job_name) + .should('be.visible'); + + dataJobsExplorePage + .searchByJobName(testJobs[0].job_name); + + dataJobsExplorePage + .getDataGridCell(testJobs[0].job_name) + .should('be.visible'); + + dataJobsExplorePage + .getDataGridCell(testJobs[1].job_name) + .should('not.exist'); + }); + + it('Data Jobs Explore Page - searches data jobs, search parameter goes into URL', () => { + cy.log('Fixture for name: ' + testJobs[0].job_name); + + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + // verify 2 test rows visible + dataJobsExplorePage + .getDataGridCell(testJobs[0].job_name) + .should('be.visible'); + dataJobsExplorePage + .getDataGridCell(testJobs[1].job_name) + .should('be.visible'); + + // do search + dataJobsExplorePage + .searchByJobName(testJobs[0].job_name); + + // verify 1 test row visible + dataJobsExplorePage + .getDataGridCell(testJobs[0].job_name) + .should('be.visible'); + dataJobsExplorePage + .getDataGridCell(testJobs[1].job_name) + .should('not.exist'); + + // verify url contains search value + dataJobsExplorePage + .getCurrentUrl() + .should('match', new RegExp(`\\/explore\\/data-jobs\\?search=${ testJobs[0].job_name }$`)); + + // clear search with clear() method + dataJobsExplorePage + .clearSearchField(); + + // verify 2 test rows visible + dataJobsExplorePage + .getDataGridCell(testJobs[0].job_name) + .should('be.visible'); + dataJobsExplorePage + .getDataGridCell(testJobs[1].job_name) + .should('be.visible'); + + // verify url does not contain search value + dataJobsExplorePage + .getCurrentUrl() + .should('match', new RegExp(`\\/explore\\/data-jobs$`)); + }); + + it('Data Jobs Explore Page - searches data jobs, perform search when URL contains search parameter', () => { + cy.log('Fixture for name: ' + testJobs[1].job_name); + + // navigate with search value in URL + dataJobsExplorePage = DataJobsExplorePage.navigateToUrl(`/explore/data-jobs?search=${ testJobs[1].job_name }`); + + // verify url contains search value + dataJobsExplorePage + .getCurrentUrl() + .should('match', new RegExp(`\\/explore\\/data-jobs\\?search=${ testJobs[1].job_name }$`)); + + // verify 1 test row visible + dataJobsExplorePage + .getDataGridCell(testJobs[0].job_name) + .should('not.exist'); + dataJobsExplorePage + .getDataGridCell(testJobs[1].job_name) + .should('be.visible'); + + // clear search with button + dataJobsExplorePage + .clearSearchFieldWithButton(); + + // verify 2 test rows visible + dataJobsExplorePage + .getDataGridCell(testJobs[0].job_name) + .should('be.visible'); + dataJobsExplorePage + .getDataGridCell(testJobs[1].job_name) + .should('be.visible'); + + // verify url does not contain search value + dataJobsExplorePage + .getCurrentUrl() + .should('match', new RegExp(`\\/explore\\/data-jobs$`)); + }); + + it('Data Jobs Explore Page - show/hide column when toggling from menu', () => { + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + // show panel for show/hide columns + dataJobsExplorePage + .toggleColumnShowHidePanel(); + + // verify correct options are rendered + dataJobsExplorePage + .getDataGridColumnShowHideOptionsValues() + .should('have.length', 9) + .invoke('join', ',') + .should('eq', 'Description,Deployment Status,Last Execution Duration,Success rate,Next run (UTC),Last Deployed (UTC),Last Deployed By,Source,Logs'); + + // verify column is not checked in toggling menu + dataJobsExplorePage + .getDataGridColumnShowHideOption('Last Execution Duration') + .should('exist') + .should('not.be.checked'); + + // verify header cell for column is not rendered + dataJobsExplorePage + .getDataGridHeaderCell('Last Execution Duration') + .should('have.length', 0); + + // toggle column to render + dataJobsExplorePage + .checkColumnShowHideOption('Last Execution Duration'); + + // verify column is checked in toggling menu + dataJobsExplorePage + .getDataGridColumnShowHideOption('Last Execution Duration') + .should('exist') + .should('be.checked'); + + // verify header cell for column is rendered + dataJobsExplorePage + .getDataGridHeaderCell('Last Execution Duration') + .should('have.length', 1); + + // toggle column to hide + dataJobsExplorePage + .uncheckColumnShowHideOption('Last Execution Duration'); + + // verify column is not checked in toggling menu + dataJobsExplorePage + .getDataGridColumnShowHideOption('Last Execution Duration') + .should('exist') + .should('not.be.checked'); + + // verify header cell for column is not rendered + dataJobsExplorePage + .getDataGridHeaderCell('Last Execution Duration') + .should('have.length', 0); + + // hide panel for show/hide columns + dataJobsExplorePage + .toggleColumnShowHidePanel(); + }); + + it('Data Jobs Explore Page - navigates to data job', () => { + cy.log('Fixture for name: ' + testJobs[0].job_name); + + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + dataJobsExplorePage + .openJobDetails(testJobs[0].team, testJobs[0].job_name); + + const dataJobExploreDetailsPage = DataJobExploreDetailsPage + .getPage(); + + dataJobExploreDetailsPage + .getMainTitle() + .should('be.visible') + .should('contains.text', testJobs[0].job_name); + + dataJobExploreDetailsPage + .getDescriptionField() + .should('be.visible') + .should('contain.text', testJobs[0].description); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/lib/manage/data-job-details.int.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/lib/manage/data-job-details.int.spec.js new file mode 100644 index 0000000000..8af498d8b1 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/integration/lib/manage/data-job-details.int.spec.js @@ -0,0 +1,220 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// + +import { DataJobManageDetailsPage } from '../../../support/pages/app/lib/manage/data-job-details.po'; +import { DataJobsManagePage } from '../../../support/pages/app/lib/manage/data-jobs.po'; +import { applyGlobalEnvSettings } from '../../../support/helpers/commands.helpers'; + +describe('Data Job Manage Details Page', { tags: ['@dataPipelines', '@manageDataJobDetails'] }, () => { + const descriptionWordsBeforeTruncate = 12; + + let dataJobManageDetailsPage; + let testJobs; + let longLivedTestJob; + + before(() => { + return DataJobManageDetailsPage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot('data-job-manage-details')) + .then(() => DataJobManageDetailsPage.login()) + .then(() => cy.saveLocalStorage('data-job-manage-details')) + .then(() => cy.cleanTestJobs()) + .then(() => cy.prepareLongLivedTestJob()) + .then(() => cy.createTwoExecutionsLongLivedTestJob()) + .then(() => cy.prepareBaseTestJobs()) + .then(() => cy.fixture('lib/explore/test-jobs.json')) + .then((loadedTestJobs) => { + testJobs = applyGlobalEnvSettings(loadedTestJobs); + + return cy.wrap({ context: 'manage::data-job-details.spec::1::before()', action: 'continue' }); + }) + .then(() => cy.fixture('lib/manage/e2e-cypress-dp-test.json')) + .then((loadedTestJob) => { + longLivedTestJob = applyGlobalEnvSettings(loadedTestJob); + + return cy.wrap({ context: 'manage::data-job-details.spec::2::before()', action: 'continue' }); + }); + }); + + after(() => { + cy.cleanTestJobs(); + + DataJobManageDetailsPage.saveHarIfSupported(); + }); + + beforeEach(() => { + cy.restoreLocalStorage('data-job-manage-details'); + + DataJobManageDetailsPage.initBackendRequestInterceptor(); + }); + + it('Data Job Manage Details Page - disable/enable job', { tags: '@integration' }, () => { + dataJobManageDetailsPage = DataJobManageDetailsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobManageDetailsPage.clickOnContentContainer(); + + //Toggle job status twice, enable to disable and vice versa. + dataJobManageDetailsPage.toggleJobStatus(longLivedTestJob.job_name); + dataJobManageDetailsPage.toggleJobStatus(longLivedTestJob.job_name); + }); + + it('Data Job Manage Details Page - edit job description', { tags: '@integration' }, () => { + let newDescription = 'Test if changing the description is working'; + + dataJobManageDetailsPage = DataJobManageDetailsPage + .navigateTo(testJobs[0].team, testJobs[0].job_name); + + dataJobManageDetailsPage.clickOnContentContainer(); + + dataJobManageDetailsPage + .openDescription(); + + dataJobManageDetailsPage + .enterDescriptionDetails(newDescription); + + dataJobManageDetailsPage + .saveDescription(); + + dataJobManageDetailsPage + .getDescription() + .should('be.visible') + .should('contain.text', newDescription.split(' ').slice(0, descriptionWordsBeforeTruncate).join(' ')); + }); + + it('Data Job Manage Details Page - execute now', () => { + dataJobManageDetailsPage = DataJobManageDetailsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobManageDetailsPage + .waitForTestJobExecutionCompletion(); + + dataJobManageDetailsPage + .executeNow(); + + dataJobManageDetailsPage + .confirmInConfirmDialog(); + + dataJobManageDetailsPage + .getToastTitle(10000) + .should('exist') + .contains(/Data job Queued for execution|Failed, Data job is already executing/); + }); + + it('Data Job Manage Details Page - download job key', () => { + dataJobManageDetailsPage = DataJobManageDetailsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobManageDetailsPage + .openActionDropdown(); + + dataJobManageDetailsPage + .downloadJobKey(); + + dataJobManageDetailsPage + .readFile('downloadsFolder', `${ longLivedTestJob.job_name }.keytab`) + .should('exist'); + }); + + it('Data Job Manage Details Page - delete job', { tags: '@integration' }, () => { + cy.fixture('lib/explore/additional-test-job.json').then((additionalTestJob) => { + const normalizedTestJob = applyGlobalEnvSettings(additionalTestJob); + + cy.prepareAdditionalTestJobs(); + + dataJobManageDetailsPage = DataJobManageDetailsPage + .navigateTo(normalizedTestJob.team, normalizedTestJob.job_name); + + dataJobManageDetailsPage.clickOnContentContainer(); + + dataJobManageDetailsPage + .openActionDropdown(); + + dataJobManageDetailsPage + .deleteJob(); + + dataJobManageDetailsPage + .confirmDeleteJob(); + + dataJobManageDetailsPage + .getToastTitle(20000) // Wait up to 20 seconds for the job to be deleted. + .should('contain.text', 'Data job delete completed') + + dataJobManageDetailsPage + .waitForBackendRequestCompletion(); + + const dataJobsManagePage = DataJobsManagePage + .getPage(); + + dataJobsManagePage + .getDataGridCell(normalizedTestJob.job_name, 10000) // Wait up to 10 seconds for the jobs list to show. + .should('not.exist'); + }); + }); + + //TODO: Double-check and enable this test + it.skip('Data Job Manage Details Page - executions timeline', () => { + const jobName = longLivedTestJob.job_name; + const team = longLivedTestJob.team; + + cy.intercept({ + method: 'GET', + url: `/data-jobs/for-team/${ team }/jobs/${ jobName }/executions` + }).as('executionApiCall') + + dataJobManageDetailsPage = DataJobManageDetailsPage + .navigateTo(team, jobName); + + cy.wait('@executionApiCall').then((interception) => { + const response = interception.response.body; + const lastExecutions = response + .sort((left, right) => compareDatesAsc(left, right)) + .slice(response.length > 5 ? response.length - 5 : 0); + + const lastExecutionsSize = lastExecutions.length; + const lastExecution = lastExecutions.at(-1); + + cy.get('.clr-timeline-step') + .should('have.length', lastExecutionsSize + 1) // +1 next execution + + cy.get(`[data-cy=${ lastExecution.id }]`) + .as('lastExecution') + + let statusIconMap = []; + statusIconMap['cancelled'] = 'times-circle'; + statusIconMap['skipped'] = 'circle-arrow'; + statusIconMap['submitted'] = 'circle'; + statusIconMap['finished'] = 'success-standard'; + statusIconMap['failed'] = 'error-standard'; + + if (lastExecution.type !== 'manual') { + cy.get('@lastExecution') + .find('.manual-execution-label') + .scrollIntoView() + .should('be.visible'); + } + + if (lastExecution.status !== 'running') { + cy.get('@lastExecution') + .find(`[shape=${ statusIconMap[lastExecution.status] }]`) + .should('exist') + } + + cy.get('@lastExecution') + .find('.clr-timeline-step-header') + .invoke('attr', 'title') + .should('contain', 'Started ') + + if (lastExecution.status !== 'running') { + cy.get('@lastExecution') + .find('u') + .last() + .invoke('attr', 'title') + .should('contain', 'Ended ') + } + }) + }) +}); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/lib/manage/data-job-executions.int.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/lib/manage/data-job-executions.int.spec.js new file mode 100644 index 0000000000..20563bbca5 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/integration/lib/manage/data-job-executions.int.spec.js @@ -0,0 +1,644 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// + +import { DataJobBasePO } from '../../../support/application/data-job-base.po'; +import { DataJobsManagePage } from '../../../support/pages/app/lib/manage/data-jobs.po'; +import { DataJobManageExecutionsPage } from '../../../support/pages/app/lib/manage/data-job-executions.po'; +import { applyGlobalEnvSettings } from '../../../support/helpers/commands.helpers'; + +describe('Data Job Manage Executions Page', { tags: ['@dataPipelines', '@manageDataJobExecutions'] }, () => { + let longLivedTestJob; + + before(() => { + return DataJobManageExecutionsPage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot('data-job-manage-executions')) + .then(() => DataJobManageExecutionsPage.login()) + .then(() => cy.saveLocalStorage('data-job-manage-executions')) + .then(() => cy.prepareLongLivedTestJob()) + .then(() => cy.createTwoExecutionsLongLivedTestJob()) + .then(() => cy.fixture('lib/manage/e2e-cypress-dp-test.json')) + .then((loadedTestJob) => { + longLivedTestJob = applyGlobalEnvSettings(loadedTestJob); + + return cy.wrap({ context: 'manage::data-job-executions.spec::before()', action: 'continue' }); + }); + }); + + after(() => { + DataJobManageExecutionsPage.saveHarIfSupported(); + }); + + beforeEach(() => { + cy.restoreLocalStorage('data-job-manage-executions'); + + DataJobManageExecutionsPage.initBackendRequestInterceptor(); + }); + + describe('Sanity', { tags: '@integration' }, () => { + it(`Data Job Manage Executions Page - should open Details and verify Executions tab is displayed and navigates`, () => { + cy.log('Fixture for name: ' + longLivedTestJob.job_name); + + const dataJobsManagePage = DataJobsManagePage.navigateTo(); + + dataJobsManagePage + .clickOnContentContainer(); + + dataJobsManagePage + .chooseQuickFilter(0); + + dataJobsManagePage + .openJobDetails(longLivedTestJob.team, longLivedTestJob.job_name); + + const dataJobBasePage = DataJobBasePO + .getPage(); + + dataJobBasePage + .getDetailsTab() + .should('exist') + .should('have.class', 'active'); + + dataJobBasePage + .getExecutionsTab() + .should('exist') + .should('not.have.class', 'active'); + + dataJobBasePage + .openExecutionsTab(); + + const dataJobExecutionsPage = DataJobManageExecutionsPage + .getPage(); + + dataJobExecutionsPage + .getDetailsTab() + .should('exist') + .should('not.have.class', 'active'); + + dataJobExecutionsPage + .getExecutionsTab() + .should('exist') + .should('have.class', 'active'); + + dataJobExecutionsPage + .getDataGrid() + .should('exist'); + }); + + it('Data Job Manage Executions Page - should verify on URL navigate to Executions will open the page', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .getCurrentUrl() + .should('match', new RegExp(`\\/manage\\/data-jobs\\/${longLivedTestJob.team}\\/${longLivedTestJob.job_name}\\/executions$`)); + + dataJobExecutionsPage + .getDetailsTab() + .should('exist') + .should('not.have.class', 'active'); + + dataJobExecutionsPage + .getExecutionsTab() + .should('exist') + .should('have.class', 'active'); + + dataJobExecutionsPage + .getDataGrid() + .should('exist'); + }); + + it('Data Job Manage Executions Page - should verify elements are rendered in DOM', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .getMainTitle() + .should('be.visible') + .should('contains.text', longLivedTestJob.job_name); + + dataJobExecutionsPage + .getDetailsTab() + .should('exist') + .should('not.have.class', 'active'); + + dataJobExecutionsPage + .getExecutionsTab() + .should('exist') + .should('have.class', 'active'); + + dataJobExecutionsPage + .getExecuteNowButton() + .should('exist'); + + dataJobExecutionsPage + .getActionDropdownBtn() + .should('exist'); + + dataJobExecutionsPage + .openActionDropdown(); + + dataJobExecutionsPage + .getDeleteJobBtn() + .should('exist'); + + dataJobExecutionsPage + .clickOnContentContainer(); + + dataJobExecutionsPage + .waitForSmartDelay(); + + dataJobExecutionsPage + .getTimePeriod() + .should('exist'); + + dataJobExecutionsPage + .getStatusChart() + .should('exist'); + + dataJobExecutionsPage + .getDurationChart() + .should('exist'); + + dataJobExecutionsPage + .getDataGrid() + .should('exist'); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + }); + + it('Data Job Manage Executions Page - should verify cancel execution button works properly', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .initPostExecutionInterceptor(); + dataJobExecutionsPage + .initDeleteExecutionInterceptor(); + dataJobExecutionsPage + .initGetExecutionInterceptor(); + dataJobExecutionsPage + .initGetExecutionsInterceptor(); + + dataJobExecutionsPage + .getExecutionsTab() + .should('exist') + .should('have.class', 'active'); + + // Execute data job and check if the execution status after that is Running or Submitted + dataJobExecutionsPage + .getExecuteNowButton() + .should('exist'); + dataJobExecutionsPage + .executeNow(); + dataJobExecutionsPage + .getConfirmDialogButton() + .should('exist') + .click({ force: true }); + dataJobExecutionsPage + .waitForPostExecutionCompletion(); + dataJobExecutionsPage + .waitForDataJobStartExecute(); + + dataJobExecutionsPage + .waitForViewToRenderShort(); + + dataJobExecutionsPage + .getExecutionStatus() + .first() + .contains(/Running|Submitted/); + + // Cancel data job execution and check if the status after that is Cancelled + dataJobExecutionsPage + .getCancelExecutionButton() + .should('exist') + .click({ force: true }) + dataJobExecutionsPage + .getConfirmDialogButton() + .should('exist') + .click({ force: true }); + dataJobExecutionsPage + .waitForDeleteExecutionCompletion(); + dataJobExecutionsPage + .waitForDataJobStopExecute(); + + dataJobExecutionsPage + .waitForViewToRenderShort(); + + dataJobExecutionsPage + .getExecutionStatus() + .first() + .should('contains.text', 'Canceled'); + }); + }); + + it('Data Job Manage Executions Page - should verify time period is in correct format', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .getTimePeriod() + .invoke('text') + .invoke('trim') + .should('match', new RegExp(`^\\w+\\s\\d+,\\s\\d+,\\s\\d+:\\d+:\\d+\\s(AM|PM)\\sto\\s\\w+\\s\\d+,\\s\\d+,\\s\\d+:\\d+:\\d+\\s(AM|PM)$`)); + }); + + it('Data Job Manage Executions Page - should verify refresh button will show spinner and then load data', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .getDataGrid() + .should('exist'); + + dataJobExecutionsPage + .getExecLoadingSpinner() + .should('not.exist'); + + dataJobExecutionsPage + .getDataGridSpinner() + .should('not.exist'); + + dataJobExecutionsPage + .waitForActionThinkingTime(); + + dataJobExecutionsPage + .refreshExecData(); + + dataJobExecutionsPage + .getDataGrid() + .should('exist'); + + dataJobExecutionsPage + .getExecLoadingSpinner() + .should('exist'); + + dataJobExecutionsPage + .getDataGridSpinner() + .should('exist'); + + dataJobExecutionsPage + .waitForBackendRequestCompletion(); + + dataJobExecutionsPage + .waitForViewToRender(); + + dataJobExecutionsPage + .getDataGrid() + .should('exist'); + + dataJobExecutionsPage + .getExecLoadingSpinner() + .should('not.exist'); + + dataJobExecutionsPage + .getDataGridSpinner() + .should('not.exist'); + }); + + describe('DataGrid Filters', () => { + it('Data Job Manage Executions Page - should verify status filter options are rendered', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .openStatusFilter(); + + dataJobExecutionsPage + .getDataGridPopupFilter() + .should('exist'); + + dataJobExecutionsPage + .getDataGridExecStatusFilters() + .then((elements) => Array.from(elements).map((el) => el.innerText)) + .should('deep.equal', ['Success', 'Platform Error', 'User Error', 'Running', 'Submitted', 'Skipped', 'Canceled']); + + dataJobExecutionsPage + .closeFilter(); + + dataJobExecutionsPage + .getDataGridPopupFilter() + .should('not.exist'); + }); + + it('Data Job Manage Executions Page - should verify type filter options are rendered', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .openTypeFilter(); + + dataJobExecutionsPage + .getDataGridPopupFilter() + .should('exist'); + + dataJobExecutionsPage + .getDataGridExecTypeFilters() + .then((elements) => Array.from(elements).map((el) => el.innerText)) + .should('deep.equal', ['Manual', 'Scheduled']); + + dataJobExecutionsPage + .closeFilter(); + + dataJobExecutionsPage + .getDataGridPopupFilter() + .should('not.exist'); + }); + + it('Data Job Manage Executions Page - should verify id filter render input and filters correctly', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .openIDFilter(); + + dataJobExecutionsPage + .getDataGridPopupFilter() + .should('exist'); + + dataJobExecutionsPage + .typeToFilterInput('xyxyxy'); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length', 0); + + dataJobExecutionsPage + .clearFilterInput(); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .closeFilter(); + + dataJobExecutionsPage + .getDataGridPopupFilter() + .should('not.exist'); + }); + + it('Data Job Manage Executions Page - should verify version filter render input and filters correctly', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .openVersionFilter(); + + dataJobExecutionsPage + .getDataGridPopupFilter() + .should('exist'); + + dataJobExecutionsPage + .typeToFilterInput('xyxyxy'); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length', 0); + + dataJobExecutionsPage + .clearFilterInput(); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .closeFilter(); + + dataJobExecutionsPage + .getDataGridPopupFilter() + .should('not.exist'); + }); + }); + + describe('DataGrid Sort', () => { + it('Data Job Manage Executions Page - should verify duration sort works', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .getDataGridExecDurationHeader() + .should('exist') + .invoke('attr', 'aria-sort') + .should('eq', 'none'); + + dataJobExecutionsPage + .sortByExecDuration(); + + dataJobExecutionsPage + .getDataGridExecDurationHeader() + .should('exist') + .invoke('attr', 'aria-sort') + .should('eq', 'ascending'); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .getDataGridExecDurationCell(1) + .invoke('text') + .invoke('trim') + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecDurationCell(2) + .invoke('text') + .invoke('trim') + .then((elText2) => { + return dataJobExecutionsPage.convertStringContentToSeconds(elText1) - + dataJobExecutionsPage.convertStringContentToSeconds(elText2); + }); + }) + .should('be.lte', 0); + + dataJobExecutionsPage + .sortByExecDuration(); + + dataJobExecutionsPage + .getDataGridExecDurationHeader() + .should('exist') + .invoke('attr', 'aria-sort') + .should('eq', 'descending'); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .getDataGridExecDurationCell(1) + .invoke('text') + .invoke('trim') + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecDurationCell(2) + .invoke('text') + .invoke('trim') + .then((elText2) => { + return dataJobExecutionsPage.convertStringContentToSeconds(elText1) - + dataJobExecutionsPage.convertStringContentToSeconds(elText2); + }); + }) + .should('be.gte', 0); + }); + + it('Data Job Manage Executions Page - should verify execution start sort works', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .getDataGridExecStartHeader() + .should('exist') + .invoke('attr', 'aria-sort') + .should('eq', 'descending'); + + dataJobExecutionsPage + .getDataGridExecStartCell(1) + .invoke('text') + .invoke('trim') + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecStartCell(2) + .invoke('text') + .invoke('trim') + .then((elText2) => { + return new Date(elText1) - new Date(elText2) + }); + }) + .should('be.gte', 0); + + dataJobExecutionsPage + .sortByExecStart(); + + dataJobExecutionsPage + .getDataGridExecStartHeader() + .should('exist') + .invoke('attr', 'aria-sort') + .should('eq', 'ascending'); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .getDataGridExecStartCell(1) + .invoke('text') + .invoke('trim') + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecStartCell(2) + .invoke('text') + .invoke('trim') + .then((elText2) => { + return new Date(elText1) - new Date(elText2) + }); + }) + .should('be.lte', 0); + }); + + it('Data Job Manage Executions Page - should verify execution end sort works', () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage + .navigateTo(longLivedTestJob.team, longLivedTestJob.job_name); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .getDataGridExecEndHeader() + .should('exist') + .invoke('attr', 'aria-sort') + .should('eq', 'none'); + + dataJobExecutionsPage + .sortByExecEnd(); + + dataJobExecutionsPage + .getDataGridExecEndHeader() + .should('exist') + .invoke('attr', 'aria-sort') + .should('eq', 'ascending'); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .getDataGridExecEndCell(1) + .invoke('text') + .invoke('trim') + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecEndCell(2) + .invoke('text') + .invoke('trim') + .then((elText2) => { + const d1 = elText1 + ? new Date(elText1) + : new Date(); + const d2 = elText2 + ? new Date(elText2) + : new Date(); + + return d1 - d2 + }); + }) + .should('be.lte', 0); + + dataJobExecutionsPage + .sortByExecEnd(); + + dataJobExecutionsPage + .getDataGridExecEndHeader() + .should('exist') + .invoke('attr', 'aria-sort') + .should('eq', 'descending'); + + dataJobExecutionsPage + .getDataGridRows() + .should('have.length.gt', 0); + + dataJobExecutionsPage + .getDataGridExecEndCell(1) + .invoke('text') + .invoke('trim') + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecEndCell(2) + .invoke('text') + .invoke('trim') + .then((elText2) => { + const d1 = elText1 + ? new Date(elText1) + : new Date(); + const d2 = elText2 + ? new Date(elText2) + : new Date(); + + return d1 - d2 + }); + }) + .should('be.gte', 0); + }); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/lib/manage/data-jobs.int.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/lib/manage/data-jobs.int.spec.js new file mode 100644 index 0000000000..9fd6c538a6 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/integration/lib/manage/data-jobs.int.spec.js @@ -0,0 +1,494 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// +// Contain tests on this page: http://localhost.vmware.com:4200/manage/data-jobs +// 1. Is search working +// When you click on details does a modal pop-up? + +import { DataJobsManagePage } from '../../../support/pages/app/lib/manage/data-jobs.po'; +import { DataJobManageDetailsPage } from '../../../support/pages/app/lib/manage/data-job-details.po'; +import { applyGlobalEnvSettings } from '../../../support/helpers/commands.helpers'; + +describe('Data Jobs Manage Page', { tags: ['@dataPipelines', '@manageDataJobs'] }, () => { + const descriptionWordsBeforeTruncate = 12; + + /** + * @type {DataJobsManagePage} + */ + let dataJobsManagePage; + let testJobs; + let longLivedTestJob; + let longLivedFailingTestJob; + + before(() => { + return DataJobsManagePage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot('data-jobs-manage')) + .then(() => DataJobsManagePage.login()) + .then(() => cy.saveLocalStorage('data-jobs-manage')) + .then(() => cy.cleanTestJobs()) + .then(() => cy.prepareBaseTestJobs()) + .then(() => cy.prepareLongLivedTestJob()) + .then(() => cy.createTwoExecutionsLongLivedTestJob()) + .then(() => cy.prepareLongLivedFailingTestJob()) + .then(() => + cy.fixture('lib/explore/test-jobs.json') + .then((loadedTestJobs) => { + testJobs = applyGlobalEnvSettings(loadedTestJobs); + + return cy.wrap({ context: 'manage::1::data-jobs.int.spec::before()', action: 'continue' }); + }) + ) + .then(() => + cy.fixture('lib/manage/e2e-cypress-dp-test.json') + .then((loadedTestJob) => { + longLivedTestJob = applyGlobalEnvSettings(loadedTestJob); + + return cy.wrap({ context: 'manage::2::data-jobs.int.spec::before()', action: 'continue' }); + }) + ) + .then(() => + cy.fixture('e2e-cy-dp-failing.job.json') + .then((failingTestJob) => { + longLivedFailingTestJob = applyGlobalEnvSettings(failingTestJob); + + return cy.wrap({ context: 'manage::3::data-jobs.int.spec::before()', action: 'continue' }); + }) + ) + .then(() => + // Enable data job after job end + DataJobsManagePage.changeJobStatus(longLivedTestJob.team, longLivedTestJob.job_name, true) + ); + }); + + beforeEach(() => { + cy.restoreLocalStorage('data-jobs-manage'); + + DataJobsManagePage.initBackendRequestInterceptor(); + + dataJobsManagePage = DataJobsManagePage.navigateTo(); + }); + + after(() => { + cy.cleanTestJobs(); + + // Enable data job after job end + DataJobsManagePage.changeJobStatus(longLivedTestJob.team, longLivedTestJob.job_name, true) + + DataJobsManagePage.saveHarIfSupported(); + }); + + it('Data Jobs Manage Page - loads title', () => { + dataJobsManagePage + .getPageTitle() + .should('be.visible'); + }); + + it('Data Jobs Manage Page - grid contains test jobs', () => { + dataJobsManagePage + .chooseQuickFilter(0); + + dataJobsManagePage + .sortByJobName(); + + dataJobsManagePage + .getDataGrid() + .should('be.visible'); + + testJobs.forEach((testJob) => { + cy.log('Fixture for name: ' + testJob.job_name); + + dataJobsManagePage + .getDataGridCell(testJob.job_name) + .scrollIntoView() + .should('be.visible'); + }) + }); + + it('Data Jobs Manage Page - grid filter by job name', { tags: '@integration' }, () => { + dataJobsManagePage + .chooseQuickFilter(0); + + dataJobsManagePage + .filterByJobName(testJobs[0].job_name); + + dataJobsManagePage + .getDataGridCell(testJobs[0].job_name) + .should('be.visible'); + + dataJobsManagePage + .getDataGridCell(testJobs[1].job_name) + .should('not.exist'); + }); + + it('Data Jobs Manage Page - grid search by job name', { tags: '@integration' }, () => { + dataJobsManagePage + .clickOnContentContainer(); + + dataJobsManagePage + .chooseQuickFilter(0); + + dataJobsManagePage + .searchByJobName(testJobs[1].job_name); + + dataJobsManagePage + .getDataGridCell(testJobs[0].job_name) + .should('not.exist'); + + dataJobsManagePage + .getDataGridCell(testJobs[1].job_name) + .should('be.visible'); + }); + + it('Data Jobs Manage Page - grid search parameter goes into URL', () => { + dataJobsManagePage + .chooseQuickFilter(0); + + dataJobsManagePage + .sortByJobName(); + + // verify 2 test rows visible + dataJobsManagePage + .getDataGridCell(testJobs[0].job_name) + .scrollIntoView() + .should('be.visible'); + dataJobsManagePage + .getDataGridCell(testJobs[1].job_name) + .scrollIntoView() + .should('be.visible'); + + // do search + dataJobsManagePage + .searchByJobName(testJobs[0].job_name); + + dataJobsManagePage + .waitForViewToRender(); + + // verify 1 test row visible + dataJobsManagePage + .getDataGridCell(testJobs[0].job_name) + .scrollIntoView() + .should('be.visible'); + dataJobsManagePage + .getDataGridCell(testJobs[1].job_name) + .should('not.exist'); + + // verify url contains search value + dataJobsManagePage + .getCurrentUrl() + .should('match', new RegExp(`\\/manage\\/data-jobs\\?search=${ testJobs[0].job_name }$`)); + + // clear search with clear() method + dataJobsManagePage + .clearSearchField(); + + dataJobsManagePage + .waitForViewToRender(); + + // verify 2 test rows visible + dataJobsManagePage + .getDataGridCell(testJobs[0].job_name) + .scrollIntoView() + .should('be.visible'); + dataJobsManagePage + .getDataGridCell(testJobs[1].job_name) + .scrollIntoView() + .should('be.visible'); + + // verify url does not contain search value + dataJobsManagePage + .getCurrentUrl() + .should('match', new RegExp(`\\/manage\\/data-jobs$`)); + }); + + it('Data Jobs Manage Page - grid search perform search when URL contains search parameter', () => { + // navigate with search value in URL + dataJobsManagePage = DataJobsManagePage.navigateToUrl(`/manage/data-jobs?search=${ testJobs[1].job_name }`); + + dataJobsManagePage + .chooseQuickFilter(0); + + dataJobsManagePage + .sortByJobName(); + + // verify url contains search value + dataJobsManagePage + .getCurrentUrl() + .should('match', new RegExp(`\\/manage\\/data-jobs\\?search=${ testJobs[1].job_name }$`)); + + // verify 1 test row visible + dataJobsManagePage + .getDataGridCell(testJobs[0].job_name) + .should('not.exist'); + dataJobsManagePage + .getDataGridCell(testJobs[1].job_name) + .scrollIntoView() + .should('be.visible'); + + // clear search with button + dataJobsManagePage + .clearSearchFieldWithButton(); + + // verify 2 test rows visible + dataJobsManagePage + .getDataGridCell(testJobs[0].job_name) + .scrollIntoView() + .should('be.visible'); + dataJobsManagePage + .getDataGridCell(testJobs[1].job_name) + .scrollIntoView() + .should('be.visible'); + + // verify url does not contain search value + dataJobsManagePage + .getCurrentUrl() + .should('match', new RegExp(`\\/manage\\/data-jobs$`)); + }); + + it('Data Jobs Manage Page - refresh shows newly created job', () => { + cy.fixture('lib/explore/additional-test-job.json').then((additionalTestJob) => { + const normalizedTestJob = applyGlobalEnvSettings(additionalTestJob); + + cy.log('Fixture for name: ' + normalizedTestJob.job_name); + + dataJobsManagePage + .chooseQuickFilter(0); + + dataJobsManagePage + .sortByJobName(); + + dataJobsManagePage + .getDataGridCell(testJobs[0].job_name) + .should('have.text', testJobs[0].job_name); + + dataJobsManagePage + .getDataGridCell(normalizedTestJob.job_name) + .should('not.exist'); + + dataJobsManagePage + .prepareAdditionalTestJob(); + + dataJobsManagePage + .refreshDataGrid(); + + dataJobsManagePage + .filterByJobName(normalizedTestJob.job_name); + + dataJobsManagePage + .getDataGridCell(normalizedTestJob.job_name) + .should('have.text', normalizedTestJob.job_name); + }); + }); + + it('Data Jobs Manage Page - click on edit button opens new page with Job details', { tags: '@integration' }, () => { + dataJobsManagePage + .clickOnContentContainer(); + + dataJobsManagePage + .chooseQuickFilter(0); + + dataJobsManagePage + .sortByJobName(); + + dataJobsManagePage + .openJobDetails(testJobs[0].team, testJobs[0].job_name); + + const dataJobManageDetailsPage = DataJobManageDetailsPage + .getPage(); + + dataJobManageDetailsPage + .getMainTitle() + .should('be.visible') + .should('contains.text', testJobs[0].job_name); + + dataJobManageDetailsPage + .getDescription() + .should('be.visible') + .should('contain.text', testJobs[0].description.split(' ').slice(0, descriptionWordsBeforeTruncate).join(' ')) + + dataJobManageDetailsPage + .getSchedule() + .should('be.visible'); + + dataJobManageDetailsPage + .getDeploymentStatus('not-deployed') + .should('be.visible') + .should('have.text', 'Not Deployed'); + }); + + it('Data Jobs Manage Page - disable/enable job', { tags: '@integration' }, () => { + dataJobsManagePage + .clickOnContentContainer(); + + dataJobsManagePage + .chooseQuickFilter(0); + + const jobName = longLivedTestJob.job_name; + const team = longLivedTestJob.team; + + dataJobsManagePage + .searchByJobName(jobName); + + //Toggle job status twice, enable to disable and vice versa. + dataJobsManagePage.toggleJobStatus(longLivedTestJob.job_name); + dataJobsManagePage.toggleJobStatus(longLivedTestJob.job_name); + }); + + it('Data Jobs Manage Page - quick filters', { tags: '@integration' }, () => { + // Disable data job before test start + DataJobsManagePage + .changeJobStatus(longLivedFailingTestJob.team, longLivedFailingTestJob.job_name, true) + .then(() => + DataJobsManagePage.changeJobStatus(longLivedTestJob.team, longLivedTestJob.job_name, false) + ) + .then(() => { + dataJobsManagePage + .clickOnContentContainer(); + + dataJobsManagePage + .waitForClickThinkingTime(); + dataJobsManagePage + .chooseQuickFilter(0); + dataJobsManagePage + .waitForViewToRender(); + + dataJobsManagePage + .getDataGridStatusIcons() + .then(($icons) => { + for (const icon of Array.from($icons)) { + cy.wrap(icon).invoke('attr', 'data-cy') + .should('match', new RegExp('data-pipelines-job-(enabled|disabled|not-deployed)')); + } + }); + + dataJobsManagePage + .waitForClickThinkingTime(); + dataJobsManagePage + .chooseQuickFilter(1); + dataJobsManagePage + .waitForViewToRender(); + + dataJobsManagePage + .getDataGridStatusIcons() + .then(($icons) => { + for (const icon of Array.from($icons)) { + cy.wrap(icon).should('have.attr', 'data-cy', 'data-pipelines-job-enabled'); + } + }); + + dataJobsManagePage + .waitForClickThinkingTime(); + dataJobsManagePage + .chooseQuickFilter(2); + dataJobsManagePage + .waitForViewToRender(); + + dataJobsManagePage + .getDataGridStatusIcons() + .then(($icons) => { + for (const icon of Array.from($icons)) { + cy.wrap(icon).should('have.attr', 'data-cy', 'data-pipelines-job-disabled'); + } + }); + + dataJobsManagePage + .waitForClickThinkingTime(); + dataJobsManagePage + .chooseQuickFilter(3); + dataJobsManagePage + .waitForViewToRender(); + + dataJobsManagePage + .getDataGridStatusIcons() + .then(($icons) => { + for (const icon of Array.from($icons)) { + cy.wrap(icon).should('have.attr', 'data-cy', 'data-pipelines-job-not-deployed'); + } + }); + + // Enable data job after job end + DataJobsManagePage.changeJobStatus(longLivedTestJob.team, longLivedTestJob.job_name, true); + }); + }); + + it('Data Jobs Manage Page - show/hide column when toggling from menu', () => { + // show panel for show/hide columns + dataJobsManagePage + .toggleColumnShowHidePanel(); + + // verify correct options are rendered + dataJobsManagePage + .getDataGridColumnShowHideOptionsValues() + .should('have.length', 10) + .invoke('join', ',') + .should('eq', 'Description,Deployment Status,Last Execution Duration,Success rate,Next run (UTC),Last Deployed (UTC),Last Deployed By,Notifications,Source,Logs'); + + // verify column is not checked in toggling menu + dataJobsManagePage + .getDataGridColumnShowHideOption('Notifications') + .should('exist') + .should('not.be.checked'); + + // verify header cell for column is not rendered + dataJobsManagePage + .getDataGridHeaderCell('Notifications') + .should('have.length', 0); + + // toggle column to render + dataJobsManagePage + .checkColumnShowHideOption('Notifications'); + + // verify column is checked in toggling menu + dataJobsManagePage + .getDataGridColumnShowHideOption('Notifications') + .should('exist') + .should('be.checked'); + + // verify header cell for column is rendered + dataJobsManagePage + .getDataGridHeaderCell('Notifications') + .should('have.length', 1); + + // toggle column to hide + dataJobsManagePage + .uncheckColumnShowHideOption('Notifications'); + + // verify column is not checked in toggling menu + dataJobsManagePage + .getDataGridColumnShowHideOption('Notifications') + .should('exist') + .should('not.be.checked'); + + // verify header cell for column is not rendered + dataJobsManagePage + .getDataGridHeaderCell('Notifications') + .should('have.length', 0); + + // hide panel for show/hide columns + dataJobsManagePage + .toggleColumnShowHidePanel(); + }); + + it('Data Jobs Manage Page - execute now', { tags: '@integration' }, () => { + const jobName = longLivedTestJob.job_name; + const team = longLivedTestJob.team; + + dataJobsManagePage + .chooseQuickFilter(0); + + dataJobsManagePage + .sortByJobName(); + + dataJobsManagePage + .executeDataJob(jobName); + + // TODO better handling toast message. If tests are executed immediately it will return + // error 409 conflict and it will say that the job is already executing + dataJobsManagePage + .getToastTitle(10000) // Wait up to 10 seconds for Toast to show. + .should('exist') + .contains(/Data job Queued for execution|Failed, Data job is already executing/); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/e2e/plugins/index.js b/projects/frontend/data-pipelines/gui/e2e/plugins/index.js new file mode 100644 index 0000000000..1fd0b4f108 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/plugins/index.js @@ -0,0 +1,48 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +const { install, ensureBrowserFlags } = require('@neuralegion/cypress-har-generator'); +const path = require('path'); + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + const options = { + outputRoot: config.env.CYPRESS_TERMINAL_LOGS, + // Used to trim the base path of specs and reduce nesting in the + // generated output directory. + specRoot: path.relative(config.fileServerFolder, config.integrationFolder), + outputTarget: { + 'cypress-logs|json': 'json', + }, + printLogsToConsole: 'always', + printLogsToFile: 'always', + includeSuccessfulHookLogs: true, + logToFilesOnAfterRun: true + }; + + require('cypress-terminal-report/src/installLogsPrinter')(on, options); + install(on, config); + + on('before:browser:launch', (browser = {}, launchOptions) => { + ensureBrowserFlags(browser, launchOptions); + return launchOptions; + }); +}; diff --git a/projects/frontend/data-pipelines/gui/e2e/support/application/data-job-base.po.js b/projects/frontend/data-pipelines/gui/e2e/support/application/data-job-base.po.js new file mode 100644 index 0000000000..54a0782984 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/application/data-job-base.po.js @@ -0,0 +1,164 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataPipelinesBasePO } from './data-pipelines-base.po'; + +export class DataJobBasePO extends DataPipelinesBasePO { + static getPage() { + return new DataJobBasePO(); + } + + initGetExecutionInterceptor() { + cy.initGetExecutionInterceptor(); + } + + initGetExecutionsInterceptor() { + cy.initGetExecutionsInterceptor(); + } + + initPostExecutionInterceptor() { + cy.initPostExecutionInterceptor(); + } + + initDeleteExecutionInterceptor() { + cy.initDeleteExecutionInterceptor(); + } + + waitForPostExecutionCompletion() { + cy.waitForPostExecutionCompletion(); + } + + waitForDeleteExecutionCompletion() { + cy.waitForDeleteExecutionCompletion(); + } + + waitForDataJobStartExecute() { + cy.waitForInterceptor('@getExecutionsRequest', 20, (response) => { + return response?.data?.content?.findIndex((e) => { + return e.status?.toLowerCase() === 'running' || e.status?.toLowerCase() === 'submitted' + }); + }); + } + + waitForDataJobStopExecute() { + cy.waitForInterceptor('@getExecutionRequest', 20, (response) => { + return response?.status === 'cancelled'; + }); + } + + /* Selectors */ + + getNavigateBackBtn() { + return cy.get('[data-cy=data-pipelines-job-navigate-back]'); + } + + getMainTitle() { + return cy.get('[data-cy=data-pipelines-job-main-title]'); + } + + getExecuteNowButton() { + return cy.get('[data-cy=data-pipelines-job-execute-btn]'); + } + + getCancelExecutionButton() { + return cy.get('[data-cy=data-pipelines-job-cancel-execution-btn]'); + } + + getConfirmDialogButton() { + return cy.get('[data-cy="confirmation-dialog-ok-btn"]'); + } + + getActionDropdownBtn() { + return cy.get('[data-cy=data-pipelines-job-action-dropdown-btn]'); + } + + getDownloadKeyBtn() { + return cy.get('[data-cy=data-pipelines-job-download-btn]'); + } + + getDeleteJobBtn() { + return cy.get('[data-cy=data-pipelines-job-delete-btn]'); + } + + getConfirmDeleteBtn() { + return cy.get('#removeBtn'); + } + + getDetailsTab() { + return cy.get('[data-cy=data-pipelines-job-details-tab]'); + } + + getExecutionsTab() { + return cy.get('[data-cy=data-pipelines-job-executions-tab]'); + } + + getExecutionStatus() { + return cy.get('[data-cy="data-pipelines-job-execution-status"]'); + } + + /* Actions */ + + executeNow() { + this.getExecuteNowButton() + .scrollIntoView() + .click({ force: true }); + } + + openActionDropdown() { + this.getActionDropdownBtn() + .scrollIntoView() + .should('be.visible') + .click({ force: true }); + + this.waitForClickThinkingTime(); + } + + downloadJobKey() { + this.getDownloadKeyBtn() + .scrollIntoView() + .should('be.visible') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + } + + deleteJob() { + this.getDeleteJobBtn() + .scrollIntoView() + .should('be.visible') + .click({ force: true }); + } + + confirmDeleteJob() { + this.getConfirmDeleteBtn() + .scrollIntoView() + .should('be.visible') + .click({ force: true }); + } + + navigateBackToDataJobs() { + this.getNavigateBackBtn() + .should('be.visible') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + } + + openDetailsTab() { + this.getDetailsTab() + .should('exist') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + } + + openExecutionsTab() { + this.getExecutionsTab() + .should('exist') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/application/data-job-details-base.po.js b/projects/frontend/data-pipelines/gui/e2e/support/application/data-job-details-base.po.js new file mode 100644 index 0000000000..609099be4a --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/application/data-job-details-base.po.js @@ -0,0 +1,74 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataJobBasePO } from './data-job-base.po'; + +export class DataJobDetailsBasePO extends DataJobBasePO { + static getPage() { + return new DataJobDetailsBasePO(); + } + + static navigateTo(context, teamName, jobName) { + return this.navigateToUrl(`/${ context }/data-jobs/${ teamName }/${ jobName }`); + } + + // Selectors + + getStatusField() { + return cy.get('[data-cy=data-pipelines-job-details-status]'); + } + + getDescriptionField() { + return cy.get('[data-cy=data-pipelines-job-details-description]'); + } + + getDescriptionFull() { + return cy.get('[data-cy=description-show-less]'); + } + + getShowDescriptionMoreBtn() { + return cy.get('[data-cy=description-show-more]'); + } + + getTeamField() { + return cy.get('[data-cy=data-pipelines-job-details-team]'); + } + + getScheduleField() { + return cy.get('[data-cy=data-pipelines-job-details-schedule]'); + } + + getSourceField() { + return cy.get('[data-cy=data-pipelines-job-details-source]'); + } + + getOnDeployedField() { + return cy.get('[data-cy=data-pipelines-job-details-on-deployed]'); + } + + getOnPlatformErrorField() { + return cy.get('[data-cy=data-pipelines-job-details-on-platform-error]'); + } + + getOnUserErrorField() { + return cy.get('[data-cy=data-pipelines-job-details-on-user-error]'); + } + + getOnSuccessField() { + return cy.get('[data-cy=data-pipelines-job-details-on-success]'); + } + + // Actions + + showMoreDescription() { + this.getShowDescriptionMoreBtn() + .should('exist') + .click({ force: true }); + + this.waitForViewToRenderShort(); + + return this; + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/application/data-jobs-base.po.js b/projects/frontend/data-pipelines/gui/e2e/support/application/data-jobs-base.po.js new file mode 100644 index 0000000000..d4b5dd9a4a --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/application/data-jobs-base.po.js @@ -0,0 +1,180 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataPipelinesBasePO } from './data-pipelines-base.po'; + +export class DataJobsBasePO extends DataPipelinesBasePO { + static getPage() { + return new DataJobsExplorePage(); + } + + static navigateTo(linkSelector) { + cy.visit('/') + + cy.wait(DataJobsBasePO.INITIAL_PAGE_LOAD_WAIT_TIME); + + cy.get(linkSelector) + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + cy.wait(DataJobsBasePO.VIEW_RENDER_WAIT_TIME); + + return this.getPage(); + } + + /* Selectors */ + + getDataGrid() { + return cy.get('clr-datagrid'); + } + + getQuickFilters() { + return cy.get('[data-cy=data-pipelines-quick-filters] > span'); + } + + getHeaderColumnJobName() { + return cy.get('[data-cy=data-pipelines-jobs-name-column]'); + } + + getHeaderColumnJobNameSortBtn() { + return this.getHeaderColumnJobName() + .should('exist') + .find('.datagrid-column-title'); + } + + getDataGridHeaderCell(content) { + return this.getDataGrid() + .should('exist') + .find('clr-dg-column:not(.datagrid-hidden-column)') + .contains(new RegExp(`${ content }`)); + } + + // Rows and Cells + + getDataGridRows() { + return this.getDataGrid() + .should('exist') + .find('clr-dg-row.datagrid-row'); + } + + getDataGridRow(rowIndex) { + return this.getDataGridRows() + .should('have.length.gte', rowIndex - 1) + .then((rows) => Array.from(rows)[rowIndex - 1]); + } + + getDataGridCells(rowIndex) { + return this.getDataGridRow(rowIndex) + .should('exist') + .find('clr-dg-cell.datagrid-cell'); + } + + getDataGridCellByIndex(rowIndex, cellIndex) { + return this.getDataGridCells(rowIndex) + .should('have.length.gte', cellIndex - 1) + .then((cells) => Array.from(cells)[cellIndex - 1]); + } + + getDataGridCellByIdentifier(rowIndex, identifier) { + return this.getDataGridRow(rowIndex) + .should('exist') + .find(identifier) + } + + getDataGridStatusCells() { + return this.getDataGrid() + .should('exist') + .find('[data-cy=data-pipelines-manage-grid-status-cell]'); + } + + getDataGridStatusIcons() { + return this.getDataGrid() + .should('exist') + .find('[data-cy=data-pipelines-manage-grid-status-cell] clr-icon[data-cy*=data-pipelines-job]'); + } + + getDataGridColumnToggle() { + return this.getDataGrid() + .should('exist') + .find('clr-dg-column-toggle button'); + } + + getDataGridColumnShowHidePanel() { + return cy.get('.column-switch'); + } + + getDataGridColumnShowHideOptions() { + return this.getDataGridColumnShowHidePanel() + .should('exist') + .find('clr-checkbox-wrapper'); + } + + getDataGridColumnShowHideOptionsValues() { + return this.getDataGridColumnShowHidePanel() + .should('exist') + .find('clr-checkbox-wrapper') + .then((elements) => Array.from(elements).map((el) => el.innerText)); + } + + getDataGridColumnShowHideOption(option) { + return this.getDataGridColumnShowHideOptions() + .should('have.length.gt', 0) + .then((elements) => + Array.from(elements) + .find((el) => el.innerText === option) + ) + .find('input'); + } + + /* Actions */ + + chooseQuickFilter(filterPosition) { + this.getQuickFilters() + .then((filters) => { + return filters && filters[filterPosition]; + }) + .should('exist') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + sortByJobName() { + this.getHeaderColumnJobNameSortBtn() + .should('exist') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForSmartDelay(); + } + + toggleColumnShowHidePanel() { + this.getDataGridColumnToggle() + .should('exist') + .click({ force: true }); + + this.waitForClickThinkingTime(); + } + + checkColumnShowHideOption(option) { + this.getDataGridColumnShowHideOption(option) + .should('exist') + .check({ force: true }); + + this.waitForViewToRenderShort(); + } + + uncheckColumnShowHideOption(option) { + this.getDataGridColumnShowHideOption(option) + .should('exist') + .uncheck({ force: true }); + + this.waitForViewToRenderShort(); + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/application/data-pipelines-base.po.js b/projects/frontend/data-pipelines/gui/e2e/support/application/data-pipelines-base.po.js new file mode 100644 index 0000000000..3cd77e6c1c --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/application/data-pipelines-base.po.js @@ -0,0 +1,194 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export class DataPipelinesBasePO { + static API_MODIFY_CALL_TIME = 1000; // ms + static SMART_DELAY_TIME = 200; // ms + static CLICK_THINK_TIME = 500; // ms + static ACTION_THINK_TIME = 750; // ms + static VIEW_RENDER_SHORT_WAIT_TIME = 600; // ms + static VIEW_RENDER_WAIT_TIME = 1500; // ms + static INITIAL_PAGE_LOAD_WAIT_TIME = 2000; // ms + static INITIAL_PAGE_LOAD_LONG_WAIT_TIME = 2500; // ms + + static getPage() { + return new DataPipelinesBasePO(); + } + + static login() { + return cy.login(); + } + + static navigateToUrl(url) { + cy.visit(url); + + this.waitForBackendRequestCompletion(); + + cy.wait(DataPipelinesBasePO.VIEW_RENDER_SHORT_WAIT_TIME); + + return this.getPage(); + } + + /** + * ** Init API requests interceptor. + */ + static initBackendRequestInterceptor() { + cy.initBackendRequestInterceptor(); + } + + /** + * ** Wait for API request interceptor. + */ + static waitForBackendRequestCompletion(numberOfReqToWait = 1) { + const thinkTimeMillis = 1000; + + for (let i = 0; i < numberOfReqToWait; i++) { + cy.waitForBackendRequestCompletion(); + } + + cy.wait(thinkTimeMillis); + } + + /** + * ** Start HAR recording. + * + * @returns {Cypress.Chainable} + */ + static recordHarIfSupported() { + return cy.recordHarIfSupported(); + } + + /** + * ** Save recorded HAR. + * + * @returns {Cypress.Chainable} + */ + static saveHarIfSupported() { + return cy.saveHarIfSupported(); + } + + /** + * ** Response from changing Data Job status. + * @param {string} teamName + * @param {string} jobName + * @param {boolean} status + * @returns {Cypress.Chainable} + */ + static changeJobStatus(teamName, jobName, status) { + return cy.changeDataJobEnabledStatus(teamName, jobName, status); + } + + + // Interceptors and waiting + + waitForBackendRequestCompletion(numberOfReqToWait = 1) { + DataPipelinesBasePO.waitForBackendRequestCompletion(numberOfReqToWait); + } + + waitForTestJobExecutionCompletion() { + cy.waitForTestJobExecutionCompletion(); + } + + waitForApiModifyCall() { + cy.wait(DataPipelinesBasePO.API_MODIFY_CALL_TIME); + } + + waitForSmartDelay() { + cy.wait(DataPipelinesBasePO.SMART_DELAY_TIME); + } + + waitForClickThinkingTime() { + cy.wait(DataPipelinesBasePO.CLICK_THINK_TIME); + } + + waitForActionThinkingTime() { + cy.wait(DataPipelinesBasePO.ACTION_THINK_TIME); + } + + waitForViewToRenderShort() { + cy.wait(DataPipelinesBasePO.VIEW_RENDER_SHORT_WAIT_TIME); + } + + waitForViewToRender() { + cy.wait(DataPipelinesBasePO.VIEW_RENDER_WAIT_TIME); + } + + waitForInitialPageLoad() { + cy.wait(DataPipelinesBasePO.INITIAL_PAGE_LOAD_WAIT_TIME); + } + + /* Selectors */ + + getMainTitle() { + return cy.get('[data-cy=dp-main-title]'); + } + + getCurrentUrl() { + return cy.url(); + } + + getToast(timeout) { + return cy.get('vdk-toast-container vdk-toast', { timeout: this.resolveTimeout(timeout) }); + } + + getToastTitle(timeout) { + return this + .getToast(timeout) + .get('.toast-title'); + } + + getToastDismiss(timeout) { + return this + .getToast(timeout) + .get('.dismiss-bg'); + } + + getContentContainer() { + return cy.get('div.content-container'); + } + + /* Actions */ + + confirmInConfirmDialog() { + cy.get('[data-cy=confirmation-dialog-ok-btn]') + .should('exist') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + clickOnContentContainer() { + this.getContentContainer() + .should('exist') + .click({ force: true }); + + this.waitForSmartDelay(); + } + + resolveTimeout(timeout) { + return timeout === undefined + ? Cypress.config('defaultCommandTimeout') + : timeout; + } + + readFile(folderName, fileName) { + const path = require('path'); + const downloadsFolder = Cypress.config(folderName); + + return cy.readFile(path.join(downloadsFolder, fileName)); + } + + clearLocalStorage(key) { + if (key) { + cy.clearLocalStorage(key); + + return; + } + + cy.clearLocalStorage(); + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/commands.js b/projects/frontend/data-pipelines/gui/e2e/support/commands.js new file mode 100644 index 0000000000..e1e3ebd8d9 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/commands.js @@ -0,0 +1,252 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import 'cypress-localstorage-commands'; + +import { + applyGlobalEnvSettings, + createExecutionsForJob, + createTestJob, + deleteTestJobIfExists, + deployTestJobIfNotExists, + waitForJobExecutionCompletion +} from './helpers/commands.helpers'; + +Cypress.Commands.add('login', () => { + return cy.request({ + method: 'POST', + // See project README.md how to set this login url + url: Cypress.env('login_url'), + form: true, + body: { + // See project README.md how to set this token + api_token: Cypress.env('OAUTH2_API_TOKEN') + }, + followRedirect: false, + failOnStatusCode: false + }) + .its('body') + .then((body) => { + const token = body.access_token; + const expiresIn = body.expires_in; + const idToken = body.id_token; + const expiresAt = JSON.stringify(Date.now() + (expiresIn * 1000)); + + window.localStorage.setItem('expires_at', expiresAt); + window.localStorage.setItem('access_token', token); + window.localStorage.setItem('id_token', idToken); + + // Set the CSP token in the session storage to enable the Integration testing against Management UI + sessionStorage.setItem('expires_at', expiresAt); + sessionStorage.setItem('access_token', token); + sessionStorage.setItem('id_token', idToken); + + return cy.wrap({ context: 'commands::login()', action: 'continue' }); + }); +}); + +Cypress.Commands.add('initBackendRequestInterceptor', () => { + cy.intercept({ + method:'GET', + url:'**/data-jobs/for-team/**'}) + .as('getJobsRequest'); +}); + +Cypress.Commands.add('waitForBackendRequestCompletion', () => { + cy.wait('@getJobsRequest'); +}); + +Cypress.Commands.add('initGetExecutionsInterceptor', () => { + cy.intercept('GET', '**/data-jobs/for-team/**', (req) => { + if (req?.query?.operationName === 'jobsQuery' && req?.query?.query?.includes('executions')) { + req.alias = 'getExecutionsRequest'; + } + }) +}); + +Cypress.Commands.add('initGetExecutionInterceptor', () => { + cy.intercept({ + method: 'GET', + url: '**/data-jobs/for-team/**/executions/**' + }) + .as('getExecutionRequest'); +}); + +Cypress.Commands.add('waitForInterceptor', (aliasName, retries, predicate) => { + cy.wait(aliasName).its('response.body').then((json) => { + + if (predicate(json)) { + return; + } + + if (retries > 0) { + cy.waitForInterceptor(aliasName, retries - 1, predicate); + } + }); +}); + +Cypress.Commands.add('initPostExecutionInterceptor', () => { + cy.intercept({ + method: 'POST', + url: '**/data-jobs/for-team/**/executions' + }).as('postExecuteNow'); +}); + +Cypress.Commands.add('waitForPostExecutionCompletion', () => { + cy.wait('@postExecuteNow'); +}); + +Cypress.Commands.add('initDeleteExecutionInterceptor', () => { + cy.intercept({ + method: 'DELETE', + url: '**/data-jobs/for-team/**/executions/**' + }).as('deleteExecution'); +}); + +Cypress.Commands.add('waitForDeleteExecutionCompletion', () => { + cy.wait('@deleteExecution'); +}); + +Cypress.Commands.add('recordHarIfSupported', () => { + if (Cypress.browser.name === 'chrome') { + return cy.recordHar({ excludePaths: ['vendor.js$', 'clr-ui.min.css$', 'scripts.js$', 'polyfills.js$'] }); + } + + return cy.wrap({ context: 'commands::recordHarIfSupported()', action: 'continue' }); +}); + +Cypress.Commands.add('saveHarIfSupported', () => { + if (Cypress.browser.name === 'chrome') { + return cy.saveHar(); + } + + return cy.wrap({ context: 'commands::saveHarIfSupported()', action: 'continue' }); +}); + +Cypress.Commands.add('prepareBaseTestJobs', () => { + return cy.fixture('lib/explore/test-jobs.json') + .then((testJobs) => { + return Promise.all( + testJobs.map((testJob) => { + const normalizedTestJob = applyGlobalEnvSettings(testJob); + + return createTestJob(normalizedTestJob); + }) + ); + }); +}); + +Cypress.Commands.add('prepareAdditionalTestJobs', () => { + return cy.fixture('lib/explore/additional-test-job.json') + .then((testJob) => { + const normalizedTestJob = applyGlobalEnvSettings(testJob); + + return createTestJob(normalizedTestJob); + }) +}); + +Cypress.Commands.add('prepareLongLivedTestJob', () => { + return deployTestJobIfNotExists('lib/manage/e2e-cypress-dp-test.json', 'lib/manage/e2e-cypress-dp-test.zip'); +}); + +Cypress.Commands.add('prepareLongLivedFailingTestJob', () => { + return deployTestJobIfNotExists('e2e-cy-dp-failing.job.json', 'e2e-cy-dp-failing.job.zip'); +}); + +Cypress.Commands.add('cleanTestJobs', () => { + return cy.fixture('lib/explore/test-jobs.json') + .then((testJobs) => { + return Promise.all( + testJobs.map((testJob) => { + const normalizedTestJob = applyGlobalEnvSettings(testJob); + + return deleteTestJobIfExists(normalizedTestJob); + }) + ); + }) + .then(() => { + return cy.fixture('lib/explore/additional-test-job.json') + .then((testJob) => { + const normalizedTestJob = applyGlobalEnvSettings(testJob); + + return deleteTestJobIfExists(normalizedTestJob); + }); + }); +}); + +Cypress.Commands.add('waitForTestJobExecutionCompletion', () => { + const waitForJobExecutionTimeout = 180000; // Wait up to 3 min for job execution to complete + + return cy.fixture('lib/manage/e2e-cypress-dp-test.json') + .then((testJob) => { + const normalizedTestJob = applyGlobalEnvSettings(testJob); + + return waitForJobExecutionCompletion(normalizedTestJob.team, normalizedTestJob.job_name, waitForJobExecutionTimeout); + }); +}); + +Cypress.Commands.add('createTwoExecutionsLongLivedTestJob', () => { + const waitForJobExecutionTimeout = 180000; // Wait up to 3 min for job execution to complete + + return cy.fixture('lib/manage/e2e-cypress-dp-test.json') + .then((testJob) => { + const normalizedTestJob = applyGlobalEnvSettings(testJob); + + return createExecutionsForJob(normalizedTestJob.team, normalizedTestJob.job_name, waitForJobExecutionTimeout, 2); + }); +}); + +Cypress.Commands.add('createExecutionsLongLivedFailingTestJob', () => { + const waitForJobExecutionTimeout = 180000; // Wait up to 3 min for job execution to complete + + return cy.fixture('e2e-cy-dp-failing.job.json') + .then((failingTestJob) => { + const normalizedTestJob = applyGlobalEnvSettings(failingTestJob); + + return createExecutionsForJob(normalizedTestJob.team, normalizedTestJob.job_name, waitForJobExecutionTimeout, 2); + }); +}); + +Cypress.Commands.add('changeDataJobEnabledStatus', (teamName, jobName, status) => { + return cy.request({ + url: Cypress.env('data_jobs_url') + `/data-jobs/for-team/${ teamName }/jobs/${ jobName }/deployments`, + method: 'get', + auth: { + 'bearer': window.localStorage.getItem('access_token') + }, + failOnStatusCode: false + }).then((outerResponse) => { + if (outerResponse.status === 200) { + const lastDeployment = outerResponse.body[outerResponse.body.length - 1]; + const lastDeploymentHash = lastDeployment.job_version; + + return cy.request({ + url: Cypress.env('data_jobs_url') + `/data-jobs/for-team/${ teamName }/jobs/${ jobName }/deployments/${ lastDeploymentHash }`, + method: 'patch', + body: { enabled: status }, + auth: { + 'bearer': window.localStorage.getItem('access_token') + }, + failOnStatusCode: false + }).then((innerResponse) => { + if (innerResponse.status >= 200 && innerResponse.status < 300) { + cy.log(`Change enable status to [${ status }] for data job [${ jobName }]`); + } else { + cy.log(`Cannot change enabled status to [${ status }] for data job [${ jobName }]`); + + console.log(`Http request:`, innerResponse); + } + + return cy.wrap({ context: 'commands::1::changeDataJobEnabledStatus()', action: 'continue' }); + }); + } else { + cy.log(`Cannot change enabled status to [${ status }] for data job [${ jobName }]`); + + console.log(`Http request:`, outerResponse); + + return cy.wrap({ context: 'commands::2::changeDataJobEnabledStatus()', action: 'continue' }); + } + }); +}); diff --git a/projects/frontend/data-pipelines/gui/e2e/support/helpers/commands.helpers.js b/projects/frontend/data-pipelines/gui/e2e/support/helpers/commands.helpers.js new file mode 100644 index 0000000000..7c5a904835 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/helpers/commands.helpers.js @@ -0,0 +1,433 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +const DEFAULT_TEST_ENV_VAR = 'lib'; + +const applyGlobalEnvSettings = (loadedElement, injectedTestEnvVar = null) => { + const TEST_ENV_VAR = injectedTestEnvVar ?? _loadTestEnvironmentVar(); + + if (typeof loadedElement === 'string') { + return loadedElement.replace('$env-placeholder$', TEST_ENV_VAR); + } + + if (typeof loadedElement === 'object') { + Object.entries(loadedElement) + .forEach(([key, value]) => { + if (typeof value === 'string') { + if (value.includes('$env-placeholder$')) { + value = value.replace('$env-placeholder$', TEST_ENV_VAR); + } + + loadedElement[key] = value; + } + + if (typeof value === 'object') { + loadedElement[key] = applyGlobalEnvSettings(value); + } + }); + } + + return loadedElement; +}; + +const createExecutionsForJob = (teamName, jobName, waitForJobExecutionTimeout, numberOfExecutions = 2) => { + cy.log(`Trying to provide at least ${ numberOfExecutions } executions for Job "${ jobName }"`); + + return _getJobExecutions(teamName, jobName) + .then((executions) => { + cy.log(`Found ${ executions.length } executions for Job "${ jobName }" and target is ${ numberOfExecutions } executions`); + + if (executions.length >= numberOfExecutions) { + cy.log(`Skipping manual execution, expected number of executions found`); + + return cy.wrap({ context: 'commands.helpers::1::createExecutionsForJob()', action: 'continue' }); + } + + return waitForJobExecutionCompletion(teamName, jobName, waitForJobExecutionTimeout) + .then(() => { + return _getJobLastDeployment(teamName, jobName) + .then((lastDeployment) => { + if (lastDeployment) { + return _executeCreateExecutionsForJob( + teamName, + jobName, + waitForJobExecutionTimeout, + lastDeployment, + executions, + numberOfExecutions + ); + } + + return cy.wrap({ context: 'commands.helpers::2::createExecutionsForJob()', action: 'continue' }); + }); + }); + }); +}; + +const waitForJobExecutionCompletion = (teamName, jobName, jobExecutionTimeout, loopStep = 0) => { + const pollInterval = 5000; + + return cy.request({ + url: Cypress.env('data_jobs_url') + `/data-jobs/for-team/${ teamName }/jobs/${ jobName }/executions`, + method: 'get', + auth: { + 'bearer': window.localStorage.getItem('access_token') + } + }).then((response) => { + if (response.status !== 200) { + cy.log(`Job execution poll failed. Status: ${ response.status }; Message: ${ response.body }`); + + return cy.wrap({ context: 'commands.helpers::1::waitForJobExecutionCompletion()', action: 'continue' }); + } + + const lastExecutions = response.body + .sort((left, right) => _compareDatesAsc(left, right)) + .slice(response.body.length > 5 ? response.body.length - 5 : 0); + + if (!lastExecutions.length || lastExecutions.length === 0) { + cy.log(`No valid Job executions available.`); + + return cy.wrap({ context: 'commands.helpers::2::waitForJobExecutionCompletion()', action: 'continue' }); + } + + const lastExecution = lastExecutions[lastExecutions.length - 1]; + const lastExecutionStatus = lastExecution.status.toLowerCase(); + + if (lastExecutionStatus !== 'running' && lastExecutionStatus !== 'submitted') { + return ( + loopStep > 0 + ? cy.wait(pollInterval) + : cy.wrap({ context: 'commands.helpers::3::waitForJobExecutionCompletion()', action: 'continue' }) + ).then(() => { + cy.log(`Job "${ jobName }" executed successfully, polling completed!`); + + return cy.wrap({ context: 'commands.helpers::4::waitForJobExecutionCompletion()', action: 'continue' }); + }); + } + + if (jobExecutionTimeout <= 0) { + cy.log('Time to wait exceeded! Will not wait for job execution to finish any longer.'); + + return cy.wrap({ context: 'commands.helpers::5::waitForJobExecutionCompletion()', action: 'continue' }); + } + + cy.log(`Job ${ jobName }, still executing... Retrying after: ${ pollInterval / 1000 } seconds`); + + return cy.wait(pollInterval) + .then(() => waitForJobExecutionCompletion(teamName, jobName, jobExecutionTimeout - pollInterval, jobExecutionTimeout++)) + }); +}; + +const createTestJob = (testJob) => { + let teamName = testJob.team; + + return cy.request({ + // TODO: use jobsQuery - deprecated jobsList in favour of jobsQuery + url: Cypress.env('data_jobs_url') + `/data-jobs/for-team/${ teamName }/jobs`, + method: 'post', + body: testJob, + auth: { + 'bearer': window.localStorage.getItem('access_token') + }, + failOnStatusCode: false + }).then((response) => { + if (response.status >= 400) { + cy.log(`Http request fail for url:`, `/data-jobs/for-team/${ teamName }/jobs`); + + console.log(`Http request error:`, response); + } + + return cy.wrap(response); + }); +}; + +const deleteTestJobIfExists = (testJob) => { + let teamName = testJob.team; + let jobName = testJob.job_name; + + return cy.request({ + url: Cypress.env('data_jobs_url') + `/data-jobs/for-team/${ teamName }/jobs/${ jobName }`, + method: 'get', + auth: { + 'bearer': window.localStorage.getItem('access_token') + }, + failOnStatusCode: false + }).then((outerResponse) => { + if (outerResponse.status === 200) { + return cy.request({ + url: Cypress.env('data_jobs_url') + `/data-jobs/for-team/${ teamName }/jobs/${ jobName }`, + method: 'delete', + auth: { + 'bearer': window.localStorage.getItem('access_token') + }, + failOnStatusCode: false + }).then((innerResponse) => { + if (innerResponse.status >= 400) { + cy.log(`Http request fail for url:`, `/data-jobs/for-team/${ teamName }/jobs/${ jobName }`); + + console.log(`Http request error:`, innerResponse); + } + + return cy.wrap({ context: 'commands.helpers::1::deleteTestJobIfExists()', action: 'continue' }); + }); + } else if (outerResponse.status >= 400) { + cy.log(`Http request fail for url:`, `/data-jobs/for-team/${ teamName }/jobs/${ jobName }`); + + console.log(`Http request error:`, outerResponse); + } + + return cy.wrap({ context: 'commands.helpers::2::deleteTestJobIfExists()', action: 'continue' }); + }); +}; + +const deployTestJobIfNotExists = (pathToJobData, pathToJobBinary) => { + return cy.fixture(pathToJobData) + .then((testJob) => { + const normalizedTestJob = applyGlobalEnvSettings(testJob); + const teamName = normalizedTestJob.team; + const jobName = normalizedTestJob.job_name; + + // Check if data job exists and wether it has deployment or not. + return cy.request({ + url: Cypress.env('data_jobs_url') + `/data-jobs/for-team/${ teamName }/jobs/${ jobName }/deployments`, + method: 'get', + auth: { + 'bearer': window.localStorage.getItem('access_token') + }, + failOnStatusCode: false + }).then((outerResponse) => { + if (outerResponse.status === 200 && outerResponse.body.length > 0) { + cy.log(jobName + ' is already existing, therefore skipping creation and deployment.'); + } else if (outerResponse.status === 200 && outerResponse.body.length === 0) { + cy.log(jobName + ' exists, but it is not deployed. Deploying...'); + + return _deployTestDataJob(normalizedTestJob, pathToJobBinary); + } else if (outerResponse.status === 404) { + return createTestJob(normalizedTestJob) + .then((innerResponse) => { + if (innerResponse.status === 201) { + return _deployTestDataJob(normalizedTestJob, pathToJobBinary); + } else { + throw new Error('Failed to create job: ' + jobName); + } + }); + } else if (outerResponse.status >= 400) { + cy.log(`Http request fail for url:`, `/data-jobs/for-team/${ teamName }/jobs/${ jobName }/deployments`); + + console.log(`Http request error:`, outerResponse); + } + + return cy.wrap({ context: 'commands.helpers::deployTestJobIfNotExists()', action: 'continue' }); + }); + }); +}; + +function _loadTestEnvironmentVar() { + let testEnv = Cypress?.env('test_environment'); + + if (!testEnv) { + console.log(`DataPipelines test_environment is not set. Using default: ${ DEFAULT_TEST_ENV_VAR }`); + testEnv = DEFAULT_TEST_ENV_VAR; + } + + return testEnv; +} + +function _executeCreateExecutionsForJob(teamName, jobName, waitForJobExecutionTimeout, lastDeployment, executions, numberOfExecutions, counterOfExecutions = 0) { + const neededExecutions = numberOfExecutions - executions.length; + + if (counterOfExecutions === 0) { + cy.log(`Submitting ${ neededExecutions } serial executions`); + } else { + cy.log(`${ neededExecutions - counterOfExecutions } more executions left for executing`) + } + + if (neededExecutions === counterOfExecutions) { + return cy.wrap({ context: 'commands.helpers::1::_executeCreateExecutionsForJob()', action: 'continue' }); + } + + return _executeJob(teamName, jobName, lastDeployment.id) + .then(() => { + return cy.wait(5000) + .then(() => cy.wrap({ context: 'commands.helpers::2::_executeCreateExecutionsForJob()', action: 'continue' })); + }) + .then(() => { + return waitForJobExecutionCompletion(teamName, jobName, waitForJobExecutionTimeout); + }) + .then(() => { + return _executeCreateExecutionsForJob( + teamName, + jobName, + waitForJobExecutionTimeout, + lastDeployment, + executions, + numberOfExecutions, + counterOfExecutions + 1 + ); + }); +} + +function _getJobExecutions(teamName, jobName) { + cy.log(`Fetching Job executions for Job "${ jobName }"`); + + return cy + .request({ + url: `/data-jobs/for-team/${ teamName }/jobs/${ jobName }/executions`, + method: 'get', + auth: { + 'bearer': window.localStorage.getItem('access_token') + } + }) + .then((response) => { + if (response.status !== 200) { + cy.log(`Getting Job executions failed. Status: ${ response.status }; Message: ${ response.body }`); + + return cy.wrap([]); + } + + return cy.wrap( + response.body && response.body.length + ? response.body + : [] + ); + }); +} + +function _getJobLastDeployment(teamName, jobName) { + cy.log(`Fetching Job deployments for Job "${ jobName }"`); + + return cy + .request({ + url: Cypress.env('data_jobs_url') + `/data-jobs/for-team/${ teamName }/jobs/${ jobName }/deployments`, + method: 'get', + auth: { + 'bearer': window.localStorage.getItem('access_token') + } + }) + .then((response) => { + if (response.status !== 200) { + cy.log(`Getting Job deployments failed. Status: ${ response.status }; Message: ${ response.body }`); + + return cy.wrap(null); + } + + return cy.wrap( + response.body && response.body.length + ? response.body[response.body.length - 1] + : null + ); + }); +} + +function _executeJob(teamName, jobName, deploymentId) { + cy.log(`Submitting execution for Job "${ jobName }"`); + + return cy.request({ + url: Cypress.env('data_jobs_url') + `/data-jobs/for-team/${ teamName }/jobs/${ jobName }/deployments/${ deploymentId }/executions`, + method: 'post', + auth: { + 'bearer': window.localStorage.getItem('access_token') + }, + body: {} + }).then((response) => { + if (response.status >= 400) { + cy.log(`Executing Job failed. Status: ${ response.status }; Message: ${ response.body }`); + } + + return cy.wrap({ context: 'commands.helpers::_executeJob()', action: 'continue' }); + }); +} + +function _compareDatesAsc(left, right) { + const leftStartTime = left.start_time ? left.start_time : 0; + const rightStartTime = right.start_time ? right.start_time : 0; + + return new Date(leftStartTime).getTime() - new Date(rightStartTime).getTime(); +} + +function _deployTestDataJob(testJob, pathToJobBinary) { + const teamName = testJob.team; + const jobName = testJob.job_name; + const waitForJobDeploymentTimeout = 300000; // Wait up to 5 min for deployment to complete. + + cy.log(`Deploying Job ${ jobName }`); + + // Upload Data Job code + return cy.fixture(pathToJobBinary, 'binary') + .then((zippedDataJob) => { + return cy.request({ + url: Cypress.env('data_jobs_url') + '/data-jobs/for-team/' + teamName + '/jobs/' + jobName + '/sources', + method: 'post', + auth: { + 'bearer': window.localStorage.getItem('access_token') + }, + body: Cypress.Blob.binaryStringToBlob(zippedDataJob) + }).then((response1) => { + let jsonResponse = JSON.parse(Cypress.Blob.arrayBufferToBinaryString(response1.body)); + + // Deploy data job + return cy.request({ + url: Cypress.env('data_jobs_url') + '/data-jobs/for-team/' + teamName + '/jobs/' + jobName + '/deployments', + method: 'post', + auth: { + 'bearer': window.localStorage.getItem('access_token') + }, + body: { + job_version: jsonResponse.version_sha, + mode: 'release', + enabled: true + } + }).then((response2) => { + if (response2.status === 202) { + cy.log(`Job: ${ jobName } deployment in progress. Waiting for deployment to complete`); + + return _waitForDeploymentToFinish(testJob, waitForJobDeploymentTimeout); + } + + return cy.wrap({ context: 'commands.helpers::_deployTestDataJob()', action: 'continue' }); + }); + }); + }); +} + +function _waitForDeploymentToFinish(testJob, timeout) { + const teamName = testJob.team; + const jobName = testJob.job_name; + const waitInterval = 5000; + + return cy.request({ + url: Cypress.env('data_jobs_url') + '/data-jobs/for-team/' + teamName + '/jobs/' + jobName + '/deployments', + method: 'get', + auth: { + 'bearer': window.localStorage.getItem('access_token') + } + }).then((resp) => { + if (resp.status === 200 && resp.body.length > 0) { + cy.log('Job deployment finished!'); + + return cy.wrap({ context: 'commands.helpers::1::_waitForDeploymentToFinish()', action: 'continue' }); + } + + if (timeout <= 0) { + cy.log('Time to wait exceeded! Will not wait for job deployment to finish any longer.'); + + return cy.wrap({ context: 'commands.helpers::2::_waitForDeploymentToFinish()', action: 'continue' }); + } + + cy.log(`Job ${ jobName }, still not deployed.. retrying after: ${ waitInterval / 1000 } seconds`); + + return cy.wait(waitInterval) + .then(() => _waitForDeploymentToFinish(testJob, timeout - waitInterval)); + }) +} + +module.exports = { + applyGlobalEnvSettings, + createExecutionsForJob, + waitForJobExecutionCompletion, + createTestJob, + deleteTestJobIfExists, + deployTestJobIfNotExists +}; diff --git a/projects/frontend/data-pipelines/gui/e2e/support/index.js b/projects/frontend/data-pipelines/gui/e2e/support/index.js new file mode 100644 index 0000000000..dfe8fbb0bc --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/index.js @@ -0,0 +1,18 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import './commands' + +require('@neuralegion/cypress-har-generator/commands'); +require('cypress-grep')(); +require('cypress-terminal-report/src/installLogsCollector')(); + +Cypress.Server.defaults({ + delay: 500, + force404: false, + ignore: (xhr) => { + return true; + } +}) diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/app/app.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/app.po.js new file mode 100644 index 0000000000..6a0e3e6cbb --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/app.po.js @@ -0,0 +1,20 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataPipelinesBasePO } from '../../application/data-pipelines-base.po'; + +export class AppPage extends DataPipelinesBasePO { + static getPage() { + return new AppPage(); + } + + static navigateTo() { + cy.visit('/'); + } + + getMainTitle() { + return cy.get('[data-cy=app-main-title]'); + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/app/getting-started/data-jobs-health-panel-component.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/getting-started/data-jobs-health-panel-component.po.js new file mode 100644 index 0000000000..13fcc897f5 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/getting-started/data-jobs-health-panel-component.po.js @@ -0,0 +1,159 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { GettingStartedPage } from './getting-started.po'; + +export class DataJobsHealthPanelComponentPO extends GettingStartedPage { + /** + * ** Returns instance of the page object. + * + * @returns {DataJobsHealthPanelComponentPO} + */ + static getComponent() { + return new DataJobsHealthPanelComponentPO(); + } + + /** + * ** Get Execution status gauge Widget. + * + * @returns {Cypress.Chainable>} + */ + getExecutionStatusGaugeWidget() { + return cy.get('[data-cy=dp-data-jobs-status-gauge-widget]'); + } + + /** + * ** Get percentage of successful executions against total from Execution status gauge Widget. + * + * @returns {Cypress.Chainable} + */ + getExecutionsSuccessPercentage() { + return this.getExecutionStatusGaugeWidget() + .should('exist') + .find('[data-cy=dp-jobs-executions-status-gauge-widget-percentage]') + .invoke('text') + .invoke('replace', /%/, '') + .then((value) => +value); + } + + /** + * ** Get number of failed executions from Execution status gauge Widget. + * + * @returns {Cypress.Chainable} + */ + getNumberOfFailedExecutions() { + return this.getExecutionStatusGaugeWidget() + .should('exist') + .find('[data-cy=dp-jobs-executions-status-gauge-widget-failed]') + .invoke('text') + .invoke('replace', /\s\w+/, '') + .then((value) => +value); + } + + /** + * ** Get number of total executions from Execution status gauge Widget. + * + * @returns {Cypress.Chainable} + */ + getExecutionsTotal() { + return this.getExecutionStatusGaugeWidget() + .should('exist') + .find('[data-cy=dp-jobs-executions-status-gauge-widget-total]') + .invoke('text') + .invoke('replace', /\s\w+/, '') + .then((value) => +value); + } + + /** + * ** Get Failing Data jobs Widget. + * + * @returns {Cypress.Chainable>} + */ + getFailingJobsWidget() { + return cy.get('[data-cy=dp-failed-data-jobs-widget]'); + } + + /** + * ** Get all Data jobs from Failing Data jobs Widget. + * + * @returns {Cypress.Chainable>} + */ + getAllFailingJobs() { + return this.getFailingJobsWidget() + .should('exist') + .find('[data-cy=dp-failed-data-jobs-widget-job-name-link]'); + } + + /** + * ** Get Most Recent Failing Data jobs executions Widget. + * + * @returns {Cypress.Chainable>} + */ + getMostRecentFailingJobsExecutionsWidget() { + return cy.get('[data-cy=dp-failed-data-jobs-executions-widget]'); + } + + /** + * ** Get all executions from Most Recent Failing Data jobs executions Widget. + * + * @returns {Cypress.Chainable>} + */ + getAllMostRecentFailingJobsExecutions() { + return this.getMostRecentFailingJobsExecutionsWidget() + .should('exist') + .find('[data-cy=dp-failed-data-jobs-executions-widget-job-name-link]'); + } + + // Actions + + navigateToFailingJobDetails(jobName) { + this._navigateToDataJob( + this.getAllFailingJobs(), + jobName, + 3 + ); + } + + navigateToMostRecentFailingJobExecutions(jobName) { + this._navigateToDataJob( + this.getAllMostRecentFailingJobsExecutions(), + jobName, + 2 + ); + } + + /** + * ** Navigate to Data job + * + * @param {Cypress.Chainable>} chainable + * @param {string} jobName + * @param {number} numberOfReqToWait + * @private + */ + _navigateToDataJob(chainable, jobName, numberOfReqToWait = 2) { + chainable + .then((elements) => { + /** + * @type {HTMLElement} + */ + let foundElement; + elements.each((_index, el) => { + if (el.innerText.trim().includes(jobName)) { + foundElement = el; + + return false; + } + }); + + return foundElement; + }) + .should('exist') + .click({ force: true }); + + this.waitForBackendRequestCompletion(numberOfReqToWait); + + this.waitForViewToRender(); + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/app/getting-started/getting-started.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/getting-started/getting-started.po.js new file mode 100644 index 0000000000..0f23a016bb --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/getting-started/getting-started.po.js @@ -0,0 +1,50 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataPipelinesBasePO } from '../../../application/data-pipelines-base.po'; + +export class GettingStartedPage extends DataPipelinesBasePO { + /** + * ** Navigate to getting started page. + * + * @returns {GettingStartedPage} + */ + static navigateTo() { + cy.visit('/get-started'); + + this.waitForBackendRequestCompletion(3); + + return this.getPage(); + } + + /** + * ** Returns instance of the page object. + * + * @returns {GettingStartedPage} + */ + static getPage() { + return new GettingStartedPage(); + } + + // Selectors + + /** + * ** Returns main title of getting started page. + * + * @returns {Cypress.Chainable>} + */ + getMainTitle() { + return cy.get('[data-cy=getting-started-main-title]'); + } + + /** + * ** Get Data jobs health panel. + * + * @returns {Cypress.Chainable>} + */ + getDataJobsHealthPanel() { + return cy.get('[data-cy=dp-data-jobs-health-panel]'); + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/explore/data-job-details.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/explore/data-job-details.po.js new file mode 100644 index 0000000000..222bdee83a --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/explore/data-job-details.po.js @@ -0,0 +1,16 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataJobDetailsBasePO } from '../../../../application/data-job-details-base.po'; + +export class DataJobExploreDetailsPage extends DataJobDetailsBasePO { + static getPage() { + return new DataJobExploreDetailsPage(); + } + + static navigateTo(teamName, jobName) { + return super.navigateTo('explore', teamName, jobName); + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/explore/data-jobs.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/explore/data-jobs.po.js new file mode 100644 index 0000000000..373f0c96e4 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/explore/data-jobs.po.js @@ -0,0 +1,127 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataJobsBasePO } from '../../../../application/data-jobs-base.po'; + +export class DataJobsExplorePage extends DataJobsBasePO { + static getPage() { + return new DataJobsExplorePage(); + } + + static navigateTo() { + return super.navigateTo('[data-cy=navigation-link-explore-datajobs]'); + } + + // Selectors + + getMainTitle() { + return cy.get('[data-cy=data-pipelines-explore-page-main-title]'); + } + + getDataGrid() { + return cy.get('[data-cy=data-pipelines-explore-grid]'); + } + + getDataGridCell(content) { + return cy.get('[id^="clr-dg-row"] > .datagrid-row-scrollable > .datagrid-scrolling-cells > .ng-star-inserted') + .contains(new RegExp(`^${ content }$`)); + } + + getDataGridNavigateBtn(team, job) { + return cy.get('[data-cy=data-pipelines-explore-grid-details-link][data-job-params="' + team + ';' + job + '"]'); + } + + getDataGridNameFilter() { + return this.getHeaderColumnJobName() + .should('exist') + .find('clr-dg-filter button'); + } + + getDataGridNameFilterInput() { + return cy.get('div.datagrid-filter input'); + } + + getDataGridRefreshButton() { + return cy.get('[data-cy=data-pipelines-explore-refresh-btn]'); + } + + getDataGridSearchInput() { + return cy.get('[data-test-id=search-input]'); + } + + getDataGridClearSearchButton() { + return cy.get('[data-test-id="clear-search-btn"]').first(); + } + + // Actions + + refreshDataGrid() { + this.getDataGridRefreshButton() + .should('be.visible') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + searchByJobName(jobName) { + this.getDataGridSearchInput() + .should('be.visible') + .type(jobName); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + clearSearchField() { + this.getDataGridSearchInput() + .should('be.visible') + .clear(); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + clearSearchFieldWithButton() { + this.getDataGridClearSearchButton() + .should('be.visible') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + filterByJobName(jobName) { + this.getDataGridNameFilter() + .click({ force: true }); + + this.getDataGridNameFilterInput() + .should('be.visible') + .type(jobName); + + this.getMainTitle() + .should('be.visible') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + openJobDetails(team, jobName) { + this.getDataGridNavigateBtn(team, jobName) + .scrollIntoView() + .should('exist') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRenderShort(); + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/manage/data-job-details.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/manage/data-job-details.po.js new file mode 100644 index 0000000000..f0ac3c8b2b --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/manage/data-job-details.po.js @@ -0,0 +1,123 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataJobDetailsBasePO } from '../../../../application/data-job-details-base.po'; + +export class DataJobManageDetailsPage extends DataJobDetailsBasePO { + static getPage() { + return new DataJobManageDetailsPage(); + } + + static navigateTo(teamName, jobName) { + return super.navigateTo('manage', teamName, jobName); + } + + // Deployment methods + + // Acceptable values are "not-deployed", "enabled", "disabled" + getDeploymentStatus(status) { + return cy.get('[data-cy=data-pipelines-job-details-status-' + status + ']'); + } + + // Description methods + + getDescription() { + return cy.get('[data-cy=data-pipelines-data-job-details-description] .form-section-readonly'); + } + + getDescriptionEditButton() { + return cy.get('[data-cy=data-pipelines-data-job-details-description] .form-section-header > .btn'); + } + + getDescriptionEditTextarea() { + return cy.get('[data-cy=data-pipelines-data-job-details-description] textarea'); + } + + getDescriptionSaveButton() { + return cy.get('[data-cy=data-pipelines-data-job-details-description] button:contains(Save)') + } + + openDescription() { + this.getDescriptionEditButton() + .click({ force: true }); + } + + enterDescriptionDetails(description) { + this.getDescriptionEditTextarea() + .clear() + .type(description) + } + + saveDescription() { + this.getDescriptionSaveButton() + .click({ force: true }); + + this.waitForBackendRequestCompletion() + } + + // Schedule methods + + getSchedule() { + return cy.get('[data-cy=data-pipelines-data-job-details-schedule] .form-section-readonly'); + } + + // Disable/Enable methods + + getStatusEditButton() { + return cy.get('[data-cy=data-pipelines-data-job-details-status] .form-section-header > .btn') + } + + getStatusSaveButton() { + return cy.get('[data-cy=data-pipelines-data-job-details-status] button:contains(Save)'); + } + + changeStatus(currentStatus) { + const newStatus = currentStatus.trim().toLowerCase() === 'enabled' + ? 'disable' + : 'enable'; + + return cy.get(`[data-cy=data-pipelines-data-job-details-status-${ newStatus }]`) + .should('exist') + .check({ force: true }); + } + + toggleJobStatus() { + cy.get('[data-cy=data-pipelines-job-details-status]') + .invoke('text') + .then((jobStatus) => { + this.getStatusEditButton() + .scrollIntoView() + .click({ force: true }); + + this.changeStatus(jobStatus); + + this.waitForClickThinkingTime(); + + this.getStatusSaveButton() + .scrollIntoView() + .click({ force: true }); + + let newStatus = jobStatus === 'Enabled' + ? 'Disabled' + : 'Enabled'; + + this.waitForBackendRequestCompletion(); + + this.getToastTitle() + .should('exist') + .should('contain.text', 'Status update completed'); + + this.waitForActionThinkingTime(); // Natural wait for User action + + this.getToastDismiss() + .should('exist') + .click({ force: true }); + + cy.get('[data-cy=data-pipelines-job-details-status]') + .scrollIntoView() + .should('have.text', newStatus); + }) + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/manage/data-job-executions.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/manage/data-job-executions.po.js new file mode 100644 index 0000000000..af3c744c6a --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/manage/data-job-executions.po.js @@ -0,0 +1,360 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataJobBasePO } from '../../../../application/data-job-base.po'; + +export class DataJobManageExecutionsPage extends DataJobBasePO { + static getPage() { + return new DataJobManageExecutionsPage(); + } + + static navigateTo(teamName, jobName) { + return super.navigateToUrl(`/manage/data-jobs/${ teamName }/${ jobName }/executions`); + } + + /* Utils */ + + /** + * @example + * + * - val=1m 34s + * - val=45s + */ + convertStringContentToSeconds(val) { + const params = val.split(' '); + + if (params.length === 2) { + return parseInt(params[0].trim().replace(/m$/, ''), 10) * 60 + parseInt(params[1].trim().replace(/s$/, ''), 10); + } else { + return parseInt(params[0].trim().replace(/s$/, ''), 10); + } + } + + /* Selectors */ + + // General + + getExecRefreshBtn() { + return cy.get('[data-cy=data-pipelines-job-executions-refresh-btn'); + } + + getExecLoadingSpinner() { + return cy.get('[data-cy=data-pipelines-job-executions-loading-spinner]'); + } + + // Charts + + getStatusChart() { + return cy.get('[data-cy=data-pipelines-job-executions-status-chart]'); + } + + getDurationChart() { + return cy.get('[data-cy=data-pipelines-job-executions-duration-chart]'); + } + + getTimePeriod() { + return cy.get('[data-cy=data-pipelines-job-executions-time-period]'); + } + + // DataGrid + + getDataGrid() { + return cy.get('[data-cy=data-pipelines-job-executions-datagrid]'); + } + + getDataGridPopupFilter() { + return cy.get('.datagrid-filter.clr-popover-content'); + } + + getDataGridInputFilter() { + return this.getDataGridPopupFilter() + .should('exist') + .find('input'); + } + + getDataGridPopupFilterCloseBtn() { + return this.getDataGridPopupFilter() + .should('exist') + .find('.close'); + } + + getDataGridSpinner() { + return this.getDataGrid() + .should('exist') + .find('.datagrid-spinner'); + } + + // Header + + getDataGridExecStatusHeader() { + return this.getDataGrid() + .should('exist') + .find('[data-cy=data-pipelines-job-executions-status-header]'); + } + + getDataGridExecStatusFilterOpenerBtn() { + return this.getDataGridExecStatusHeader() + .should('exist') + .find('.datagrid-filter-toggle'); + } + + getDataGridExecStatusFilters() { + return this.getDataGridPopupFilter() + .should('exist') + .find('[data-cy=data-pipelines-job-executions-status-filters]'); + } + + getDataGridExecTypeHeader() { + return this.getDataGrid() + .should('exist') + .find('[data-cy=data-pipelines-job-executions-type-header]'); + } + + getDataGridExecTypeFilterOpenerBtn() { + return this.getDataGridExecTypeHeader() + .should('exist') + .find('.datagrid-filter-toggle'); + } + + getDataGridExecTypeFilters() { + return this.getDataGridPopupFilter() + .should('exist') + .find('[data-cy=data-pipelines-job-executions-type-filters]'); + } + + getDataGridExecDurationHeader() { + return this.getDataGrid() + .should('exist') + .find('[data-cy=data-pipelines-job-executions-duration-header]'); + } + + getDataGridExecDurationSortBtn() { + return this.getDataGridExecDurationHeader() + .should('exist') + .find('.datagrid-column-title'); + } + + getDataGridExecStartHeader() { + return this.getDataGrid() + .should('exist') + .find('[data-cy=data-pipelines-job-executions-start-header]'); + } + + getDataGridExecStartSortBtn() { + return this.getDataGridExecStartHeader() + .should('exist') + .find('.datagrid-column-title'); + } + + getDataGridExecEndHeader() { + return this.getDataGrid() + .should('exist') + .find('[data-cy=data-pipelines-job-executions-end-header]'); + } + + getDataGridExecEndSortBtn() { + return this.getDataGridExecEndHeader() + .should('exist') + .find('.datagrid-column-title'); + } + + getDataGridExecIDHeader() { + return this.getDataGrid() + .should('exist') + .find('[data-cy=data-pipelines-job-executions-id-header]'); + } + + getDataGridExecIDFilterOpenerBtn() { + return this.getDataGridExecIDHeader() + .should('exist') + .find('.datagrid-filter-toggle'); + } + + getDataGridExecVersionHeader() { + return this.getDataGrid() + .should('exist') + .find('[data-cy=data-pipelines-job-executions-version-header]'); + } + + getDataGridExecVersionFilterOpenerBtn() { + return this.getDataGridExecVersionHeader() + .should('exist') + .find('.datagrid-filter-toggle'); + } + + // Rows and Cells + + getDataGridRows() { + return this.getDataGrid() + .should('exist') + .find('clr-dg-row.datagrid-row'); + } + + getDataGridRow(rowIndex) { + return this.getDataGridRows() + .should('have.length.gte', rowIndex - 1) + .then((rows) => Array.from(rows)[rowIndex - 1]); + } + + getDataGridCells(rowIndex) { + return this.getDataGridRow(rowIndex) + .should('exist') + .find('clr-dg-cell.datagrid-cell'); + } + + getDataGridCellByIndex(rowIndex, cellIndex) { + if (rowIndex) { + return this.getDataGridCells(rowIndex) + .should('have.length.gte', cellIndex - 1) + .then((cells) => Array.from(cells)[cellIndex - 1]); + } + + return this.getDataGridRows() + .should('have.length.gte', 0) + .then(($rows) => { + const _rows = $rows.reduce((accumulator, row) => { + const _cells = Array.from(row.querySelectorAll('clr-dg-cell.datagrid-cell')); + + if (_cells[cellIndex - 1]) { + accumulator.push(_cells[cellIndex - 1]); + } + + return accumulator; + }, []); + + return _rows; + }) + } + + getDataGridCellByIdentifier(rowIndex, identifier) { + return this.getDataGridRow(rowIndex) + .should('exist') + .find(identifier) + } + + getDataGridExecTypeCell(rowIndex) { + return this.getDataGridCellByIdentifier(rowIndex, '[data-cy=data-pipelines-job-executions-type-cell]'); + } + + getDataGridExecTypeContainer(rowIndex) { + return this.getDataGridExecTypeCell(rowIndex) + .should('exist') + .find('[data-cy=data-pipelines-job-executions-type-container]'); + } + + getDataGridExecDurationCell(rowIndex) { + return this.getDataGridCellByIdentifier(rowIndex, '[data-cy=data-pipelines-job-executions-duration-cell]'); + } + + getDataGridExecStartCell(rowIndex) { + return this.getDataGridCellByIdentifier(rowIndex, '[data-cy=data-pipelines-job-executions-start-cell]'); + } + + getDataGridExecEndCell(rowIndex) { + return this.getDataGridCellByIdentifier(rowIndex, '[data-cy=data-pipelines-job-executions-end-cell]'); + } + + // Pagination + + getDataGridPagination() { + return this.getDataGrid() + .should('exist') + .find('[data-cy=data-pipelines-job-executions-datagrid-pagination]'); + } + + /* Actions */ + + // General + + refreshExecData() { + this.getExecRefreshBtn() + .should('exist') + .click({ force: true }) + } + + // DataGrid + + typeToFilterInput(value) { + this.getDataGridInputFilter() + .should('exist') + .type(value); + + this.waitForViewToRenderShort(); + } + + clearFilterInput() { + this.getDataGridInputFilter() + .should('exist') + .clear({ force: true }); + + this.waitForViewToRenderShort(); + } + + closeFilter() { + this.getDataGridPopupFilterCloseBtn() + .should('exist') + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + // Header + + openStatusFilter() { + this.getDataGridExecStatusFilterOpenerBtn() + .should('exist') + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + openTypeFilter() { + this.getDataGridExecTypeFilterOpenerBtn() + .should('exist') + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + openIDFilter() { + this.getDataGridExecIDFilterOpenerBtn() + .should('exist') + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + openVersionFilter() { + this.getDataGridExecVersionFilterOpenerBtn() + .should('exist') + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + sortByExecDuration() { + this.getDataGridExecDurationSortBtn() + .should('exist') + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + sortByExecStart() { + this.getDataGridExecStartSortBtn() + .should('exist') + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + sortByExecEnd() { + this.getDataGridExecEndSortBtn() + .should('exist') + .click({ force: true }); + + this.waitForViewToRenderShort(); + } +} diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/manage/data-jobs.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/manage/data-jobs.po.js new file mode 100644 index 0000000000..1de29752f5 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/app/lib/manage/data-jobs.po.js @@ -0,0 +1,208 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataJobsBasePO } from '../../../../application/data-jobs-base.po'; + +export class DataJobsManagePage extends DataJobsBasePO { + static getPage() { + return new DataJobsManagePage(); + } + + static navigateTo() { + return super.navigateTo('[data-cy=navigation-link-manage-datajobs]'); + } + + getPageTitle() { + return cy.get('[data-cy=data-pipelines-manage-page-main-title]'); + } + + getDataGrid() { + return cy.get('[data-cy=data-pipelines-manage-grid]'); + } + + getDataGridCell(content, timeout) { + return cy.get('[id^="clr-dg-row"] > .datagrid-row-scrollable > .datagrid-scrolling-cells > .ng-star-inserted') + .contains(new RegExp(`^\\s*${ content }\\s*$`), { timeout: this.resolveTimeout(timeout) }); + } + + getDataGridRow(jobName) { + cy.log(`Looking for Job row: ${ jobName }`); + + return this.getDataGridCell(jobName) + .parents('clr-dg-row'); + } + + getDataGridNavigateBtn(team, job) { + return cy.get('[data-cy=data-pipelines-manage-grid-details-link][data-job-params="' + team + ';' + job + '"]'); + } + + getDataGridSearchInput() { + return cy.get('[data-test-id=search-input]') + .first(); + } + + getDataGridClearSearchButton() { + return cy.get('[data-test-id="clear-search-btn"]').first(); + } + + searchByJobName(jobName) { + this.getDataGridSearchInput() + .type(jobName); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + clearSearchField() { + this.getDataGridSearchInput() + .should('be.visible') + .clear(); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + clearSearchFieldWithButton() { + this.getDataGridClearSearchButton() + .should('be.visible') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + refreshDataGrid() { + cy.get('[data-cy=data-pipelines-manage-refresh-btn]') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRender(); + } + + openJobDetails(teamName, jobName) { + this.getDataGridNavigateBtn(teamName, jobName) + .scrollIntoView() + .should('exist') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRenderShort(); + + cy.get('[data-cy=data-pipelines-job-page]') + .should('be.visible'); + } + + selectRow(jobName) { + this.getDataGridRow(jobName) + .find('.datagrid-select input') + .check({ force: true }); + } + + getExecuteNowGridButton() { + return cy.get('[data-cy=data-pipelines-manage-grid-execute-btn]'); + } + + changeStatus(newStatus) { + cy.get(`[data-cy=data-pipelines-job-${ newStatus }-btn]`) + .should('exist') + .should('be.enabled') + .click({ force: true }); + } + + getJobStatus(jobName) { + return this.getDataGridRow(jobName) + .then(($row) => { + if ($row.find('[data-cy=data-pipelines-job-disabled]').length) { + return 'disable'; + } + + if ($row.find('[data-cy=data-pipelines-job-enabled]').length) { + return 'enable'; + } + + return 'not_deployed'; + }); + } + + toggleJobStatus(jobName) { + this.selectRow(jobName); + + this.getJobStatus(jobName) + .then((currentStatus) => { + if (currentStatus === 'not_deployed') { + throw new Error('Data job is not Deployed.'); + } + + const newStatus = currentStatus === 'enable' + ? 'disable' + : 'enable'; + + cy.log(`Current status: ${ currentStatus }, new status: ${ newStatus }`); + + this.changeStatus(newStatus) + + this.confirmInConfirmDialog(); + + this.getToastTitle() + .should('exist') + .should('contain.text', 'Status update completed'); + + this.getToastDismiss() + .should('exist') + .click({ force: true }); + + this.waitForClickThinkingTime(); // Natural wait for User action + + this.refreshDataGrid(); + + this.getJobStatus(jobName) + .then((changedStatus) => { + expect(changedStatus).to.equal(newStatus); + }); + }); + } + + filterByJobName(jobName) { + cy.get('[data-cy=data-pipelines-jobs-name-column] > .datagrid-column-flex > clr-dg-string-filter.ng-star-inserted > clr-dg-filter > .datagrid-filter-toggle > cds-icon') + .click({ force: true }); + + cy.get('div.datagrid-filter > input') + .should('be.visible') + .type(jobName); + + this.getPageTitle() + .should('be.visible') + .click({ force: true }); + + this.waitForBackendRequestCompletion(); + + this.waitForViewToRenderShort(); + } + + executeDataJob(jobName) { + this.selectRow(jobName); + + this.waitForClickThinkingTime(); + + this.getExecuteNowGridButton() + .should('exist') + .click({ force: true }); + + this.waitForViewToRenderShort(); + + this.confirmInConfirmDialog(); + } + + prepareAdditionalTestJob() { + cy.prepareAdditionalTestJobs(); + + this.waitForApiModifyCall(); + } +}