diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/IncidentManager.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/IncidentManager.spec.ts deleted file mode 100644 index 09e32610f07..00000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/IncidentManager.spec.ts +++ /dev/null @@ -1,538 +0,0 @@ -/* - * Copyright 2024 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { interceptURL, verifyResponseStatusCode } from '../../common/common'; -import { triggerTestCasePipeline } from '../../common/Utils/DataQuality'; -import { - createEntityTableViaREST, - deleteEntityViaREST, - visitEntityDetailsPage, -} from '../../common/Utils/Entity'; -import { getToken } from '../../common/Utils/LocalStorage'; -import { generateRandomUser } from '../../common/Utils/Owner'; -import { uuid } from '../../constants/constants'; -import { EntityType, SidebarItem } from '../../constants/Entity.interface'; -import { DATABASE_SERVICE } from '../../constants/EntityConstant'; -const TABLE_NAME = DATABASE_SERVICE.entity.name; - -const testSuite = { - name: `${DATABASE_SERVICE.entity.databaseSchema}.${DATABASE_SERVICE.entity.name}.testSuite`, - executableEntityReference: `${DATABASE_SERVICE.entity.databaseSchema}.${DATABASE_SERVICE.entity.name}`, -}; - -const testCases = [ - `cy_first_table_column_count_to_be_between_${uuid()}`, - `cy_second_table_column_count_to_be_between_${uuid()}`, - `cy_third_table_column_count_to_be_between_${uuid()}`, -]; -const user1 = generateRandomUser(); -const user2 = generateRandomUser(); -const user3 = generateRandomUser(); -const userData1 = { - displayName: `${user1.firstName}${user1.lastName}`, - name: user1.email.split('@')[0], -}; -const userData2 = { - displayName: `${user2.firstName}${user2.lastName}`, - name: user2.email.split('@')[0], -}; -const userData3 = { - displayName: `${user3.firstName}${user3.lastName}`, - name: user3.email.split('@')[0], -}; -const userIds: string[] = []; - -const goToProfilerTab = () => { - interceptURL( - 'GET', - `api/v1/tables/name/${DATABASE_SERVICE.service.name}.*.${TABLE_NAME}?fields=*&include=all`, - 'waitForPageLoad' - ); - visitEntityDetailsPage({ - term: TABLE_NAME, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - }); - verifyResponseStatusCode('@waitForPageLoad', 200); - - cy.get('[data-testid="profiler"]').should('be.visible').click(); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Table Profile') - .click(); -}; - -const acknowledgeTask = (testCase: string) => { - goToProfilerTab(); - - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - cy.get(`[data-testid="${testCase}"]`) - .find('.last-run-box.failed') - .scrollIntoView() - .should('be.visible'); - cy.get(`[data-testid="${testCase}-status"]`).should('contain', 'New'); - cy.get(`[data-testid="${testCase}"]`).contains(testCase).click(); - cy.get('[data-testid="edit-resolution-icon"]').click(); - cy.get('[data-testid="test-case-resolution-status-type"]').click(); - cy.get('[title="Ack"]').click(); - interceptURL( - 'POST', - '/api/v1/dataQuality/testCases/testCaseIncidentStatus', - 'updateTestCaseIncidentStatus' - ); - cy.get('#update-status-button').click(); - verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200); - cy.get(`[data-testid="${testCase}-status"]`).should('contain', 'Ack'); -}; - -const assignIncident = (testCaseName: string) => { - cy.sidebarClick(SidebarItem.INCIDENT_MANAGER); - cy.get(`[data-testid="test-case-${testCaseName}"]`).should('be.visible'); - cy.get(`[data-testid="${testCaseName}-status"]`) - .find(`[data-testid="edit-resolution-icon"]`) - .click(); - cy.get(`[data-testid="test-case-resolution-status-type"]`).click(); - cy.get(`[title="Assigned"]`).click(); - cy.get('#testCaseResolutionStatusDetails_assignee').should('be.visible'); - interceptURL( - 'GET', - `/api/v1/search/suggest?q=*${user1.firstName}*${user1.lastName}*&index=user_search_index*`, - 'searchAssignee' - ); - interceptURL('GET', '/api/v1/users/name/*', 'userList'); - cy.get('#testCaseResolutionStatusDetails_assignee').click(); - cy.wait('@userList'); - cy.get('#testCaseResolutionStatusDetails_assignee').type( - userData1.displayName - ); - verifyResponseStatusCode('@searchAssignee', 200); - cy.get(`[data-testid="${userData1.name.toLocaleLowerCase()}"]`).click(); - interceptURL( - 'POST', - '/api/v1/dataQuality/testCases/testCaseIncidentStatus', - 'updateTestCaseIncidentStatus' - ); - cy.get('#update-status-button').click(); - verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200); - cy.get( - `[data-testid="${testCaseName}-status"] [data-testid="badge-container"]` - ).should('contain', 'Assigned'); -}; - -describe('Incident Manager', { tags: 'Observability' }, () => { - before(() => { - cy.login(); - - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - - // Create a new user - for (const user of [user1, user2, user3]) { - cy.request({ - method: 'POST', - url: `/api/v1/users/signup`, - headers: { Authorization: `Bearer ${token}` }, - body: user, - }).then((response) => { - userIds.push(response.body.id); - }); - } - - createEntityTableViaREST({ - token, - ...DATABASE_SERVICE, - tables: [DATABASE_SERVICE.entity], - }); - // create testSuite - cy.request({ - method: 'POST', - url: `/api/v1/dataQuality/testSuites/executable`, - headers: { Authorization: `Bearer ${token}` }, - body: testSuite, - }).then((testSuiteResponse) => { - // creating test case - - testCases.forEach((testCase) => { - cy.request({ - method: 'POST', - url: `/api/v1/dataQuality/testCases`, - headers: { Authorization: `Bearer ${token}` }, - body: { - name: testCase, - entityLink: `<#E::table::${testSuite.executableEntityReference}>`, - parameterValues: [ - { name: 'minColValue', value: 12 }, - { name: 'maxColValue', value: 24 }, - ], - testDefinition: 'tableColumnCountToBeBetween', - testSuite: testSuite.name, - }, - }); - }); - cy.request({ - method: 'POST', - url: `/api/v1/services/ingestionPipelines`, - headers: { Authorization: `Bearer ${token}` }, - body: { - airflowConfig: {}, - name: `${testSuite.executableEntityReference}_test_suite`, - pipelineType: 'TestSuite', - service: { - id: testSuiteResponse.body.id, - type: 'testSuite', - }, - sourceConfig: { - config: { - type: 'TestSuite', - entityFullyQualifiedName: testSuite.executableEntityReference, - }, - }, - }, - }).then((response) => - cy.request({ - method: 'POST', - url: `/api/v1/services/ingestionPipelines/deploy/${response.body.id}`, - headers: { Authorization: `Bearer ${token}` }, - }) - ); - }); - }); - - triggerTestCasePipeline({ - serviceName: DATABASE_SERVICE.service.name, - tableName: TABLE_NAME, - }); - }); - - after(() => { - cy.login(); - - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - deleteEntityViaREST({ - token, - endPoint: EntityType.DatabaseService, - entityName: DATABASE_SERVICE.service.name, - }); - - // Delete created user - userIds.forEach((userId) => { - cy.request({ - method: 'DELETE', - url: `/api/v1/users/${userId}?hardDelete=true&recursive=false`, - headers: { Authorization: `Bearer ${token}` }, - }); - }); - }); - }); - - describe('Basic Scenario', () => { - const testCaseName = testCases[0]; - - beforeEach(() => { - cy.login(); - }); - - it("Acknowledge table test case's failure", () => { - acknowledgeTask(testCaseName); - }); - - it('Assign incident to user', () => { - assignIncident(testCaseName); - }); - - it('Re-assign incident to user', () => { - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/name/*?fields=*', - 'getTestCase' - ); - interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed'); - cy.sidebarClick(SidebarItem.INCIDENT_MANAGER); - cy.get(`[data-testid="test-case-${testCaseName}"]`).click(); - verifyResponseStatusCode('@getTestCase', 200); - cy.get('[data-testid="incident"]').click(); - verifyResponseStatusCode('@getTaskFeed', 200); - cy.get('[data-testid="task-cta-buttons"] [role="img"]') - .scrollIntoView() - .click(); - cy.get('[role="menu"').find('[data-menu-id*="re-assign"]').click(); - - interceptURL( - 'GET', - `/api/v1/search/suggest?q=*${user2.firstName}*${user2.lastName}*&index=user_search_index*`, - 'searchAssignee' - ); - interceptURL('GET', '/api/v1/users/name/*', 'userList'); - cy.get('[data-testid="select-assignee"]').click(); - cy.wait('@userList'); - cy.get('[data-testid="select-assignee"]').type(userData2.displayName); - verifyResponseStatusCode('@searchAssignee', 200); - cy.get(`[data-testid="${userData2.name.toLocaleLowerCase()}"]`).click(); - - interceptURL( - 'POST', - '/api/v1/dataQuality/testCases/testCaseIncidentStatus', - 'updateTestCaseIncidentStatus' - ); - cy.get('.ant-modal-footer').contains('Submit').click(); - verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200); - // Todo: skipping this for now as its not working from backend - cy.clickOnLogo(); - cy.get('[id*="tab-tasks"]').click(); - cy.get('[data-testid="task-feed-card"]') - .contains(testCaseName) - .scrollIntoView() - .should('be.visible'); - }); - - it("Re-assign incident from test case page's header", () => { - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/name/*?fields=*', - 'getTestCase' - ); - interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed'); - cy.sidebarClick(SidebarItem.INCIDENT_MANAGER); - cy.get(`[data-testid="test-case-${testCaseName}"]`).click(); - verifyResponseStatusCode('@getTestCase', 200); - interceptURL('GET', '/api/v1/users?*', 'getUsers'); - cy.get('[data-testid="assignee"] [data-testid="edit-owner"]').click(); - verifyResponseStatusCode('@getUsers', 200); - cy.get('[data-testid="loader"]').should('not.exist'); - interceptURL('GET', `api/v1/search/query?q=*`, 'searchOwner'); - cy.get('[data-testid="owner-select-users-search-bar"]').type( - userData3.displayName - ); - verifyResponseStatusCode('@searchOwner', 200); - interceptURL( - 'POST', - '/api/v1/dataQuality/testCases/testCaseIncidentStatus', - 'updateTestCaseIncidentStatus' - ); - cy.get(`.ant-popover [title="${userData3.displayName}"]`).click(); - verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200); - cy.get('[data-testid="assignee"] [data-testid="owner-link"]').should( - 'contain', - userData3.displayName - ); - }); - - it('Resolve incident', () => { - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/name/*?fields=*', - 'getTestCase' - ); - interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed'); - cy.sidebarClick(SidebarItem.INCIDENT_MANAGER); - cy.get(`[data-testid="test-case-${testCaseName}"]`).click(); - verifyResponseStatusCode('@getTestCase', 200); - cy.get('[data-testid="incident"]').click(); - verifyResponseStatusCode('@getTaskFeed', 200); - cy.get('[data-testid="task-cta-buttons"]') - .contains('Resolve') - .scrollIntoView() - .click(); - cy.get('#testCaseFailureReason').click(); - cy.get('[title="Missing Data"]').click(); - cy.get('.toastui-editor-md-container > .toastui-editor > .ProseMirror') - .click() - .type('test'); - interceptURL( - 'POST', - '/api/v1/dataQuality/testCases/testCaseIncidentStatus', - 'updateTestCaseIncidentStatus' - ); - cy.get('.ant-modal-footer').contains('Submit').click(); - verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200); - }); - }); - - describe('Resolving incident & re-run pipeline', () => { - const testName = testCases[1]; - - beforeEach(() => { - cy.login(); - }); - - it("Acknowledge table test case's failure", () => { - acknowledgeTask(testName); - }); - - it('Resolve task from incident list page', () => { - goToProfilerTab(); - - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases?fields=*&entityLink=*&includeAllTests=true&limit=*', - 'testCaseList' - ); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - verifyResponseStatusCode('@testCaseList', 200); - cy.get(`[data-testid="${testName}"]`) - .find('.last-run-box.failed') - .scrollIntoView() - .should('be.visible'); - cy.get('.ant-table-row-level-0').should('contain', 'Ack'); - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*', - 'getIncidentList' - ); - cy.sidebarClick(SidebarItem.INCIDENT_MANAGER); - - verifyResponseStatusCode('@getIncidentList', 200); - - cy.get(`[data-testid="test-case-${testName}"]`).should('be.visible'); - cy.get(`[data-testid="${testName}-status"]`) - .find(`[data-testid="edit-resolution-icon"]`) - .click(); - cy.get(`[data-testid="test-case-resolution-status-type"]`).click(); - cy.get(`[title="Resolved"]`).click(); - cy.get('#testCaseResolutionStatusDetails_testCaseFailureReason').click(); - cy.get('[title="Missing Data"]').click(); - cy.get('.toastui-editor-md-container > .toastui-editor > .ProseMirror') - .click() - .type('test'); - interceptURL( - 'POST', - '/api/v1/dataQuality/testCases/testCaseIncidentStatus', - 'updateTestCaseIncidentStatus' - ); - cy.get('.ant-modal-footer').contains('Submit').click(); - verifyResponseStatusCode('@updateTestCaseIncidentStatus', 200); - }); - - it('Task should be closed', () => { - goToProfilerTab(); - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/name/*?fields=*', - 'getTestCase' - ); - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases?fields=*&entityLink=*&includeAllTests=true&limit=*', - 'testCaseList' - ); - interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed'); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - verifyResponseStatusCode('@testCaseList', 200); - cy.get(`[data-testid="${testName}"]`) - .find('.last-run-box.failed') - .scrollIntoView() - .should('be.visible'); - - cy.get(`[data-testid="${testName}"]`).contains(testName).click(); - verifyResponseStatusCode('@getTestCase', 200); - cy.get('[data-testid="incident"]').click(); - verifyResponseStatusCode('@getTaskFeed', 200); - cy.get('[data-testid="closed-task"]').click(); - cy.get('[data-testid="task-feed-card"]').should('be.visible'); - cy.get('[data-testid="task-tab"]').should( - 'contain', - 'Resolved the Task.' - ); - }); - - it('Re-run pipeline', () => { - triggerTestCasePipeline({ - serviceName: DATABASE_SERVICE.service.name, - tableName: TABLE_NAME, - }); - }); - - it('Verify open and closed task', () => { - acknowledgeTask(testName); - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/name/*?fields=*', - 'getTestCase' - ); - interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed'); - cy.reload(); - verifyResponseStatusCode('@getTestCase', 200); - cy.get('[data-testid="incident"]').click(); - verifyResponseStatusCode('@getTaskFeed', 200); - cy.get('[data-testid="open-task"]') - .invoke('text') - .then((text) => { - expect(text.trim()).equal('1 Open'); - }); - cy.get('[data-testid="closed-task"]') - .invoke('text') - .then((text) => { - expect(text.trim()).equal('1 Closed'); - }); - }); - }); - - describe('Rerunning pipeline for an open incident', () => { - const testName = testCases[2]; - - beforeEach(() => { - cy.login(); - }); - - it('Ack incident and verify open task', () => { - acknowledgeTask(testName); - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/name/*?fields=*', - 'getTestCase' - ); - interceptURL('GET', '/api/v1/feed?entityLink=*&type=Task', 'getTaskFeed'); - cy.reload(); - verifyResponseStatusCode('@getTestCase', 200); - cy.get('[data-testid="incident"]').click(); - verifyResponseStatusCode('@getTaskFeed', 200); - cy.get('[data-testid="open-task"]') - .invoke('text') - .then((text) => { - expect(text.trim()).equal('1 Open'); - }); - }); - - it('Assign incident to user', () => { - assignIncident(testName); - }); - - it('Re-run pipeline', () => { - triggerTestCasePipeline({ - serviceName: DATABASE_SERVICE.service.name, - tableName: TABLE_NAME, - }); - }); - - it("Verify incident's status on DQ page", () => { - goToProfilerTab(); - - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - cy.get(`[data-testid="${testName}"]`) - .find('.last-run-box.failed') - .scrollIntoView() - .should('be.visible'); - cy.get(`[data-testid="${testName}-status"]`).should( - 'contain', - 'Assigned' - ); - }); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts new file mode 100644 index 00000000000..c68b49a4752 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/IncidentManager.spec.ts @@ -0,0 +1,394 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import test, { expect } from '@playwright/test'; +import { SidebarItem } from '../../constant/sidebar'; +import { TableClass } from '../../support/entity/TableClass'; +import { UserClass } from '../../support/user/UserClass'; +import { + createNewPage, + descriptionBox, + getApiContext, + redirectToHomePage, +} from '../../utils/common'; +import { + acknowledgeTask, + assignIncident, + triggerTestSuitePipelineAndWaitForSuccess, + visitProfilerTab, +} from '../../utils/incidentManager'; +import { sidebarClick } from '../../utils/sidebar'; + +const user1 = new UserClass(); +const user2 = new UserClass(); +const user3 = new UserClass(); +const users = [user1, user2, user3]; +const table1 = new TableClass(); + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +test.describe.configure({ mode: 'serial' }); + +test.describe('Incident Manager', () => { + test.beforeAll(async ({ browser }) => { + // since we need to poll for the pipeline status, we need to increase the timeout + test.setTimeout(90000); + + const { afterAction, apiContext, page } = await createNewPage(browser); + + const { pipeline } = await table1.createTestSuiteAndPipelines(apiContext); + for (let i = 0; i < 3; i++) { + await table1.createTestCase(apiContext, { + parameterValues: [ + { name: 'minColValue', value: 12 }, + { name: 'maxColValue', value: 24 }, + ], + testDefinition: 'tableColumnCountToBeBetween', + }); + } + await apiContext.post( + `/api/v1/services/ingestionPipelines/deploy/${pipeline.id}` + ); + await triggerTestSuitePipelineAndWaitForSuccess({ + page, + table: table1, + pipeline: { id: pipeline.id }, + apiContext, + }); + + for (const user of users) { + await user.create(apiContext); + } + + await afterAction(); + }); + + test.afterAll(async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + for (const entity of [...users, table1]) { + await entity.delete(apiContext); + } + await afterAction(); + }); + + test.slow(true); + + test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); + }); + + test('Basic Scenario', async ({ page }) => { + const testCase = table1.testCasesResponseData[0]; + const testCaseName = testCase?.['name']; + const assignee = { + name: user1.data.email.split('@')[0], + displayName: user1.getUserName(), + }; + + await test.step("Acknowledge table test case's failure", async () => { + await acknowledgeTask({ + page, + testCase: testCaseName, + table: table1, + }); + }); + + await test.step('Assign incident to user', async () => { + await assignIncident({ + page, + testCaseName, + user: assignee, + }); + }); + + await test.step('Re-assign incident to user', async () => { + const assignee1 = { + name: user2.data.email.split('@')[0], + displayName: user2.getUserName(), + }; + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/name/*?fields=*' + ); + await page.click(`[data-testid="test-case-${testCaseName}"]`); + + await testCaseResponse; + + const incidentDetails = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus/stateId/*' + ); + await page.click('[data-testid="incident"]'); + await incidentDetails; + + await page.getByRole('button', { name: 'down' }).click(); + await page.waitForSelector('role=menuitem[name="Reassign"]', { + state: 'visible', + }); + await page.getByRole('menuitem', { name: 'Reassign' }).click(); + + const searchUserResponse = page.waitForResponse( + `/api/v1/search/suggest?q=*${user2.data.firstName}*${user2.data.lastName}*&index=user_search_index*` + ); + + await page.getByTestId('select-assignee').locator('div').click(); + await page.getByLabel('Assignee:').fill(assignee1.displayName); + await searchUserResponse; + + await page.click(`[data-testid="${assignee1.name.toLocaleLowerCase()}"]`); + const updateAssignee = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus' + ); + await page.getByRole('button', { name: 'Submit' }).click(); + + await updateAssignee; + }); + + await test.step( + "Re-assign incident from test case page's header", + async () => { + const assignee2 = { + name: user3.data.email.split('@')[0], + displayName: user3.getUserName(), + }; + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/name/*?fields=*' + ); + await page.reload(); + + await testCaseResponse; + + const listUserResponse = page.waitForResponse('/api/v1/users?*'); + await page.click('[data-testid="assignee"] [data-testid="edit-owner"]'); + listUserResponse; + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + const searchUserResponse = page.waitForResponse( + '/api/v1/search/query?q=*' + ); + await page.fill( + '[data-testid="owner-select-users-search-bar"]', + assignee2.displayName + ); + await searchUserResponse; + + const updateIncident = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus' + ); + await page.click(`.ant-popover [title="${assignee2.displayName}"]`); + await updateIncident; + + await page.waitForSelector( + '[data-testid="assignee"] [data-testid="owner-link"]' + ); + + await expect( + page.locator('[data-testid="assignee"] [data-testid="owner-link"]') + ).toContainText(assignee2.displayName); + } + ); + + await test.step('Resolve incident', async () => { + await page.click('[data-testid="incident"]'); + await page.getByRole('button', { name: 'Resolve' }).click(); + await page.click('#testCaseFailureReason'); + await page.click('[title="Missing Data"]'); + await page.click(descriptionBox); + await page.fill(descriptionBox, 'test'); + + const updateIncident = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus' + ); + await page.click('.ant-modal-footer >> text=Submit'); + await updateIncident; + }); + }); + + test('Resolving incident & re-run pipeline', async ({ page }) => { + const testCase = table1.testCasesResponseData[1]; + const testCaseName = testCase?.['name']; + const pipeline = table1.testSuitePipelineResponseData[0]; + const { apiContext } = await getApiContext(page); + + await test.step("Acknowledge table test case's failure", async () => { + await acknowledgeTask({ + page, + testCase: testCaseName, + table: table1, + }); + }); + + await test.step('Resolve task from incident list page', async () => { + await visitProfilerTab(page, table1); + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases?fields=*' + ); + await page + .getByTestId('profiler-tab-left-panel') + .getByText('Data Quality') + .click(); + await testCaseResponse; + + await expect( + page.locator(`[data-testid="${testCaseName}"] .last-run-box.failed`) + ).toBeVisible(); + await expect(page.getByTestId(`${testCaseName}-status`)).toContainText( + 'Ack' + ); + + const incidentDetailsRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true&startTs=*&endTs=*&limit=*' + ); + await sidebarClick(page, SidebarItem.INCIDENT_MANAGER); + await incidentDetailsRes; + + await expect( + page.locator(`[data-testid="test-case-${testCaseName}"]`) + ).toBeVisible(); + + await page.click( + `[data-testid="${testCaseName}-status"] [data-testid="edit-resolution-icon"]` + ); + await page.click(`[data-testid="test-case-resolution-status-type"]`); + await page.click(`[title="Resolved"]`); + await page.click( + '#testCaseResolutionStatusDetails_testCaseFailureReason' + ); + await page.click('[title="Missing Data"]'); + await page.click(descriptionBox); + await page.fill(descriptionBox, 'test'); + const updateTestCaseIncidentStatus = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus' + ); + await page.click('.ant-modal-footer >> text=Submit'); + await updateTestCaseIncidentStatus; + }); + + await test.step('Task should be closed', async () => { + await visitProfilerTab(page, table1); + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases?fields=*' + ); + await page + .getByTestId('profiler-tab-left-panel') + .getByText('Data Quality') + .click(); + await testCaseResponse; + + await expect( + page.locator(`[data-testid="${testCaseName}"] .last-run-box.failed`) + ).toBeVisible(); + + await page.click( + `[data-testid="${testCaseName}"] >> text=${testCaseName}` + ); + await page.click('[data-testid="incident"]'); + await page.click('[data-testid="closed-task"]'); + await page.waitForSelector('[data-testid="task-feed-card"]'); + + await expect(page.locator('[data-testid="task-tab"]')).toContainText( + 'Resolved the Task.' + ); + }); + + await test.step('Re-run pipeline', async () => { + await triggerTestSuitePipelineAndWaitForSuccess({ + page, + table: table1, + pipeline: { id: pipeline?.['id'] }, + apiContext, + }); + }); + + await test.step('Verify open and closed task', async () => { + await acknowledgeTask({ + page, + testCase: testCaseName, + table: table1, + }); + await page.reload(); + + await page.click('[data-testid="incident"]'); + + await expect(page.locator(`[data-testid="open-task"]`)).toHaveText( + '1 Open' + ); + await expect(page.locator(`[data-testid="closed-task"]`)).toHaveText( + '1 Closed' + ); + }); + }); + + test('Rerunning pipeline for an open incident', async ({ page }) => { + const testCase = table1.testCasesResponseData[2]; + const testCaseName = testCase?.['name']; + const pipeline = table1.testSuitePipelineResponseData[0]; + const assignee = { + name: user1.data.email.split('@')[0], + displayName: user1.getUserName(), + }; + const { apiContext } = await getApiContext(page); + + await test.step('Ack incident and verify open task', async () => { + await acknowledgeTask({ + page, + testCase: testCaseName, + table: table1, + }); + + await page.reload(); + + await page.click('[data-testid="incident"]'); + + await expect(page.locator(`[data-testid="open-task"]`)).toHaveText( + '1 Open' + ); + }); + + await test.step('Assign incident to user', async () => { + await assignIncident({ + page, + testCaseName, + user: assignee, + }); + }); + + await test.step('Re-run pipeline', async () => { + await triggerTestSuitePipelineAndWaitForSuccess({ + page, + table: table1, + pipeline: { id: pipeline?.['id'] }, + apiContext, + }); + }); + + await test.step("Verify incident's status on DQ page", async () => { + await visitProfilerTab(page, table1); + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases?fields=*' + ); + await page + .getByTestId('profiler-tab-left-panel') + .getByText('Data Quality') + .click(); + await testCaseResponse; + + await expect( + page.locator(`[data-testid="${testCaseName}"] .last-run-box.failed`) + ).toBeVisible(); + await expect(page.getByTestId(`${testCaseName}-status`)).toContainText( + 'Assigned' + ); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/Entity.interface.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/Entity.interface.ts index 337156fe82c..56073e0f5ed 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/Entity.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/Entity.interface.ts @@ -65,3 +65,8 @@ export enum ENTITY_PATH { 'apiEndpoints' = 'apiEndpoint', 'dataProducts' = 'dataProduct', } + +export type TestCaseData = { + parameterValues: unknown[]; + testDefinition: string; +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts index 05781d5480e..cda4f17aab8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts @@ -14,7 +14,7 @@ import { APIRequestContext, Page } from '@playwright/test'; import { SERVICE_TYPE } from '../../constant/service'; import { uuid } from '../../utils/common'; import { visitEntityPage } from '../../utils/entity'; -import { EntityTypeEndpoint } from './Entity.interface'; +import { EntityTypeEndpoint, TestCaseData } from './Entity.interface'; import { EntityClass } from './EntityClass'; export class TableClass extends EntityClass { @@ -169,7 +169,7 @@ export class TableClass extends EntityClass { async createTestSuiteAndPipelines(apiContext: APIRequestContext) { if (!this.entityResponseData) { - return this.create(apiContext); + await this.create(apiContext); } const testSuiteData = await apiContext @@ -177,7 +177,7 @@ export class TableClass extends EntityClass { data: { name: `pw-test-suite-${uuid()}`, executableEntityReference: - this.entityResponseData['fullyQualifiedName'], + this.entityResponseData?.['fullyQualifiedName'], description: 'Playwright test suite for table', }, }) @@ -221,12 +221,16 @@ export class TableClass extends EntityClass { }, }) .then((res) => res.json()); + this.testSuitePipelineResponseData.push(pipelineData); return pipelineData; } - async createTestCase(apiContext: APIRequestContext) { + async createTestCase( + apiContext: APIRequestContext, + testCaseData?: TestCaseData + ) { if (!this.testSuiteResponseData) { await this.createTestSuiteAndPipelines(apiContext); } @@ -236,9 +240,10 @@ export class TableClass extends EntityClass { data: { name: `pw-test-case-${uuid()}`, entityLink: `<#E::table::${this.entityResponseData?.['fullyQualifiedName']}>`, - testDefinition: 'tableRowCountToBeBetween', + testDefinition: + testCaseData?.testDefinition ?? 'tableRowCountToBeBetween', testSuite: this.testSuiteResponseData?.['fullyQualifiedName'], - parameterValues: [ + parameterValues: testCaseData?.parameterValues ?? [ { name: 'minValue', value: 12 }, { name: 'maxValue', value: 34 }, ], diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/incidentManager.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/incidentManager.ts new file mode 100644 index 00000000000..54002122fd0 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/incidentManager.ts @@ -0,0 +1,128 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { APIRequestContext, expect, Page } from '@playwright/test'; +import { SidebarItem } from '../constant/sidebar'; +import { TableClass } from '../support/entity/TableClass'; +import { redirectToHomePage } from './common'; +import { sidebarClick } from './sidebar'; + +export const visitProfilerTab = async (page: Page, table: TableClass) => { + await redirectToHomePage(page); + await table.visitEntityPage(page); + await page.click('[data-testid="profiler"]'); +}; + +export const acknowledgeTask = async (data: { + testCase: string; + page: Page; + table: TableClass; +}) => { + const { testCase, page, table } = data; + await visitProfilerTab(page, table); + await page.click('[data-testid="profiler-tab-left-panel"]'); + await page + .getByTestId('profiler-tab-left-panel') + .getByText('Data Quality') + .click(); + await page.click(`[data-testid="${testCase}"] >> .last-run-box.failed`); + await page.waitForSelector(`[data-testid="${testCase}-status"] >> text=New`); + await page.click(`[data-testid="${testCase}"] >> text=${testCase}`); + await page.click('[data-testid="edit-resolution-icon"]'); + await page.click('[data-testid="test-case-resolution-status-type"]'); + await page.click('[title="Ack"]'); + const statusChangeResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus' + ); + await page.click('#update-status-button'); + await statusChangeResponse; + await page.waitForSelector(`[data-testid="${testCase}-status"] >> text=Ack`); +}; + +export const assignIncident = async (data: { + testCaseName: string; + page: Page; + user: { name: string; displayName: string }; +}) => { + const { testCaseName, page, user } = data; + await sidebarClick(page, SidebarItem.INCIDENT_MANAGER); + await page.waitForSelector(`[data-testid="test-case-${testCaseName}"]`); + await page.click( + `[data-testid="${testCaseName}-status"] [data-testid="edit-resolution-icon"]` + ); + await page.click('[data-testid="test-case-resolution-status-type"]'); + await page.click('[title="Assigned"]'); + await page.waitForSelector('#testCaseResolutionStatusDetails_assignee'); + await page.fill( + '#testCaseResolutionStatusDetails_assignee', + user.displayName + ); + await page.waitForResponse('/api/v1/search/suggest?q=*'); + await page.click(`[data-testid="${user.name.toLocaleLowerCase()}"]`); + const updateIncident = page.waitForResponse( + '/api/v1/dataQuality/testCases/testCaseIncidentStatus' + ); + await page.click('#update-status-button'); + await updateIncident; + await page.waitForSelector( + `[data-testid="${testCaseName}-status"] [data-testid="badge-container"] >> text=Assigned` + ); + + await expect( + page.locator( + `[data-testid="${testCaseName}-status"] [data-testid="badge-container"]` + ) + ).toContainText('Assigned'); +}; + +export const triggerTestSuitePipelineAndWaitForSuccess = async (data: { + page: Page; + apiContext: APIRequestContext; + table: TableClass; + pipeline: { id: string }; +}) => { + const { page, apiContext, table, pipeline } = data; + // wait for 2s before the pipeline to be run + await page.waitForTimeout(2000); + await apiContext + .post(`/api/v1/services/ingestionPipelines/trigger/${pipeline.id}`) + .then((res) => { + if (res.status() !== 200) { + return apiContext.post( + `/api/v1/services/ingestionPipelines/trigger/${pipeline.id}` + ); + } + }); + + // Wait for the run to complete + await page.waitForTimeout(2000); + + await expect + .poll( + async () => { + const response = await apiContext + .get( + `/api/v1/services/ingestionPipelines?fields=pipelineStatuses&testSuite=${table.testSuiteResponseData?.['fullyQualifiedName']}&pipelineType=TestSuite` + ) + .then((res) => res.json()); + + return response.data?.[0]?.pipelineStatuses?.pipelineState; + }, + { + // Custom expect message for reporting, optional. + message: 'Wait for the pipeline to be successful', + timeout: 60_000, + intervals: [5_000, 10_000], + } + ) + .toBe('success'); +};