diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts index 00ada6a171..083a8f16ed 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts @@ -16,6 +16,7 @@ import { pipelineDeleteModal, configurePipelineServerModal, viewPipelineServerModal, + PipelineSort, } from '~/__tests__/cypress/cypress/pages/pipelines'; import { verifyRelativeURL } from '~/__tests__/cypress/cypress/utils/url'; import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; @@ -28,7 +29,6 @@ import { import { asProductAdminUser } from '~/__tests__/cypress/cypress/utils/users'; import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; import { PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; -import { be } from '~/__tests__/cypress/cypress/utils/should'; import { tablePagination } from '~/__tests__/cypress/cypress/pages/components/Pagination'; import { mockSuccessGoogleRpcStatus } from '~/__mocks__/mockGoogleRpcStatusKF'; @@ -363,32 +363,8 @@ describe('Pipelines', () => { }); it('View pipeline server', () => { - initIntercepts({}); - cy.interceptK8s( - { - model: SecretModel, - ns: projectName, - }, - mockSecretK8sResource({ - s3Bucket: 'c2RzZA==', - namespace: projectName, - name: 'aws-connection-test', - }), - ); - - pipelinesGlobal.visit(projectName); - - pipelinesGlobal.selectPipelineServerAction('View pipeline server configuration'); - viewPipelineServerModal.findCloseButton().click(); - - pipelinesGlobal.selectPipelineServerAction('View pipeline server configuration'); - viewPipelineServerModal.shouldHaveAccessKey('sdsd'); - viewPipelineServerModal.findPasswordHiddenButton().click(); - viewPipelineServerModal.shouldHaveSecretKey('sdsd'); - viewPipelineServerModal.shouldHaveEndPoint('https://s3.amazonaws.com'); - viewPipelineServerModal.shouldHaveBucketName('test-pipelines-bucket'); - - viewPipelineServerModal.findDoneButton().click(); + const visitPipelineProjects = () => pipelinesGlobal.visit(projectName); + viewPipelineServerDetailsTest(visitPipelineProjects); }); it('renders the page with pipelines table data', () => { @@ -483,17 +459,22 @@ describe('Pipelines', () => { initIntercepts({}); pipelinesGlobal.visit(projectName); - // by Pipeline - pipelinesTable.findTableHeaderButton('Pipeline').click(); - pipelinesTable.findTableHeaderButton('Pipeline').should(be.sortAscending); - pipelinesTable.findTableHeaderButton('Pipeline').click(); - pipelinesTable.findTableHeaderButton('Pipeline').should(be.sortDescending); - - // by Created - pipelinesTable.findTableHeaderButton('Created').click(); - pipelinesTable.findTableHeaderButton('Created').should(be.sortAscending); - pipelinesTable.findTableHeaderButton('Created').click(); - pipelinesTable.findTableHeaderButton('Created').should(be.sortDescending); + pipelinesTable.shouldSortTable({ + sortType: PipelineSort.All, + pipelines: [ + buildMockPipelineV2({ + display_name: 'Test pipeline 1', + pipeline_id: 'test-pipeline-1', + created_at: '2023-01-30T22:55:17Z', + }), + buildMockPipelineV2({ + display_name: 'Test pipeline 2', + pipeline_id: 'test-pipeline-2', + created_at: '2024-01-30T22:55:17Z', + }), + ], + projectName, + }); }); }); @@ -945,29 +926,13 @@ describe('Pipelines', () => { }); it('navigate to create run page from pipeline row', () => { - initIntercepts({}); - pipelinesGlobal.visit(projectName); - - // Wait for the pipelines table to load - pipelinesTable.find(); - pipelinesTable - .getRowById(initialMockPipeline.pipeline_id) - .findKebabAction('Create run') - .click(); - verifyRelativeURL(`/pipelines/${projectName}/pipelineRun/create`); + const visitPipelineProjects = () => pipelinesGlobal.visit(projectName); + runCreateRunPageNavTest(visitPipelineProjects); }); it('navigates to "Schedule run" page from pipeline row', () => { - initIntercepts({}); - pipelinesGlobal.visit(projectName); - - pipelinesTable.find(); - pipelinesTable - .getRowById(initialMockPipeline.pipeline_id) - .findKebabAction('Schedule run') - .click(); - - verifyRelativeURL(`/pipelines/${projectName}/pipelineRun/create?runType=scheduled`); + const visitPipelineProjects = () => pipelinesGlobal.visit(projectName); + runScheduleRunPageNavTest(visitPipelineProjects); }); it('navigate to create run page from pipeline version row', () => { @@ -1119,12 +1084,12 @@ type HandlersProps = { nextPageToken?: string | undefined; }; -const initIntercepts = ({ +export const initIntercepts = ({ isEmpty = false, mockPipelines = [initialMockPipeline], totalSize = mockPipelines.length, nextPageToken, -}: HandlersProps) => { +}: HandlersProps): void => { cy.interceptK8sList( DataSciencePipelineApplicationModel, mockK8sResourceList( @@ -1193,3 +1158,59 @@ const createDeletePipelineIntercept = (pipelineId: string) => }, mockSuccessGoogleRpcStatus({}), ); + +export const runCreateRunPageNavTest = (visitPipelineProjects: () => void): void => { + initIntercepts({}); + visitPipelineProjects(); + + // Wait for the pipelines table to load + pipelinesTable.find(); + pipelinesTable.getRowById(initialMockPipeline.pipeline_id).findKebabAction('Create run').click(); + verifyRelativeURL(`/pipelines/${projectName}/pipelineRun/create`); +}; + +export const runScheduleRunPageNavTest = (visitPipelineProjects: () => void): void => { + initIntercepts({}); + visitPipelineProjects(); + + pipelinesTable.find(); + pipelinesTable + .getRowById(initialMockPipeline.pipeline_id) + .findKebabAction('Schedule run') + .click(); + + verifyRelativeURL(`/pipelines/${projectName}/pipelineRun/create?runType=scheduled`); +}; + +export const viewPipelineServerDetailsTest = (visitPipelineProjects: () => void): void => { + initIntercepts({}); + cy.interceptK8s( + { + model: SecretModel, + ns: projectName, + }, + mockSecretK8sResource({ + s3Bucket: 'c2RzZA==', + namespace: projectName, + name: 'aws-connection-test', + }), + ); + visitPipelineProjects(); + viewPipelinelineDetails(); +}; + +const viewPipelinelineDetails = ( + accessKey = 'sdsd', + secretKey = 'sdsd', + endpoint = 'https://s3.amazonaws.com', + bucketName = 'test-pipelines-bucket', +) => { + pipelinesGlobal.selectPipelineServerAction('View pipeline server configuration'); + viewPipelineServerModal.shouldHaveAccessKey(accessKey); + viewPipelineServerModal.findPasswordHiddenButton().click(); + viewPipelineServerModal.shouldHaveSecretKey(secretKey); + viewPipelineServerModal.shouldHaveEndPoint(endpoint); + viewPipelineServerModal.shouldHaveBucketName(bucketName); + + viewPipelineServerModal.findDoneButton().click(); +}; diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts index 1f67aa9b6e..dd46cdf695 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts @@ -1,63 +1,54 @@ /* eslint-disable camelcase */ -import { buildMockPipelineVersionV2, buildMockPipelineVersionsV2 } from '~/__mocks__'; -import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; +import { buildMockPipelineVersionV2 } from '~/__mocks__'; import { mockDataSciencePipelineApplicationK8sResource } from '~/__mocks__/mockDataSciencePipelinesApplicationK8sResource'; -import { mockDscStatus } from '~/__mocks__/mockDscStatus'; import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; import { mock404Error } from '~/__mocks__/mockK8sStatus'; -import { mockNotebookK8sResource } from '~/__mocks__/mockNotebookK8sResource'; -import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource'; import { buildMockPipelineV2, buildMockPipelines } from '~/__mocks__/mockPipelinesProxy'; -import { mockPodK8sResource } from '~/__mocks__/mockPodK8sResource'; -import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; -import { mockRouteK8sResource } from '~/__mocks__/mockRouteK8sResource'; -import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; -import { pipelinesTable } from '~/__tests__/cypress/cypress/pages/pipelines'; +import { + pipelinesTable, + configurePipelineServerModal, + pipelineVersionImportModal, + PipelineSort, +} from '~/__tests__/cypress/cypress/pages/pipelines'; import { pipelinesSection } from '~/__tests__/cypress/cypress/pages/pipelines/pipelinesSection'; import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; import { verifyRelativeURL } from '~/__tests__/cypress/cypress/utils/url'; +import { DataSciencePipelineApplicationModel } from '~/__tests__/cypress/cypress/utils/models'; +import { PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; import { - DataSciencePipelineApplicationModel, - PVCModel, - PodModel, - ProjectModel, - RouteModel, - SecretModel, -} from '~/__tests__/cypress/cypress/utils/models'; + initIntercepts, + runCreateRunPageNavTest, + runScheduleRunPageNavTest, + viewPipelineServerDetailsTest, +} from './Pipelines.cy'; +const projectName = 'test-project-name'; const initialMockPipeline = buildMockPipelineV2({ display_name: 'Test pipeline' }); const initialMockPipelineVersion = buildMockPipelineVersionV2({ pipeline_id: initialMockPipeline.pipeline_id, }); -const initIntercepts = () => { - cy.interceptOdh( - 'GET /api/dsc/status', - mockDscStatus({ installedComponents: { 'data-science-pipelines-operator': true } }), - ); - cy.interceptOdh('GET /api/config', mockDashboardConfig({ disableModelServing: true })); - cy.interceptK8sList(PodModel, mockK8sResourceList([mockPodK8sResource({})])); - cy.interceptK8s(RouteModel, mockRouteK8sResource({})); - cy.interceptK8sList(RouteModel, mockK8sResourceList([mockNotebookK8sResource({})])); - cy.interceptK8sList(ProjectModel, mockK8sResourceList([mockProjectK8sResource({})])); - cy.interceptK8sList(PVCModel, mockK8sResourceList([mockPVCK8sResource({})])); - cy.interceptK8s(ProjectModel, mockProjectK8sResource({})); - cy.interceptK8sList(SecretModel, mockK8sResourceList([mockSecretK8sResource({})])); - cy.interceptK8s( - RouteModel, - mockRouteK8sResource({ - notebookName: 'ds-pipeline-dspa', - }), - ); -}; +const mockPipelines: PipelineKFv2[] = [ + buildMockPipelineV2({ + display_name: 'Test pipeline 1', + pipeline_id: 'test-pipeline-1', + created_at: '2023-01-30T22:55:17Z', + }), + + buildMockPipelineV2({ + display_name: 'Test pipeline 2', + pipeline_id: 'test-pipeline-2', + created_at: '2024-01-30T22:55:17Z', + }), +]; describe('PipelinesList', () => { it('should show the configure pipeline server button when the server is not configured', () => { - initIntercepts(); + initIntercepts({ isEmpty: true }); cy.interceptK8s( { model: DataSciencePipelineApplicationModel, - ns: 'test-project', + ns: projectName, name: 'pipelines-definition', }, { @@ -66,13 +57,27 @@ describe('PipelinesList', () => { }, ); - projectDetails.visitSection('test-project', 'pipelines-projects'); + projectDetails.visitSection(projectName, 'pipelines-projects'); pipelinesSection.findCreatePipelineButton().should('be.enabled'); }); + it('should verify that clicking on Configure pipeline server button will open a modal', () => { + initIntercepts({ isEmpty: true }); + projectDetails.visitSection(projectName, 'pipelines-projects'); + pipelinesSection.findCreatePipelineButton().should('be.enabled'); + pipelinesSection.findCreatePipelineButton().click(); + configurePipelineServerModal.findSubmitButton().should('exist'); + }); + + it('should view pipeline server', () => { + const visitPipelineProjects = () => + projectDetails.visitSection(projectName, 'pipelines-projects'); + viewPipelineServerDetailsTest(visitPipelineProjects); + }); + it('should disable the upload version button when the list is empty', () => { - initIntercepts(); + initIntercepts({}); cy.interceptK8sList( DataSciencePipelineApplicationModel, mockK8sResourceList([mockDataSciencePipelineApplicationK8sResource({})]), @@ -84,12 +89,12 @@ describe('PipelinesList', () => { cy.interceptOdh( 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/pipelines', { - path: { namespace: 'test-project', serviceName: 'dspa' }, + path: { namespace: projectName, serviceName: 'dspa' }, }, buildMockPipelines([]), ).as('pipelines'); - projectDetails.visitSection('test-project', 'pipelines-projects'); + projectDetails.visitSection(projectName, 'pipelines-projects'); pipelinesSection.findImportPipelineSplitButton().should('be.enabled').click(); @@ -104,52 +109,27 @@ describe('PipelinesList', () => { }); it('should show the ability to delete the pipeline server kebab option', () => { - initIntercepts(); - cy.interceptK8sList( - DataSciencePipelineApplicationModel, - mockK8sResourceList([mockDataSciencePipelineApplicationK8sResource({ dspVersion: 'v1' })]), - ); - cy.interceptK8s( - DataSciencePipelineApplicationModel, - mockDataSciencePipelineApplicationK8sResource({ dspVersion: 'v1' }), - ); - projectDetails.visitSection('test-project', 'pipelines-projects'); + initIntercepts({}); + + projectDetails.visitSection(projectName, 'pipelines-projects'); - pipelinesSection.findAllActions().should('have.length', 1); - pipelinesSection.findImportPipelineSplitButton().should('not.exist'); pipelinesSection.findKebabActions().should('be.visible').should('be.enabled'); pipelinesSection.findKebabActionItem('Delete pipeline server').should('be.visible'); }); + it('should show the ability to upload new version when clicking the pipeline server kebab option', () => { + initIntercepts({}); + + projectDetails.visitSection(projectName, 'pipelines-projects'); + pipelinesTable.find(); + const pipelineRow = pipelinesTable.getRowById(initialMockPipeline.pipeline_id); + pipelineRow.findKebabAction('Upload new version').should('be.visible').click(); + pipelineVersionImportModal.shouldBeOpen(); + }); + it('should navigate to details page when clicking on the version name', () => { - initIntercepts(); - cy.interceptK8sList( - DataSciencePipelineApplicationModel, - mockK8sResourceList([mockDataSciencePipelineApplicationK8sResource({})]), - ); - cy.interceptK8s( - DataSciencePipelineApplicationModel, - mockDataSciencePipelineApplicationK8sResource({}), - ); - cy.interceptOdh( - 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/pipelines', - { - path: { namespace: 'test-project', serviceName: 'dspa' }, - }, - buildMockPipelines([initialMockPipeline]), - ); - cy.interceptOdh( - 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/pipelines/:pipelineId/versions', - { - path: { - namespace: 'test-project', - serviceName: 'dspa', - pipelineId: initialMockPipeline.pipeline_id, - }, - }, - buildMockPipelineVersionsV2([initialMockPipelineVersion]), - ); - projectDetails.visitSection('test-project', 'pipelines-projects'); + initIntercepts({}); + projectDetails.visitSection(projectName, 'pipelines-projects'); pipelinesTable.find(); const pipelineRow = pipelinesTable.getRowById(initialMockPipeline.pipeline_id); @@ -159,7 +139,53 @@ describe('PipelinesList', () => { .findPipelineVersionLink() .click(); verifyRelativeURL( - `/projects/test-project/pipeline/view/${initialMockPipeline.pipeline_id}/${initialMockPipelineVersion.pipeline_version_id}`, + `/projects/${projectName}/pipeline/view/${initialMockPipeline.pipeline_id}/${initialMockPipelineVersion.pipeline_version_id}`, ); }); + + it('clicking on upload a new pipeline version should show a modal', () => { + initIntercepts({}); + cy.intercept( + { + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, + }, + buildMockPipelines([initialMockPipelineVersion]), + ); + projectDetails.visitSection(projectName, 'pipelines-projects'); + cy.intercept( + { + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, + }, + buildMockPipelines(mockPipelines), + ); + + pipelinesSection.findImportPipelineSplitButton().click(); + pipelinesSection.findUploadVersionButton().click(); + pipelineVersionImportModal.shouldBeOpen(); + }); + + it('should sort the table', () => { + pipelineTableSetup(); + pipelinesTable.shouldSortTable({ + sortType: PipelineSort.All, + pipelines: mockPipelines, + }); + }); + + it('navigates to create run page from pipeline row', () => { + const visitPipelineProjects = () => + projectDetails.visitSection(projectName, 'pipelines-projects'); + runCreateRunPageNavTest(visitPipelineProjects); + }); + + it('navigates to "Schedule run" page from pipeline row', () => { + const visitPipelineProjects = () => + projectDetails.visitSection(projectName, 'pipelines-projects'); + runScheduleRunPageNavTest(visitPipelineProjects); + }); }); + +const pipelineTableSetup = () => { + initIntercepts({ mockPipelines }); + projectDetails.visitSection(projectName, 'pipelines-projects'); +}; diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts index 6a307536ad..b6616269ed 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts @@ -3,6 +3,7 @@ import { PipelineKFv2, PipelineVersionKFv2 } from '~/concepts/pipelines/kfTypes' import { buildMockPipelines } from '~/__mocks__/mockPipelinesProxy'; import { buildMockPipelineVersionsV2 } from '~/__mocks__/mockPipelineVersionsProxy'; import { TableRow } from '~/__tests__/cypress/cypress/pages/components/table'; +import { be } from '~/__tests__/cypress/cypress/utils/should'; class PipelinesTableRow extends TableRow { findPipelineVersionsTable() { @@ -31,9 +32,119 @@ class PipelineVersionsTableRow extends TableRow { } } +export enum PipelineSort { + PipelineAsc = 'PIPELINE_ASC', + PipelineDesc = 'PIPELINE_DESC', + Pipeline = 'PIPELINE_SORT', + CreatedAsc = 'CREATED_ASC', + CreatedDesc = 'CREATED_DESC', + Created = 'CREATED_SORT', + All = 'SORT_ALL', +} + class PipelinesTable { private testId = 'pipelines-table'; + shouldSortTable({ + sortType, + pipelines, + projectName = 'test-project-name', + }: { + sortType: PipelineSort; + pipelines: PipelineKFv2[]; + projectName?: string; + }): void { + const sortPipelineByAscending = (): void => { + cy.interceptOdh( + 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/pipelines', + { + path: { namespace: projectName, serviceName: 'dspa' }, + query: { sort_by: 'name asc' }, + }, + buildMockPipelines(pipelines), + ).as('asc'); + this.findTableHeaderButton('Pipeline').click(); + this.findTableHeaderButton('Pipeline').should(be.sortAscending); + cy.wait('@asc'); + }; + + const sortPipelineByDescending = (): void => { + cy.interceptOdh( + 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/pipelines', + { + path: { namespace: projectName, serviceName: 'dspa' }, + query: { sort_by: 'name desc' }, + }, + buildMockPipelines(pipelines), + ).as('desc'); + this.findTableHeaderButton('Pipeline').click(); + this.findTableHeaderButton('Pipeline').should(be.sortDescending); + cy.wait('@desc'); + }; + + const sortPipelineCreationByAscending = (): void => { + cy.interceptOdh( + 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/pipelines', + { + path: { namespace: projectName, serviceName: 'dspa' }, + query: { sort_by: 'created_at asc' }, + }, + buildMockPipelines(pipelines), + ).as('asc'); + this.findTableHeaderButton('Created').click(); + this.findTableHeaderButton('Created').should(be.sortAscending); + cy.wait('@asc'); + }; + + const sortPipelineCreationByDescending = (): void => { + cy.interceptOdh( + 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/pipelines', + { + path: { namespace: projectName, serviceName: 'dspa' }, + query: { sort_by: 'created_at desc' }, + }, + buildMockPipelines(pipelines), + ).as('desc'); + + this.findTableHeaderButton('Created').click(); + this.findTableHeaderButton('Created').should(be.sortDescending); + cy.wait('@desc'); + }; + + switch (sortType) { + case PipelineSort.PipelineAsc: + sortPipelineByAscending(); + break; + case PipelineSort.PipelineDesc: + this.findTableHeaderButton('Pipeline').click(); + sortPipelineByDescending(); + break; + case PipelineSort.Pipeline: + sortPipelineByAscending(); + sortPipelineByDescending(); + break; + case PipelineSort.CreatedAsc: + sortPipelineCreationByAscending(); + break; + case PipelineSort.CreatedDesc: + this.findTableHeaderButton('Created').click(); + sortPipelineCreationByDescending(); + break; + case PipelineSort.Created: + sortPipelineCreationByAscending(); + sortPipelineCreationByDescending(); + break; + case PipelineSort.All: + sortPipelineByAscending(); + sortPipelineByDescending(); + sortPipelineCreationByAscending(); + sortPipelineCreationByDescending(); + break; + default: + throw new Error('Invalid invocation of shouldSortTable() method'); + } + } + find() { return cy.findByTestId(this.testId); } diff --git a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts index 478386288e..eec62b873a 100644 --- a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts +++ b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts @@ -421,7 +421,10 @@ declare global { interceptOdh( type: `GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/pipelines`, - options: { path: { namespace: string; serviceName: string } }, + options: { + path: { namespace: string; serviceName: string }; + query?: { sort_by: string }; + }, response: OdhResponse, ): Cypress.Chainable;