diff --git a/.gitignore b/.gitignore index 5a895be3a35..fa9f80c01f3 100644 --- a/.gitignore +++ b/.gitignore @@ -87,7 +87,7 @@ openmetadata-ui/src/main/resources/ui/cypress/fixtures # Playwright artifacts openmetadata-ui/src/main/resources/ui/playwright/output/ openmetadata-ui/src/main/resources/ui/playwright/e2e/.cache/ -openmetadata-ui/src/main/resources/ui/playwright/.env +openmetadata-ui/src/main/resources/ui/.env openmetadata-ui/src/main/resources/ui/playwright/.auth #UI - Dereferenced Schemas diff --git a/openmetadata-ui/src/main/resources/ui/.eslintrc.yaml b/openmetadata-ui/src/main/resources/ui/.eslintrc.yaml index e63ff2ac4d0..35f74755c4a 100644 --- a/openmetadata-ui/src/main/resources/ui/.eslintrc.yaml +++ b/openmetadata-ui/src/main/resources/ui/.eslintrc.yaml @@ -263,6 +263,8 @@ overrides: - off jest/no-standalone-expect: - off + jest/no-conditional-expect: + - off # i18next rule is not required for js, jsx, json and test file - files: diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataQualityAndProfiler.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataQualityAndProfiler.spec.ts deleted file mode 100644 index 1beb65e8d47..00000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataQualityAndProfiler.spec.ts +++ /dev/null @@ -1,1315 +0,0 @@ -/* - * Copyright 2022 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 { - descriptionBox, - interceptURL, - selectOptionFromDropdown, - toastNotification, - verifyResponseStatusCode, -} from '../../common/common'; -import { createEntityTable, hardDeleteService } from '../../common/EntityUtils'; -import MysqlIngestionClass from '../../common/Services/MysqlIngestionClass'; -import { searchServiceFromSettingPage } from '../../common/serviceUtils'; -import { - DATA_QUALITY_TEST_CASE_DATA, - prepareDataQualityTestCases, -} from '../../common/Utils/DataQuality'; -import { addDomainToEntity } from '../../common/Utils/Domain'; -import { visitEntityDetailsPage } from '../../common/Utils/Entity'; -import { - handleIngestionRetry, - scheduleIngestion, -} from '../../common/Utils/Ingestion'; -import { getToken } from '../../common/Utils/LocalStorage'; -import { removeOwner, updateOwner } from '../../common/Utils/Owner'; -import { goToServiceListingPage, Services } from '../../common/Utils/Services'; -import { - DATA_QUALITY_SAMPLE_DATA_TABLE, - DELETE_TERM, - NEW_COLUMN_TEST_CASE, - NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE, - NEW_TABLE_TEST_CASE, - NEW_TEST_SUITE, -} from '../../constants/constants'; -import { EntityType, SidebarItem } from '../../constants/Entity.interface'; -import { DATABASE_SERVICE } from '../../constants/EntityConstant'; -import { SERVICE_CATEGORIES } from '../../constants/service.constants'; -import { GlobalSettingOptions } from '../../constants/settings.constant'; - -const OWNER1 = 'Aaron Johnson'; -const OWNER2 = 'Cynthia Meyer'; -const { - testCase1, - testCase2, - filterTable, - filterTable2, - filterTableTestCases, - filterTable2TestCases, - customTable, - domainDetail, -} = DATA_QUALITY_TEST_CASE_DATA; -const TEAM_ENTITY = customTable.name; -const serviceName = DATABASE_SERVICE.service.name; -const goToProfilerTab = (data?: { service: string; entityName: string }) => { - visitEntityDetailsPage({ - term: data?.entityName ?? TEAM_ENTITY, - serviceName: data?.service ?? serviceName, - entity: EntityType.Table, - }); - - cy.get('[data-testid="profiler"]').should('be.visible').click(); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Table Profile') - .click(); -}; - -const visitTestSuiteDetailsPage = (testSuiteName: string) => { - interceptURL( - 'GET', - '/api/v1/dataQuality/testSuites/search/list?*testSuiteType=logical*', - 'testSuite' - ); - interceptURL('GET', '/api/v1/dataQuality/testCases?fields=*', 'testCase'); - - cy.sidebarClick(SidebarItem.DATA_QUALITY); - - cy.get('[data-testid="by-test-suites"]').click(); - verifyResponseStatusCode('@testSuite', 200); - interceptURL( - 'GET', - `/api/v1/dataQuality/testSuites/search/list?*${testSuiteName}*testSuiteType=logical*`, - 'testSuiteBySearch' - ); - cy.get('[data-testid="search-bar-container"]').type(testSuiteName); - verifyResponseStatusCode('@testSuiteBySearch', 200); - cy.get(`[data-testid="${testSuiteName}"]`).scrollIntoView().click(); -}; - -const verifyFilterTestCase = () => { - filterTableTestCases.map((testCase) => { - cy.get(`[data-testid="${testCase}"]`).scrollIntoView().should('be.visible'); - }); -}; -const verifyFilter2TestCase = (negation = false) => { - filterTable2TestCases.map((testCase) => { - negation - ? cy.get(`[data-testid="${testCase}"]`).should('not.exist') - : cy - .get(`[data-testid="${testCase}"]`) - .scrollIntoView() - .should('be.visible'); - }); -}; - -describe( - 'Data Quality and Profiler should work properly', - { tags: 'Observability' }, - () => { - const mySql = new MysqlIngestionClass(); - before(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - - createEntityTable({ - token, - ...DATABASE_SERVICE, - tables: [ - DATABASE_SERVICE.entity, - filterTable, - filterTable2, - customTable, - ], - }); - - prepareDataQualityTestCases(token); - }); - }); - - after(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - hardDeleteService({ - token, - serviceFqn: DATABASE_SERVICE.service.name, - serviceType: SERVICE_CATEGORIES.DATABASE_SERVICES, - }); - }); - }); - - beforeEach(() => { - cy.login(); - interceptURL('GET', `/api/v1/tables/*/systemProfile?*`, 'systemProfile'); - interceptURL('GET', `/api/v1/tables/*/tableProfile?*`, 'tableProfile'); - }); - - it('Add and ingest mysql data', () => { - goToServiceListingPage(Services.Database); - - mySql.createService(); - }); - - it('Add Profiler ingestion', () => { - const data = { - entityName: 'alert_entity', - service: 'cypress%mysql', - }; - interceptURL( - 'POST', - '/api/v1/services/ingestionPipelines/deploy/*', - 'deployIngestion' - ); - - goToProfilerTab(data); - - cy.get('[data-testid="no-profiler-placeholder"]').should('be.visible'); - cy.clickOnLogo(); - - cy.settingClick(GlobalSettingOptions.DATABASES); - - cy.intercept('/api/v1/services/ingestionPipelines?*').as('ingestionData'); - - searchServiceFromSettingPage(data.service); - cy.get(`[data-testid="service-name-${data.service}"]`) - .should('exist') - .click(); - cy.get('[data-testid="tabs"]').should('exist'); - cy.wait('@ingestionData'); - cy.get('[data-testid="ingestions"]') - .scrollIntoView() - .should('be.visible') - .click(); - cy.get('[data-testid="ingestion-details-container"]').should('exist'); - cy.get('[data-testid="add-new-ingestion-button"]') - .should('be.visible') - .click(); - cy.get('[data-menu-id*="profiler"') - .scrollIntoView() - .contains('Profiler Ingestion') - .click(); - cy.get('#root\\/profileSample') - .scrollIntoView() - .should('be.visible') - .and('not.be.disabled') - .type('10'); - cy.get('[data-testid="submit-btn"]') - .scrollIntoView() - .should('be.visible') - .click(); - - scheduleIngestion(false); - - cy.wait('@deployIngestion').then(() => { - cy.get('[data-testid="view-service-button"]') - .scrollIntoView() - .should('be.visible') - .click(); - - handleIngestionRetry(0, 'profiler'); - }); - }); - - it('Verifying profiler ingestion', () => { - goToProfilerTab({ - entityName: 'alert_entity', - service: 'cypress%mysql', - }); - cy.get('[data-testid="no-profiler-placeholder"]').should('not.exist'); - }); - - it('Add table test case', () => { - goToProfilerTab(); - interceptURL( - 'GET', - `api/v1/tables/name/${serviceName}.*.${TEAM_ENTITY}?include=all`, - 'addTableTestPage' - ); - verifyResponseStatusCode('@systemProfile', 200); - verifyResponseStatusCode('@tableProfile', 200); - interceptURL('GET', '/api/v1/dataQuality/testCases?fields=*', 'testCase'); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - verifyResponseStatusCode('@testCase', 200); - cy.get('[data-testid="profiler-add-table-test-btn"]').click(); - cy.get('[data-testid="table"]').click(); - - // creating new test case - cy.get('#tableTestForm_testTypeId').scrollIntoView().click(); - cy.contains(NEW_TABLE_TEST_CASE.label).should('be.visible').click(); - cy.get('#tableTestForm_testName').type(NEW_TABLE_TEST_CASE.name); - cy.get('#tableTestForm_params_columnName').type( - NEW_TABLE_TEST_CASE.field - ); - cy.get(descriptionBox).scrollIntoView(); - cy.get(descriptionBox).type(NEW_TABLE_TEST_CASE.description); - - cy.get('[data-testid="submit-test"]') - .scrollIntoView() - .should('be.visible') - .click(); - - cy.get('[data-testid="success-line"]') - .scrollIntoView() - .should('be.visible'); - cy.get('[data-testid="add-ingestion-button"]') - .should('be.visible') - .click(); - cy.get('[data-testid="select-all-test-cases"]').click(); - scheduleIngestion(false); - - cy.get('[data-testid="success-line"]') - .scrollIntoView() - .should('be.visible'); - - cy.get('[data-testid="view-service-button"]') - .should('be.visible') - .click({ force: true }); - - verifyResponseStatusCode('@getEntityDetails', 200); - - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - verifyResponseStatusCode('@testCase', 200); - cy.contains(NEW_TABLE_TEST_CASE.name).should('be.visible'); - }); - - it('Edit table test case', () => { - goToProfilerTab(); - - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - - cy.get(`[data-testid="${NEW_TABLE_TEST_CASE.name}"]`).should( - 'be.visible' - ); - cy.get(`[data-testid="edit-${NEW_TABLE_TEST_CASE.name}"]`).click(); - cy.get('#tableTestForm_params_columnName') - .scrollIntoView() - .clear() - .type('test'); - interceptURL('PATCH', '/api/v1/dataQuality/testCases/*', 'updateTest'); - cy.get('.ant-modal-footer').contains('Submit').click(); - verifyResponseStatusCode('@updateTest', 200); - cy.get('.Toastify__toast-body') - .contains('Test case updated successfully.') - .should('be.visible'); - - cy.get(`[data-testid="edit-${NEW_TABLE_TEST_CASE.name}"]`).click(); - cy.get('#tableTestForm_params_columnName').should('have.value', 'test'); - }); - - it('Add Column test case with min max params', () => { - goToProfilerTab(); - interceptURL( - 'GET', - `api/v1/tables/name/${serviceName}.*.${TEAM_ENTITY}?include=all`, - 'addTableTestPage' - ); - verifyResponseStatusCode('@systemProfile', 200); - verifyResponseStatusCode('@tableProfile', 200); - interceptURL('GET', '/api/v1/dataQuality/testCases?fields=*', 'testCase'); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - verifyResponseStatusCode('@testCase', 200); - cy.get('[data-testid="profiler-add-table-test-btn"]').click(); - cy.get('[data-testid="column"]').click(); - - // creating new test case - cy.get('#tableTestForm_column').click(); - cy.get(`[title="${NEW_COLUMN_TEST_CASE.column}"]`) - .scrollIntoView() - .click(); - cy.get('#tableTestForm_testName').type(NEW_COLUMN_TEST_CASE.name); - cy.get('#tableTestForm_testTypeId').scrollIntoView().click(); - cy.get(`[title="${NEW_COLUMN_TEST_CASE.label}"]`) - .scrollIntoView() - .click(); - cy.get('#tableTestForm_params_minLength') - .scrollIntoView() - .type(NEW_COLUMN_TEST_CASE.min); - cy.get('#tableTestForm_params_maxLength') - .scrollIntoView() - .type(NEW_COLUMN_TEST_CASE.max); - cy.get(descriptionBox) - .scrollIntoView() - .type(NEW_COLUMN_TEST_CASE.description); - - cy.get('[data-testid="submit-test"]').scrollIntoView().click(); - - cy.get('[data-testid="success-line"]') - .scrollIntoView() - .contains( - 'has been created successfully. This will be picked up in the next run.' - ) - .should('be.visible'); - cy.get('[data-testid="view-service-button"]').scrollIntoView().click(); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - cy.contains(NEW_COLUMN_TEST_CASE.name) - .scrollIntoView() - .should('be.visible'); - }); - - it('Add column test case for columnValuesToBeNotNull', () => { - // Creating new test case and selecting Null team type - - goToProfilerTab(); - interceptURL( - 'GET', - `api/v1/tables/name/${serviceName}.*.${TEAM_ENTITY}?include=all`, - 'addTableTestPage' - ); - verifyResponseStatusCode('@systemProfile', 200); - verifyResponseStatusCode('@tableProfile', 200); - interceptURL('GET', '/api/v1/dataQuality/testCases?fields=*', 'testCase'); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - verifyResponseStatusCode('@testCase', 200); - cy.get('[data-testid="profiler-add-table-test-btn"]').click(); - cy.get('[data-testid="column"]').click(); - - cy.get('#tableTestForm_column').click(); - cy.get(`[title="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.column}"]`) - .scrollIntoView() - .click(); - cy.get('#tableTestForm_testName').type( - NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name - ); - cy.get('#tableTestForm_testTypeId').type( - NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.type - ); - cy.get(`[title="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.label}"]`).click(); - cy.get(descriptionBox) - .scrollIntoView() - .type(NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.description); - - cy.get('[data-testid="submit-test"]').scrollIntoView().click(); - - cy.get('[data-testid="success-line"]') - .contains( - 'has been created successfully. This will be picked up in the next run.' - ) - .should('be.visible'); - cy.get('[data-testid="view-service-button"]').scrollIntoView().click(); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - cy.get( - `[data-testid="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name}"]` - ).should('contain', NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name); - }); - - it('Edit column test case should work properly', () => { - interceptURL('GET', '/api/v1/dataQuality/testCases?*', 'testCase'); - goToProfilerTab(); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - verifyResponseStatusCode('@testCase', 200); - cy.get(`[data-testid="${NEW_COLUMN_TEST_CASE.name}"]`).should( - 'be.visible' - ); - cy.get(`[data-testid="edit-${NEW_COLUMN_TEST_CASE.name}"]`) - .scrollIntoView() - .should('be.visible') - .click(); - cy.get('#tableTestForm_params_minLength') - .scrollIntoView() - .should('be.visible') - .clear() - .type('4'); - interceptURL('PATCH', '/api/v1/dataQuality/testCases/*', 'updateTest'); - cy.get('.ant-modal-footer').contains('Submit').click(); - verifyResponseStatusCode('@updateTest', 200); - cy.get('.Toastify__toast-body') - .contains('Test case updated successfully.') - .should('be.visible'); - - cy.get(`[data-testid="edit-${NEW_COLUMN_TEST_CASE.name}"]`).click(); - cy.get('#tableTestForm_params_minLength').should('have.value', '4'); - cy.get('.ant-modal-footer').contains('Cancel').click(); - - // Editing Non Team Type Test Case - cy.get( - `[data-testid="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name}"]` - ).should('be.visible'); - cy.get(`[data-testid="edit-${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name}"]`) - .scrollIntoView() - .should('be.visible') - .click(); - cy.get('.ant-modal-footer').contains('Cancel').click(); - }); - - it('Delete Column Test Case should work properly', () => { - interceptURL('GET', '/api/v1/dataQuality/testCases?*', 'testCase'); - goToProfilerTab(); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .should('be.visible') - .click(); - verifyResponseStatusCode('@testCase', 200); - [NEW_COLUMN_TEST_CASE.name, NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name].map( - (test) => { - cy.get(`[data-testid="${test}"]`) - .scrollIntoView() - .should('be.visible'); - cy.get(`[data-testid="delete-${test}"]`).scrollIntoView().click(); - cy.get('[data-testid="hard-delete-option"]').click(); - cy.get('[data-testid="confirmation-text-input"]').type(DELETE_TERM); - interceptURL( - 'DELETE', - '/api/v1/dataQuality/testCases/*?hardDelete=true&recursive=true', - 'deleteTest' - ); - interceptURL('GET', '/api/v1/dataQuality/testCases?*', 'getTestCase'); - cy.get('[data-testid="confirm-button"]').click(); - verifyResponseStatusCode('@deleteTest', 200); - verifyResponseStatusCode('@getTestCase', 200); - toastNotification(`"${test}" deleted successfully!`); - } - ); - }); - - it('Create logical test suite', () => { - const testCaseName = 'column_value_max_to_be_between'; - interceptURL( - 'GET', - '/api/v1/dataQuality/testSuites/search/list?*testSuiteType=logical*', - 'testSuite' - ); - interceptURL( - 'GET', - '/api/v1/search/query?q=*&index=test_case_search_index*', - 'getTestCase' - ); - - cy.sidebarClick(SidebarItem.DATA_QUALITY); - - cy.get('[data-testid="by-test-suites"]').click(); - verifyResponseStatusCode('@testSuite', 200); - cy.get('[data-testid="add-test-suite-btn"]').click(); - - // creating test suite - cy.get('[data-testid="test-suite-name"]').type(NEW_TEST_SUITE.name); - cy.get(descriptionBox).scrollIntoView().type(NEW_TEST_SUITE.description); - - cy.get('[data-testid="submit-button"]').click(); - cy.get('[data-testid="searchbar"]').type(testCaseName); - verifyResponseStatusCode('@getTestCase', 200); - cy.get(`[data-testid="${testCaseName}"]`).scrollIntoView().as('testCase'); - cy.get('@testCase').click(); - cy.get('[data-testid="submit"]').scrollIntoView().click(); - - cy.get('[data-testid="success-line"]').should( - 'contain', - 'has been created successfully' - ); - }); - - it('User as Owner assign, update & delete for test suite', () => { - interceptURL( - 'GET', - '/api/v1/search/query?q=*&index=test_case_search_index*', - 'searchTestCase' - ); - interceptURL('GET', '/api/v1/dataQuality/testCases?fields=*', 'testCase'); - interceptURL( - 'PUT', - '/api/v1/dataQuality/testCases/logicalTestCases', - 'putTestCase' - ); - - visitTestSuiteDetailsPage(NEW_TEST_SUITE.name); - - updateOwner(OWNER2); - removeOwner(OWNER2); - updateOwner(OWNER1); - }); - - it('Add test case to logical test suite', () => { - const testCaseName = 'column_values_to_be_between'; - interceptURL( - 'GET', - '/api/v1/search/query?q=*&index=test_case_search_index*', - 'searchTestCase' - ); - interceptURL('GET', '/api/v1/dataQuality/testCases?fields=*', 'testCase'); - interceptURL( - 'PUT', - '/api/v1/dataQuality/testCases/logicalTestCases', - 'putTestCase' - ); - - visitTestSuiteDetailsPage(NEW_TEST_SUITE.name); - - cy.get('[data-testid="add-test-case-btn"]').click(); - verifyResponseStatusCode('@testCase', 200); - - cy.get('[data-testid="searchbar"]').type(testCaseName); - verifyResponseStatusCode('@searchTestCase', 200); - cy.get(`[data-testid="${testCaseName}"]`) - .scrollIntoView() - .as('newTestCase'); - cy.get('@newTestCase').click(); - cy.get('[data-testid="submit"]').scrollIntoView().click(); - verifyResponseStatusCode('@putTestCase', 200); - }); - - it('Remove test case from logical test suite', () => { - interceptURL('GET', '/api/v1/dataQuality/testCases?fields=*', 'testCase'); - interceptURL( - 'GET', - '/api/v1/permissions/testSuite/name/mysql_matrix', - 'testSuitePermission' - ); - interceptURL( - 'DELETE', - '/api/v1/dataQuality/testCases/logicalTestCases/*/*', - 'removeTestCase' - ); - visitTestSuiteDetailsPage(NEW_TEST_SUITE.name); - verifyResponseStatusCode('@testSuitePermission', 200); - verifyResponseStatusCode('@testCase', 200); - - cy.get('[data-testid="remove-column_values_to_be_between"]').click(); - cy.get('[data-testid="save-button"]').click(); - verifyResponseStatusCode('@removeTestCase', 200); - - cy.get('[data-testid="remove-column_value_max_to_be_between"]').click(); - cy.get('[data-testid="save-button"]').click(); - verifyResponseStatusCode('@removeTestCase', 200); - }); - - it('Test suite filters', () => { - interceptURL( - 'GET', - '/api/v1/dataQuality/testSuites/search/list?*testSuiteType=logical*', - 'testSuite' - ); - interceptURL( - 'GET', - '/api/v1/dataQuality/testSuites/search/list?*owner=*', - 'testSuiteByOwner' - ); - cy.sidebarClick(SidebarItem.DATA_QUALITY); - - cy.get('[data-testid="by-test-suites"]').click(); - verifyResponseStatusCode('@testSuite', 200); - interceptURL('GET', '/api/v1/users?*isBot=false*', 'getOwner'); - // owner filter - cy.get('[data-testid="owner-select-filter"]').click(); - cy.get("[data-testid='select-owner-tabs']").should('be.visible'); - cy.get('.ant-tabs [id*=tab-users]').click(); - cy.wait('@getOwner'); - interceptURL( - 'GET', - `api/v1/search/query?q=*&index=user_search_index*`, - 'searchOwner' - ); - - cy.get('[data-testid="owner-select-users-search-bar"]').type(OWNER1); - - verifyResponseStatusCode('@searchOwner', 200); - cy.get(`.ant-popover [title="${OWNER1}"]`).click(); - verifyResponseStatusCode('@testSuiteByOwner', 200); - cy.get(`[data-testid="${NEW_TEST_SUITE.name}"]`) - .scrollIntoView() - .should('be.visible'); - }); - - it('Delete test suite', () => { - visitTestSuiteDetailsPage(NEW_TEST_SUITE.name); - - cy.get('[data-testid="manage-button"]').should('be.visible').click(); - - cy.get('[data-testid="delete-button"]').should('be.visible').click(); - - // Click on Permanent/Hard delete option - cy.get('[data-testid="hard-delete-option"]') - .should('contain', NEW_TEST_SUITE.name) - .should('be.visible') - .click(); - - cy.get('[data-testid="confirm-button"]') - .should('exist') - .should('be.disabled'); - - cy.get('[data-testid="confirmation-text-input"]') - .should('be.visible') - .type(DELETE_TERM); - interceptURL( - 'DELETE', - '/api/v1/dataQuality/testSuites/*?hardDelete=true&recursive=true', - 'deleteTestSuite' - ); - cy.get('[data-testid="confirm-button"]') - .should('be.visible') - .should('not.be.disabled') - .click(); - verifyResponseStatusCode('@deleteTestSuite', 200); - - toastNotification('"mysql_matrix" deleted successfully!'); - }); - - it('delete created service', () => { - goToServiceListingPage(Services.Database); - mySql.deleteService(); - }); - - it('Profiler matrix and test case graph should visible', () => { - const { term, entity, serviceName, testCaseName } = - DATA_QUALITY_SAMPLE_DATA_TABLE; - visitEntityDetailsPage({ term, serviceName, entity }); - cy.get('[data-testid="entity-header-display-name"]') - .contains(term) - .should('be.visible'); - - cy.get('[data-testid="profiler"]').should('be.visible').click(); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Table Profile') - .click(); - interceptURL( - 'GET', - '/api/v1/tables/*/columnProfile?*', - 'getProfilerInfo' - ); - interceptURL('GET', '/api/v1/dataQuality/testCases?*', 'getTestCaseInfo'); - - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Column Profile') - .click(); - - cy.get('[data-row-key="shop_id"]') - .contains('shop_id') - .scrollIntoView() - .click(); - verifyResponseStatusCode('@getProfilerInfo', 200); - verifyResponseStatusCode('@getTestCaseInfo', 200); - - cy.get('#count_graph').scrollIntoView().should('be.visible'); - cy.get('#proportion_graph').scrollIntoView().should('be.visible'); - cy.get('#math_graph').scrollIntoView().should('be.visible'); - cy.get('#sum_graph').scrollIntoView().should('be.visible'); - - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/name/*?fields=*', - 'getTestCaseDetails' - ); - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/*/testCaseResult?*', - 'getTestResult' - ); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - - cy.get(`[data-testid="${testCaseName}"]`).contains(testCaseName).click(); - verifyResponseStatusCode('@getTestCaseDetails', 200); - cy.wait('@getTestResult').then(() => { - cy.get(`[id="${testCaseName}_graph"]`) - .scrollIntoView() - .should('be.visible'); - }); - }); - - it('Array params value should be visible while editing the test case', () => { - const tableName = DATABASE_SERVICE.entity.name; - goToProfilerTab({ - service: DATABASE_SERVICE.service.name, - entityName: tableName, - }); - - interceptURL('GET', '/api/v1/dataQuality/testCases?fields=*', 'testCase'); - interceptURL( - 'GET', - '/api/v1/dataQuality/testDefinitions/*', - 'testCaseDefinition' - ); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - verifyResponseStatusCode('@testCase', 200); - cy.get(`[data-testid="${testCase2.name}"]`) - .scrollIntoView() - .should('be.visible'); - cy.get(`[data-testid="edit-${testCase2.name}"]`) - .should('be.visible') - .click(); - - verifyResponseStatusCode('@testCaseDefinition', 200); - - cy.get('#tableTestForm_params_allowedValues_0_value') - .scrollIntoView() - .should('have.value', 'gmail'); - cy.get('#tableTestForm_params_allowedValues_1_value') - .scrollIntoView() - .should('have.value', 'yahoo'); - cy.get('#tableTestForm_params_allowedValues_2_value') - .scrollIntoView() - .should('have.value', 'collate'); - }); - - it('Validate patch request for edit test case', () => { - const tableName = DATABASE_SERVICE.entity.name; - goToProfilerTab({ - service: DATABASE_SERVICE.service.name, - entityName: tableName, - }); - - interceptURL( - 'PATCH', - '/api/v1/dataQuality/testCases/*', - 'updateTestCase' - ); - interceptURL('GET', '/api/v1/dataQuality/testCases?fields=*', 'testCase'); - interceptURL( - 'GET', - '/api/v1/dataQuality/testDefinitions/*', - 'testCaseDefinition' - ); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - verifyResponseStatusCode('@testCase', 200); - cy.get(`[data-testid="edit-${testCase2.name}"]`).scrollIntoView().click(); - - verifyResponseStatusCode('@testCaseDefinition', 200); - cy.get('#tableTestForm_displayName').type('Table test case display name'); - cy.get('#tableTestForm_table').should( - 'have.value', - DATABASE_SERVICE.entity.name - ); - cy.get('#tableTestForm_column').should('have.value', 'email'); - cy.get('#tableTestForm_name').should('have.value', testCase2.name); - cy.get('#tableTestForm_testDefinition').should( - 'have.value', - 'Column Values To Be In Set' - ); - cy.get('.ant-modal-footer').contains('Submit').click(); - cy.wait('@updateTestCase').then((interception) => { - const { body } = interception.request; - - expect(body).to.deep.equal([ - { - op: 'add', - path: '/displayName', - value: 'Table test case display name', - }, - ]); - }); - cy.get(`[data-testid="edit-${testCase2.name}"]`).scrollIntoView().click(); - cy.get('#tableTestForm_params_allowedValues_0_value') - .scrollIntoView() - .clear() - .type('test'); - cy.get('.ant-modal-footer').contains('Submit').click(); - cy.wait('@updateTestCase').then((interception) => { - const { body } = interception.request; - - expect(body).to.deep.equal([ - { - op: 'replace', - path: '/parameterValues/0/value', - value: '["test","yahoo","collate"]', - }, - ]); - }); - cy.get(`[data-testid="edit-${testCase2.name}"]`).scrollIntoView().click(); - cy.get(descriptionBox).scrollIntoView().type('Test case description'); - cy.get('.ant-modal-footer').contains('Submit').click(); - cy.wait('@updateTestCase').then((interception) => { - const { body } = interception.request; - - expect(body).to.deep.equal([ - { op: 'add', path: '/description', value: 'Test case description' }, - ]); - }); - }); - - it('Update displayName of test case', () => { - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/search/list?*', - 'getTestCase' - ); - - cy.sidebarClick(SidebarItem.DATA_QUALITY); - - cy.get('[data-testid="by-test-cases"]').click(); - verifyResponseStatusCode('@getTestCase', 200); - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*q=*${testCase1.name}*`, - 'searchTestCase' - ); - cy.get( - '[data-testid="test-case-container"] [data-testid="searchbar"]' - ).type(testCase1.name); - verifyResponseStatusCode('@searchTestCase', 200); - cy.get(`[data-testid="${testCase1.name}"]`) - .scrollIntoView() - .should('be.visible'); - cy.get(`[data-testid="edit-${testCase1.name}"]`).click(); - cy.get('.ant-modal-body').should('be.visible'); - cy.get('#tableTestForm_displayName').type('Table test case display name'); - interceptURL( - 'PATCH', - '/api/v1/dataQuality/testCases/*', - 'updateTestCase' - ); - cy.get('.ant-modal-footer').contains('Submit').click(); - verifyResponseStatusCode('@updateTestCase', 200); - cy.get(`[data-testid="${testCase1.name}"]`) - .scrollIntoView() - .invoke('text') - .then((text) => { - expect(text).to.eq('Table test case display name'); - }); - }); - - it('Test case filters', () => { - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/search/list?*', - 'getTestCase' - ); - - interceptURL( - 'GET', - `/api/v1/search/query?q=*index=tag_search_index*`, - 'searchTags' - ); - - cy.sidebarClick(SidebarItem.DATA_QUALITY); - - cy.get('[data-testid="by-test-cases"]').click(); - verifyResponseStatusCode('@getTestCase', 200); - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*q=*${filterTableTestCases[0]}*`, - 'searchTestCase' - ); - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="tableFqn"]').click({ waitForAnimations: true }); - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="testPlatforms"]').click({ waitForAnimations: true }); - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="lastRunRange"]').click({ waitForAnimations: true }); - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="serviceName"]').click({ waitForAnimations: true }); - - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="tags"]').click({ waitForAnimations: true }); - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="tier"]').click({ waitForAnimations: true }); - - // Test case search filter - cy.get( - '[data-testid="test-case-container"] [data-testid="searchbar"]' - ).type(filterTableTestCases[0]); - verifyResponseStatusCode('@searchTestCase', 200); - cy.get(`[data-testid="${filterTableTestCases[0]}"]`) - .scrollIntoView() - .should('be.visible'); - cy.get('.ant-input-clear-icon').click(); - verifyResponseStatusCode('@getTestCase', 200); - - // Test case filter by service name - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*serviceName=${DATABASE_SERVICE.service.name}*`, - 'getTestCaseByServiceName' - ); - interceptURL( - 'GET', - `/api/v1/search/query?q=*index=database_service_search_index*`, - 'searchService' - ); - cy.get('#serviceName') - .scrollIntoView() - .type(DATABASE_SERVICE.service.name); - verifyResponseStatusCode('@searchService', 200); - cy.get('.ant-select-dropdown') - .not('.ant-select-dropdown-hidden') - .find(`[data-testid="${DATABASE_SERVICE.service.name}"]`) - .click({ force: true }); - verifyResponseStatusCode('@getTestCaseByServiceName', 200); - verifyFilterTestCase(); - verifyFilter2TestCase(); - // remove service filter - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="serviceName"]').click({ waitForAnimations: true }); - verifyResponseStatusCode('@getTestCase', 200); - cy.get('#serviceName').should('not.exist'); - - // Test case filter by Tags - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*tags=${'PII.None'}*`, - 'getTestCaseByTags' - ); - cy.get('#tags').scrollIntoView().click().type('PII.None'); - verifyResponseStatusCode('@searchTags', 200); - cy.get('.ant-select-dropdown') - .not('.ant-select-dropdown-hidden') - .find(`[data-testid="${'PII.None'}"]`) - .click({ force: true }); - verifyResponseStatusCode('@getTestCaseByTags', 200); - verifyFilterTestCase(); - verifyFilter2TestCase(true); - // remove tags filter - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="tags"]').click({ waitForAnimations: true }); - verifyResponseStatusCode('@getTestCase', 200); - cy.get('#tags').should('not.exist'); - - // Test case filter by Tier - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*tier=${'Tier.Tier2'}*`, - 'getTestCaseByTier' - ); - cy.get('#tier').click(); - cy.get('.ant-select-dropdown') - .not('.ant-select-dropdown-hidden') - .find(`[data-testid="${'Tier.Tier2'}"]`) - .click({ force: true }); - verifyResponseStatusCode('@getTestCaseByTier', 200); - verifyFilterTestCase(); - verifyFilter2TestCase(true); - // remove tier filter - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="tier"]').click({ waitForAnimations: true }); - verifyResponseStatusCode('@getTestCase', 200); - cy.get('#tier').should('not.exist'); - - // Test case filter by table name - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*entityLink=*${filterTable.name}*`, - 'searchTestCaseByTable' - ); - interceptURL( - 'GET', - `/api/v1/search/query?q=*index=table_search_index*`, - 'searchTable' - ); - cy.get('#tableFqn').scrollIntoView().type(filterTable.name); - verifyResponseStatusCode('@searchTable', 200); - cy.get('.ant-select-dropdown') - .not('.ant-select-dropdown-hidden') - .find( - `[data-testid="${filterTable.databaseSchema}.${filterTable.name}"]` - ) - .click({ force: true }); - verifyResponseStatusCode('@searchTestCaseByTable', 200); - verifyFilterTestCase(); - verifyFilter2TestCase(true); - - // Test case filter by test type - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*testCaseType=column*`, - 'testCaseTypeByColumn' - ); - cy.get('[data-testid="test-case-type-select-filter"]').click(); - selectOptionFromDropdown('Column'); - verifyResponseStatusCode('@testCaseTypeByColumn', 200); - cy.get('[data-testid="search-error-placeholder"]').should('be.visible'); - - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*testCaseType=table*`, - 'testCaseTypeByTable' - ); - cy.get('[data-testid="test-case-type-select-filter"]').click(); - selectOptionFromDropdown('Table'); - verifyResponseStatusCode('@testCaseTypeByTable', 200); - verifyFilterTestCase(); - - cy.get('[data-testid="test-case-type-select-filter"]').click(); - selectOptionFromDropdown('All'); - verifyResponseStatusCode('@getTestCase', 200); - - // Test case filter by status - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*testCaseStatus=Success*`, - 'testCaseStatusBySuccess' - ); - cy.get('[data-testid="status-select-filter"]').click(); - selectOptionFromDropdown('Success'); - verifyResponseStatusCode('@testCaseStatusBySuccess', 200); - cy.get('[data-testid="search-error-placeholder"]').should('be.visible'); - - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*testCaseStatus=Failed*`, - 'testCaseStatusByFailed' - ); - cy.get('[data-testid="status-select-filter"]').click(); - selectOptionFromDropdown('Failed'); - verifyResponseStatusCode('@testCaseStatusByFailed', 200); - verifyFilterTestCase(); - - // Test case filter by platform - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*testPlatforms=DBT*`, - 'testCasePlatformByDBT' - ); - cy.get('[data-testid="platform-select-filter"]').click(); - selectOptionFromDropdown('DBT'); - verifyResponseStatusCode('@testCasePlatformByDBT', 200); - cy.clickOutside(); - cy.get('[data-testid="search-error-placeholder"]').should('be.visible'); - cy.get( - '[data-testid="platform-select-filter"] .ant-select-clear' - ).click(); - verifyResponseStatusCode('@getTestCase', 200); - - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*testPlatforms=OpenMetadata*`, - 'testCasePlatformByOpenMetadata' - ); - cy.get('[data-testid="platform-select-filter"]').click(); - selectOptionFromDropdown('OpenMetadata'); - verifyResponseStatusCode('@testCasePlatformByOpenMetadata', 200); - cy.clickOutside(); - verifyFilterTestCase(); - cy.url().then((url) => { - cy.reload(); - verifyResponseStatusCode('@testCasePlatformByOpenMetadata', 200); - cy.url().then((updatedUrl) => { - expect(url).to.be.equal(updatedUrl); - }); - }); - - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="testPlatforms"]').click({ - waitForAnimations: true, - }); - verifyResponseStatusCode('@getTestCase', 200); - cy.get('[value="platform-select-filter"]').should('not.exist'); - cy.reload(); - verifyResponseStatusCode('@getTestCase', 200); - cy.get('[value="tier"]').should('not.exist'); - }); - - it('Filter with domain', () => { - visitEntityDetailsPage({ - term: filterTable.name, - serviceName: serviceName, - entity: EntityType.Table, - }); - - addDomainToEntity(domainDetail.name); - - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/search/list?*', - 'getTestCase' - ); - cy.get('[data-testid="domain-dropdown"]').click(); - cy.get(`li[data-menu-id*='${domainDetail.name}']`).click(); - cy.sidebarClick(SidebarItem.DATA_QUALITY); - - cy.get('[data-testid="by-test-cases"]').click(); - verifyResponseStatusCode('@getTestCase', 200); - - cy.get('[data-testid="advanced-filter"]').click({ - waitForAnimations: true, - }); - cy.get('[value="tableFqn"]').click({ waitForAnimations: true }); - - // Test case filter by table name - interceptURL( - 'GET', - `/api/v1/dataQuality/testCases/search/list?*entityLink=*${filterTable.name}*`, - 'searchTestCaseByTable' - ); - cy.get('#tableFqn').scrollIntoView().type(filterTable.name); - cy.get('.ant-select-dropdown') - .not('.ant-select-dropdown-hidden') - .find( - `[data-testid="${filterTable.databaseSchema}.${filterTable.name}"]` - ) - .click({ force: true }); - verifyResponseStatusCode('@searchTestCaseByTable', 200); - verifyFilterTestCase(); - }); - - it('Update profiler setting modal', () => { - const profilerSetting = { - profileSample: '60', - sampleDataCount: '100', - profileQuery: 'select * from table', - excludeColumns: 'user_id', - includeColumns: 'shop_id', - partitionColumnName: 'name', - partitionIntervalType: 'COLUMN-VALUE', - partitionValues: 'test', - }; - interceptURL( - 'GET', - '/api/v1/tables/*/tableProfile?startTs=*', - 'tableProfiler' - ); - interceptURL('GET', '/api/v1/tables/*/systemProfile?*', 'systemProfiler'); - interceptURL( - 'GET', - '/api/v1/tables/*/tableProfilerConfig', - 'tableProfilerConfig' - ); - visitEntityDetailsPage({ - term: DATABASE_SERVICE.entity.name, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - }); - cy.get('[data-testid="profiler"]').should('be.visible').click(); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Table Profile') - .click(); - verifyResponseStatusCode('@tableProfiler', 200); - verifyResponseStatusCode('@systemProfiler', 200); - cy.get('[data-testid="profiler-setting-btn"]').click(); - cy.get('.ant-modal-body').should('be.visible'); - cy.get('[data-testid="slider-input"]') - .clear() - .type(profilerSetting.profileSample); - cy.get('[data-testid="sample-data-count-input"]') - .clear() - .type(profilerSetting.sampleDataCount); - cy.get('[data-testid="exclude-column-select"]') - .scrollIntoView() - .type(`${profilerSetting.excludeColumns}{enter}`); - cy.clickOutside(); - cy.get('.CodeMirror-scroll') - .scrollIntoView() - .click() - .type(profilerSetting.profileQuery); - - cy.get('[data-testid="include-column-select"]').scrollIntoView().click(); - cy.get('.ant-select-dropdown') - .not('.ant-select-dropdown-hidden') - .find(`[title="${profilerSetting.includeColumns}"]`) - .click(); - cy.get('[data-testid="enable-partition-switch"]') - .scrollIntoView() - .click(); - cy.get('[data-testid="interval-type"]').scrollIntoView().click(); - cy.get('.ant-select-dropdown') - .not('.ant-select-dropdown-hidden') - .find(`[title="${profilerSetting.partitionIntervalType}"]`) - .click(); - cy.get('#includeColumnsProfiler_partitionColumnName').click(); - cy.get('.ant-select-dropdown') - .not('.ant-select-dropdown-hidden') - .find(`[title="${profilerSetting.partitionColumnName}"]`) - .click(); - cy.get('[data-testid="partition-value"]') - .scrollIntoView() - .type(profilerSetting.partitionValues); - - interceptURL( - 'PUT', - '/api/v1/tables/*/tableProfilerConfig', - 'updateTableProfilerConfig' - ); - cy.get('.ant-modal-footer').contains('Save').scrollIntoView().click(); - cy.wait('@updateTableProfilerConfig').then(({ request }) => { - expect(request.body).to.deep.equal({ - excludeColumns: ['user_id'], - profileQuery: 'select * from table', - profileSample: 60, - profileSampleType: 'PERCENTAGE', - includeColumns: [{ columnName: 'shop_id' }], - partitioning: { - partitionColumnName: 'name', - partitionIntervalType: 'COLUMN-VALUE', - partitionValues: ['test'], - enablePartitioning: true, - }, - sampleDataCount: 100, - }); - }); - - cy.get('[data-testid="table_queries"]').click(); - cy.get('[data-testid="profiler"]').click(); - // verify profiler setting details - cy.get('[data-testid="profiler-setting-btn"]').click(); - // need extra time to load API response - verifyResponseStatusCode('@tableProfilerConfig', 200, { timeout: 10000 }); - - cy.get('[data-testid="slider-input"]').should( - 'have.value', - `${profilerSetting.profileSample}%` - ); - cy.get('.CodeMirror-scroll').should( - 'contain', - profilerSetting.profileQuery - ); - cy.get('[data-testid="exclude-column-select"]').should( - 'contain', - profilerSetting.excludeColumns - ); - cy.get('[data-testid="enable-partition-switch"]').should( - 'have.value', - 'true' - ); - cy.get('[data-testid="interval-type"]').should( - 'contain', - profilerSetting.partitionIntervalType - ); - cy.get('[data-testid="column-name"]').should( - 'contain', - profilerSetting.partitionColumnName - ); - cy.get('[data-testid="partition-value"]').should( - 'have.value', - profilerSetting.partitionValues - ); - }); - } -); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/service.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/service.ts index 8144a1bdc5e..9be3a9c6142 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/constant/service.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/service.ts @@ -42,6 +42,8 @@ export const POSTGRES = { tableName: 'order_items', }; +export const MYSQL = 'Mysql'; + export const HTTP_CONFIG_SOURCE = { DBT_CATALOG_HTTP_PATH: 'https://raw.githubusercontent.com/OnkarVO7/dbt_git_test/dbt_aut/catalog.json', diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataQualityAndProfiler.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataQualityAndProfiler.spec.ts new file mode 100644 index 00000000000..693cb465391 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataQualityAndProfiler.spec.ts @@ -0,0 +1,937 @@ +/* + * 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 { expect, Page, test } from '@playwright/test'; +import { getCurrentMillis } from '../../../src/utils/date-time/DateTimeUtils'; +import { SidebarItem } from '../../constant/sidebar'; +import { Domain } from '../../support/domain/Domain'; +import { TableClass } from '../../support/entity/TableClass'; +import { + assignDomain, + clickOutside, + createNewPage, + descriptionBox, + getApiContext, + redirectToHomePage, + toastNotification, + uuid, +} from '../../utils/common'; +import { visitEntityPage } from '../../utils/entity'; +import { sidebarClick } from '../../utils/sidebar'; +import { deleteTestCase, visitDataQualityTab } from '../../utils/testCases'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +const table1 = new TableClass(); +const table2 = new TableClass(); + +test.beforeAll(async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + await table1.create(apiContext); + await table2.create(apiContext); + const { testSuiteData } = await table2.createTestSuiteAndPipelines( + apiContext + ); + await table2.createTestCase(apiContext, { + name: `email_column_values_to_be_in_set_${uuid()}`, + entityLink: `<#E::table::${table2.entityResponseData?.['fullyQualifiedName']}::columns::email>`, + parameterValues: [ + { name: 'allowedValues', value: '["gmail","yahoo","collate"]' }, + ], + testDefinition: 'columnValuesToBeInSet', + testSuite: testSuiteData?.['fullyQualifiedName'], + }); + await afterAction(); +}); + +test.afterAll(async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + await table1.delete(apiContext); + await table2.delete(apiContext); + await afterAction(); +}); + +test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); +}); + +test('Table test case', async ({ page }) => { + test.slow(); + + const NEW_TABLE_TEST_CASE = { + name: `table_column_name_to_exist_in_id_${uuid()}`, + label: 'Table Column Name To Exist', + type: 'tableColumnNameToExist', + field: 'testCase', + description: 'New table test case for TableColumnNameToExist', + }; + await visitDataQualityTab(page, table1); + + await page.click('[data-testid="profiler-add-table-test-btn"]'); + await page.click('[data-testid="table"]'); + + await test.step('Create', async () => { + await page.click('#tableTestForm_testTypeId'); + await page.waitForSelector(`text=${NEW_TABLE_TEST_CASE.label}`); + await page.click(`text=${NEW_TABLE_TEST_CASE.label}`); + await page.fill('#tableTestForm_testName', NEW_TABLE_TEST_CASE.name); + await page.fill( + '#tableTestForm_params_columnName', + NEW_TABLE_TEST_CASE.field + ); + await page.fill(descriptionBox, NEW_TABLE_TEST_CASE.description); + await page.click('[data-testid="submit-test"]'); + + await page.waitForSelector('[data-testid="success-line"]'); + + await expect(page.locator('[data-testid="success-line"]')).toBeVisible(); + + await page.waitForSelector('[data-testid="add-ingestion-button"]'); + await page.click('[data-testid="add-ingestion-button"]'); + await page.click('[data-testid="select-all-test-cases"]'); + + // Schedule & Deploy + await page.click('[data-testid="cron-type"]'); + await page.waitForSelector('.ant-select-item-option-content'); + await page.click('.ant-select-item-option-content:has-text("Hour")'); + const ingestionPipelines = page.waitForResponse( + '/api/v1/services/ingestionPipelines' + ); + const deploy = page.waitForResponse( + '/api/v1/services/ingestionPipelines/deploy/*' + ); + const status = page.waitForResponse( + '/api/v1/services/ingestionPipelines/status' + ); + await page.click('[data-testid="deploy-button"]'); + await ingestionPipelines; + await deploy; + await status; + + // check success + await page.waitForSelector('[data-testid="success-line"]', { + timeout: 15000, + }); + + await expect(page.locator('[data-testid="success-line"]')).toBeVisible(); + await expect( + page.getByText('has been created and deployed successfully') + ).toBeVisible(); + + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases?fields=*' + ); + await page.click(`[data-testid="view-service-button"]`); + await testCaseResponse; + + await expect(page.getByTestId(NEW_TABLE_TEST_CASE.name)).toBeVisible(); + }); + + await test.step('Edit', async () => { + await page.click(`[data-testid="edit-${NEW_TABLE_TEST_CASE.name}"]`); + await page.waitForSelector('.ant-modal-title'); + await page.locator('#tableTestForm_params_columnName').clear(); + await page.fill('#tableTestForm_params_columnName', 'new_column_name'); + const updateTestCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/*' + ); + await page.locator('button').filter({ hasText: 'Submit' }).click(); + await updateTestCaseResponse; + await toastNotification(page, 'Test case updated successfully.'); + await page.click(`[data-testid="edit-${NEW_TABLE_TEST_CASE.name}"]`); + + await page.waitForSelector('#tableTestForm_params_columnName'); + + await expect(page.locator('#tableTestForm_params_columnName')).toHaveValue( + 'new_column_name' + ); + + await page.getByRole('button', { name: 'Cancel' }).click(); + }); + + await test.step('Delete', async () => { + await deleteTestCase(page, NEW_TABLE_TEST_CASE.name); + }); +}); + +test('Column test case', async ({ page }) => { + test.slow(); + + const NEW_COLUMN_TEST_CASE = { + name: 'email_column_value_lengths_to_be_between', + column: 'email', + type: 'columnValueLengthsToBeBetween', + label: 'Column Value Lengths To Be Between', + min: '3', + max: '6', + description: 'New table test case for columnValueLengthsToBeBetween', + }; + + await visitDataQualityTab(page, table1); + await page.click('[data-testid="profiler-add-table-test-btn"]'); + await page.click('[data-testid="column"]'); + + await test.step('Create', async () => { + const testDefinitionResponse = page.waitForResponse( + '/api/v1/dataQuality/testDefinitions?limit=*&entityType=COLUMN&testPlatform=OpenMetadata&supportedDataType=VARCHAR' + ); + await page.click('#tableTestForm_column'); + await page.click(`[title="${NEW_COLUMN_TEST_CASE.column}"]`); + await testDefinitionResponse; + await page.fill('#tableTestForm_testName', NEW_COLUMN_TEST_CASE.name); + await page.click('#tableTestForm_testTypeId'); + await page.click(`[title="${NEW_COLUMN_TEST_CASE.label}"]`); + await page.fill( + '#tableTestForm_params_minLength', + NEW_COLUMN_TEST_CASE.min + ); + await page.fill( + '#tableTestForm_params_maxLength', + NEW_COLUMN_TEST_CASE.max + ); + await page.fill(descriptionBox, NEW_COLUMN_TEST_CASE.description); + + await page.click('[data-testid="submit-test"]'); + await page.waitForSelector('[data-testid="success-line"]'); + + await expect(page.locator('[data-testid="success-line"]')).toBeVisible(); + + await page.waitForSelector('[data-testid="view-service-button"]'); + + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases?fields=*' + ); + await page.click(`[data-testid="view-service-button"]`); + await testCaseResponse; + + await page.waitForSelector(`[data-testid="${NEW_COLUMN_TEST_CASE.name}"]`); + + await expect( + page.locator(`[data-testid="${NEW_COLUMN_TEST_CASE.name}"]`) + ).toBeVisible(); + }); + + await test.step('Edit', async () => { + await page.click(`[data-testid="edit-${NEW_COLUMN_TEST_CASE.name}"]`); + await page.waitForSelector('#tableTestForm_params_minLength'); + await page.locator('#tableTestForm_params_minLength').clear(); + await page.fill('#tableTestForm_params_minLength', '4'); + const updateTestCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/*' + ); + await page.locator('button').getByText('Submit').click(); + await updateTestCaseResponse; + await toastNotification(page, 'Test case updated successfully.'); + + await page.click(`[data-testid="edit-${NEW_COLUMN_TEST_CASE.name}"]`); + await page.waitForSelector('#tableTestForm_params_minLength'); + const minLengthValue = await page + .locator('#tableTestForm_params_minLength') + .inputValue(); + + expect(minLengthValue).toBe('4'); + + await page.locator('button').getByText('Cancel').click(); + }); + + await test.step('Delete', async () => { + await deleteTestCase(page, NEW_COLUMN_TEST_CASE.name); + }); +}); + +test('Profiler matrix and test case graph should visible', async ({ page }) => { + const DATA_QUALITY_TABLE = { + term: 'dim_address', + serviceName: 'sample_data', + testCaseName: 'column_value_max_to_be_between', + }; + + await visitEntityPage({ + page, + searchTerm: DATA_QUALITY_TABLE.term, + dataTestId: `${DATA_QUALITY_TABLE.serviceName}-${DATA_QUALITY_TABLE.term}`, + }); + await page.waitForSelector(`[data-testid="entity-header-display-name"]`); + + await expect( + page.locator(`[data-testid="entity-header-display-name"]`) + ).toContainText(DATA_QUALITY_TABLE.term); + + const profilerResponse = page.waitForResponse( + `/api/v1/tables/*/tableProfile/latest` + ); + await page.click('[data-testid="profiler"]'); + await profilerResponse; + await page.waitForTimeout(1000); + await page + .getByRole('menuitem', { + name: 'Column Profile', + }) + .click(); + const getProfilerInfo = page.waitForResponse( + '/api/v1/tables/*/columnProfile?*' + ); + await page.locator('[data-row-key="shop_id"]').getByText('shop_id').click(); + await getProfilerInfo; + + await expect(page.locator('#count_graph')).toBeVisible(); + await expect(page.locator('#proportion_graph')).toBeVisible(); + await expect(page.locator('#math_graph')).toBeVisible(); + await expect(page.locator('#sum_graph')).toBeVisible(); + + await page + .getByRole('menuitem', { + name: 'Data Quality', + }) + .click(); + + await page.waitForSelector( + `[data-testid="${DATA_QUALITY_TABLE.testCaseName}"]` + ); + const getTestCaseDetails = page.waitForResponse( + '/api/v1/dataQuality/testCases/name/*?fields=*' + ); + const getTestResult = page.waitForResponse( + '/api/v1/dataQuality/testCases/*/testCaseResult?*' + ); + await page + .locator(`[data-testid="${DATA_QUALITY_TABLE.testCaseName}"]`) + .getByText(DATA_QUALITY_TABLE.testCaseName) + .click(); + + await getTestCaseDetails; + await getTestResult; + + await expect( + page.locator(`#${DATA_QUALITY_TABLE.testCaseName}_graph`) + ).toBeVisible(); +}); + +test('TestCase with Array params value', async ({ page }) => { + test.slow(); + + const testCase = table2.testCasesResponseData[0]; + const testCaseName = testCase?.['name']; + await visitDataQualityTab(page, table2); + + await test.step( + 'Array params value should be visible while editing the test case', + async () => { + await expect( + page.locator(`[data-testid="${testCaseName}"]`) + ).toBeVisible(); + await expect( + page.locator(`[data-testid="edit-${testCaseName}"]`) + ).toBeVisible(); + + await page.click(`[data-testid="edit-${testCaseName}"]`); + + await expect( + page.locator('#tableTestForm_params_allowedValues_0_value') + ).toHaveValue('gmail'); + await expect( + page.locator('#tableTestForm_params_allowedValues_1_value') + ).toHaveValue('yahoo'); + await expect( + page.locator('#tableTestForm_params_allowedValues_2_value') + ).toHaveValue('collate'); + } + ); + + await test.step('Validate patch request for edit test case', async () => { + await page.fill( + '#tableTestForm_displayName', + 'Table test case display name' + ); + + await expect(page.locator('#tableTestForm_table')).toHaveValue( + table2.entityResponseData?.['name'] + ); + await expect(page.locator('#tableTestForm_column')).toHaveValue('email'); + await expect(page.locator('#tableTestForm_name')).toHaveValue(testCaseName); + await expect(page.locator('#tableTestForm_testDefinition')).toHaveValue( + 'Column Values To Be In Set' + ); + + // Edit test case display name + const updateTestCaseResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/dataQuality/testCases/') && + response.request().method() === 'PATCH' + ); + await page.click('.ant-modal-footer >> text=Submit'); + const updateResponse1 = await updateTestCaseResponse; + const body1 = await updateResponse1.request().postData(); + + expect(body1).toEqual( + JSON.stringify([ + { + op: 'add', + path: '/displayName', + value: 'Table test case display name', + }, + ]) + ); + + // Edit test case description + await page.click(`[data-testid="edit-${testCaseName}"]`); + await page.fill(descriptionBox, 'Test case description'); + const updateTestCaseResponse2 = page.waitForResponse( + (response) => + response.url().includes('/api/v1/dataQuality/testCases/') && + response.request().method() === 'PATCH' + ); + await page.click('.ant-modal-footer >> text=Submit'); + const updateResponse2 = await updateTestCaseResponse2; + const body2 = await updateResponse2.request().postData(); + + expect(body2).toEqual( + JSON.stringify([ + { op: 'add', path: '/description', value: 'Test case description' }, + ]) + ); + + // Edit test case parameter values + await page.click(`[data-testid="edit-${testCaseName}"]`); + await page.fill('#tableTestForm_params_allowedValues_0_value', 'test'); + const updateTestCaseResponse3 = page.waitForResponse( + (response) => + response.url().includes('/api/v1/dataQuality/testCases/') && + response.request().method() === 'PATCH' + ); + await page.click('.ant-modal-footer >> text=Submit'); + const updateResponse3 = await updateTestCaseResponse3; + const body3 = await updateResponse3.request().postData(); + + expect(body3).toEqual( + JSON.stringify([ + { + op: 'replace', + path: '/parameterValues/0/value', + value: '["test","yahoo","collate"]', + }, + ]) + ); + }); + + await test.step( + 'Update test case display name from Data Quality page', + async () => { + const getTestCase = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await sidebarClick(page, SidebarItem.DATA_QUALITY); + await page.click('[data-testid="by-test-cases"]'); + await getTestCase; + const searchTestCaseResponse = page.waitForResponse( + `/api/v1/dataQuality/testCases/search/list?*q=*${testCaseName}*` + ); + await page.fill( + '[data-testid="test-case-container"] [data-testid="searchbar"]', + testCaseName + ); + await searchTestCaseResponse; + await page.waitForSelector('.ant-spin', { + state: 'detached', + }); + await page.click(`[data-testid="edit-${testCaseName}"]`); + await page.waitForSelector('.ant-modal-title'); + + await expect(page.locator('#tableTestForm_displayName')).toHaveValue( + 'Table test case display name' + ); + + await page.locator('#tableTestForm_displayName').clear(); + await page.fill('#tableTestForm_displayName', 'Updated display name'); + await page.click('.ant-modal-footer >> text=Submit'); + await toastNotification(page, 'Test case updated successfully.'); + + await expect(page.locator(`[data-testid="${testCaseName}"]`)).toHaveText( + 'Updated display name' + ); + } + ); +}); + +test('Update profiler setting modal', async ({ page }) => { + const profilerSetting = { + profileSample: '60', + sampleDataCount: '100', + profileQuery: 'select * from table', + excludeColumns: 'user_id', + includeColumns: 'shop_id', + partitionColumnName: 'name', + partitionIntervalType: 'COLUMN-VALUE', + partitionValues: 'test', + }; + + await table1.visitEntityPage(page); + await page.getByTestId('profiler').click(); + await page + .getByTestId('profiler-tab-left-panel') + .getByText('Table Profile') + .click(); + + await page.click('[data-testid="profiler-setting-btn"]'); + await page.waitForSelector('.ant-modal-body'); + await page.locator('[data-testid="slider-input"]').clear(); + await page + .locator('[data-testid="slider-input"]') + .fill(profilerSetting.profileSample); + + await page.locator('[data-testid="sample-data-count-input"]').clear(); + await page + .locator('[data-testid="sample-data-count-input"]') + .fill(profilerSetting.sampleDataCount); + await page.locator('[data-testid="exclude-column-select"]').click(); + await page.keyboard.type(`${profilerSetting.excludeColumns}`); + await page.keyboard.press('Enter'); + await page.locator('.CodeMirror-scroll').click(); + await page.keyboard.type(profilerSetting.profileQuery); + + await page.locator('[data-testid="include-column-select"]').click(); + await page + .locator('.ant-select-dropdown') + .locator( + `[title="${profilerSetting.includeColumns}"]:not(.ant-select-dropdown-hidden)` + ) + .last() + .click(); + await page.locator('[data-testid="enable-partition-switch"]').click(); + await page.locator('[data-testid="interval-type"]').click(); + await page + .locator('.ant-select-dropdown') + .locator( + `[title="${profilerSetting.partitionIntervalType}"]:not(.ant-select-dropdown-hidden)` + ) + .click(); + + await page.locator('#includeColumnsProfiler_partitionColumnName').click(); + await page + .locator('.ant-select-dropdown') + .locator( + `[title="${profilerSetting.partitionColumnName}"]:not(.ant-select-dropdown-hidden)` + ) + .last() + .click(); + await page + .locator('[data-testid="partition-value"]') + .fill(profilerSetting.partitionValues); + + const updateTableProfilerConfigResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/tables/') && + response.url().includes('/tableProfilerConfig') && + response.request().method() === 'PUT' + ); + await page.getByRole('button', { name: 'Save' }).click(); + const updateResponse = await updateTableProfilerConfigResponse; + const requestBody = await updateResponse.request().postData(); + + expect(requestBody).toEqual( + JSON.stringify({ + excludeColumns: ['user_id'], + profileQuery: 'select * from table', + profileSample: 60, + profileSampleType: 'PERCENTAGE', + includeColumns: [{ columnName: 'shop_id' }], + partitioning: { + partitionColumnName: 'name', + partitionIntervalType: 'COLUMN-VALUE', + partitionValues: ['test'], + enablePartitioning: true, + }, + sampleDataCount: 100, + }) + ); +}); + +test('TestCase filters', async ({ page }) => { + test.slow(); + + const { apiContext, afterAction } = await getApiContext(page); + const filterTable1 = new TableClass(); + + await filterTable1.create(apiContext); + const filterTable2 = { + ...filterTable1.entity, + name: `${filterTable1.entity.name}-model`, + }; + const filterTable2Response = await apiContext + .post('/api/v1/tables', { + data: filterTable2, + }) + .then((response) => response.json()); + const domain = new Domain(); + await domain.create(apiContext); + + // Add domain to table + await filterTable1.visitEntityPage(page); + await assignDomain(page, domain.responseData); + const testCases = [ + `pw_first_table_column_count_to_be_between_${uuid()}`, + `pw_second_table_column_count_to_be_between_${uuid()}`, + `pw_third_table_column_count_to_be_between_${uuid()}`, + ]; + const smilerNameTestCase = testCases.map((test) => `${test}_version_2`); + await filterTable1.patch({ + apiContext, + patchData: [ + { + op: 'add', + path: '/tags/0', + value: { + tagFQN: 'PII.None', + name: 'None', + description: 'Non PII', + source: 'Classification', + labelType: 'Manual', + state: 'Confirmed', + }, + }, + { + op: 'add', + path: '/tags/1', + value: { + tagFQN: 'Tier.Tier2', + name: 'Tier2', + source: 'Classification', + labelType: 'Manual', + state: 'Confirmed', + }, + }, + ], + }); + await filterTable1.createTestSuiteAndPipelines(apiContext); + const { testSuiteData: testSuite2Response } = + await filterTable1.createTestSuiteAndPipelines(apiContext, { + executableEntityReference: filterTable2Response?.['fullyQualifiedName'], + }); + + const testCaseResult = { + result: 'Found min=10001, max=27809 vs. the expected min=90001, max=96162.', + testCaseStatus: 'Failed', + testResultValue: [ + { + name: 'minValueForMaxInCol', + value: '10001', + }, + { + name: 'maxValueForMaxInCol', + value: '27809', + }, + ], + timestamp: getCurrentMillis(), + }; + for (let i = 0; i < testCases.length; i++) { + const testCase1 = await filterTable1.createTestCase(apiContext, { + name: testCases[i], + }); + await filterTable1.addTestCaseResult( + apiContext, + testCase1?.['fullyQualifiedName'], + testCaseResult + ); + const testCase2 = await filterTable1.createTestCase(apiContext, { + name: smilerNameTestCase[i], + entityLink: `<#E::table::${filterTable2Response?.['fullyQualifiedName']}>`, + testSuite: testSuite2Response?.['fullyQualifiedName'], + }); + await filterTable1.addTestCaseResult( + apiContext, + testCase2?.['fullyQualifiedName'], + testCaseResult + ); + } + + const verifyFilterTestCase = async (page: Page) => { + for (const testCase of testCases) { + const element = page.locator(`[data-testid="${testCase}"]`); + + await expect(element).toBeVisible(); + } + }; + + const verifyFilter2TestCase = async (page: Page, negation = false) => { + for (const testCase of smilerNameTestCase) { + const element = page.locator(`[data-testid="${testCase}"]`); + if (negation) { + await expect(element).not.toBeVisible(); + } else { + await expect(element).toBeVisible(); + } + } + }; + + try { + await sidebarClick(page, SidebarItem.DATA_QUALITY); + const getTestCaseListData = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await page.click('[data-testid="by-test-cases"]'); + await getTestCaseListData; + // get all the filters + await page.click('[data-testid="advanced-filter"]'); + await page.click('[value="tableFqn"]'); + await page.click('[data-testid="advanced-filter"]'); + await page.click('[value="testPlatforms"]'); + await page.click('[data-testid="advanced-filter"]'); + await page.click('[value="lastRunRange"]'); + await page.click('[data-testid="advanced-filter"]'); + await page.click('[value="serviceName"]'); + await page.click('[data-testid="advanced-filter"]'); + await page.click('[value="tags"]'); + await page.click('[data-testid="advanced-filter"]'); + await page.click('[value="tier"]'); + + // Test case search filter + const searchTestCaseResponse = page.waitForResponse( + (url) => + url.url().includes('/api/v1/dataQuality/testCases/search/list') && + url.url().includes(testCases[0]) + ); + await page.fill( + '[data-testid="test-case-container"] [data-testid="searchbar"]', + testCases[0] + ); + await searchTestCaseResponse; + + await expect(page.locator(`[data-testid="${testCases[0]}"]`)).toBeVisible(); + + // clear the search filter + const getTestCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await page.locator('.ant-input-clear-icon').click(); + await getTestCaseResponse; + + // Test case filter by service name + const serviceResponse = page.waitForResponse( + '/api/v1/search/query?q=*index=database_service_search_index*' + ); + await page.fill('#serviceName', filterTable1.service.name); + await serviceResponse; + + const testCaseByServiceName = page.waitForResponse( + `/api/v1/dataQuality/testCases/search/list?*serviceName=${filterTable1.service.name}*` + ); + await page + .locator('.ant-select-dropdown') + .filter({ + hasNot: page.locator('.ant-select-dropdown-hidden'), + has: page.locator(`[data-testid="${filterTable1.service.name}"]`), + }) + .click(); + await testCaseByServiceName; + await verifyFilterTestCase(page); + await verifyFilter2TestCase(page); + + // remove service filter + await page.click('[data-testid="advanced-filter"]'); + const getTestCase = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await page.click('[value="serviceName"]'); + await getTestCase; + + // Test case filter by Tags + const tagResponse = page.waitForResponse( + '/api/v1/search/query?q=*index=tag_search_index*' + ); + await page + .getByTestId('tags-select-filter') + .locator('div') + .filter({ hasText: 'Tags' }) + .click(); + await page.fill('#tags', 'PII.None'); + await tagResponse; + + const getTestCaseByTag = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*tags=PII.None*' + ); + await page + .locator('.ant-select-dropdown') + .filter({ + hasNot: page.locator('.ant-select-dropdown-hidden'), + has: page.locator(`[data-testid="PII.None"]`), + }) + .click(); + await getTestCaseByTag; + await verifyFilterTestCase(page); + await verifyFilter2TestCase(page, true); + + // remove tags filter + await page.click('[data-testid="advanced-filter"]'); + const getTestCaseWithoutTag = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await page.click('[value="tags"]'); + await getTestCaseWithoutTag; + + // Test case filter by Tier + + await page.click('#tier'); + const getTestCaseByTier = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*tier=Tier.Tier2*' + ); + await page.getByTestId('Tier.Tier2').getByText('Tier.Tier2').click(); + await getTestCaseByTier; + await verifyFilterTestCase(page); + await verifyFilter2TestCase(page, true); + + // remove tier filter + await page.click('[data-testid="advanced-filter"]'); + const getTestCaseWithoutTier = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await page.click('[value="tier"]'); + await getTestCaseWithoutTier; + + // Test case filter by table name + const tableSearchResponse = page.waitForResponse( + `/api/v1/search/query?q=*index=table_search_index*` + ); + await page.fill('#tableFqn', filterTable1.entity.name); + await tableSearchResponse; + const getTestCaseByTable = page.waitForResponse( + `/api/v1/dataQuality/testCases/search/list?*entityLink=*${filterTable1.entity.name}*` + ); + + await page + .getByTestId(filterTable1.entityResponseData?.['fullyQualifiedName']) + .click(); + await getTestCaseByTable; + await verifyFilterTestCase(page); + await verifyFilter2TestCase(page, true); + + // Test case filter by test type column + const testCaseTypeByColumn = page.waitForResponse( + `/api/v1/dataQuality/testCases/search/list?*testCaseType=column*` + ); + await page.getByTestId('test-case-type-select-filter').click(); + await page.getByTitle('Column').click(); + await testCaseTypeByColumn; + + await expect( + page.locator('[data-testid="search-error-placeholder"]') + ).toBeVisible(); + + // Test case filter by test type table + const testCaseTypeByTable = page.waitForResponse( + `/api/v1/dataQuality/testCases/search/list?*testCaseType=table*` + ); + await page.getByTestId('test-case-type-select-filter').click(); + await page + .locator('.ant-select-dropdown') + .filter({ + hasNot: page.locator('.ant-select-dropdown-hidden'), + has: page.locator(`[title="Table"]`), + hasText: 'Table', + }) + .click(); + await testCaseTypeByTable; + await verifyFilterTestCase(page); + + // Test case filter by test type all + const testCaseTypeByAll = page.waitForResponse( + `/api/v1/dataQuality/testCases/search/list?*testCaseType=all*` + ); + await page.getByTestId('test-case-type-select-filter').click(); + await page.getByTitle('All').nth(1).click(); + await testCaseTypeByAll; + + // Test case filter by status + const testCaseStatusBySuccess = page.waitForResponse( + `/api/v1/dataQuality/testCases/search/list?*testCaseStatus=Success*` + ); + await page.getByTestId('status-select-filter').click(); + await page.getByTitle('Success').click(); + await testCaseStatusBySuccess; + + await expect( + page.locator('[data-testid="search-error-placeholder"]') + ).toBeVisible(); + + // Test case filter by status + const testCaseStatusByFailed = page.waitForResponse( + `/api/v1/dataQuality/testCases/search/list?*testCaseStatus=Failed*` + ); + await page.getByTestId('status-select-filter').click(); + await page.getByTitle('Failed').click(); + await testCaseStatusByFailed; + await verifyFilterTestCase(page); + await verifyFilter2TestCase(page, true); + + // Test case filter by platform + const testCasePlatformByDBT = page.waitForResponse( + `/api/v1/dataQuality/testCases/search/list?*testPlatforms=DBT*` + ); + await page.getByTestId('platform-select-filter').click(); + await page.getByTitle('DBT').click(); + await testCasePlatformByDBT; + + await expect( + page.locator('[data-testid="search-error-placeholder"]') + ).toBeVisible(); + + const getTestCaseWithoutPlatform = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await page + .getByTestId('platform-select-filter') + .locator('.ant-select-clear') + .click(); + await getTestCaseWithoutPlatform; + const testCasePlatformByOpenMetadata = page.waitForResponse( + `/api/v1/dataQuality/testCases/search/list?*testPlatforms=OpenMetadata*` + ); + await page.getByTestId('platform-select-filter').click(); + await page.getByTitle('OpenMetadata').click(); + await testCasePlatformByOpenMetadata; + await clickOutside(page); + await verifyFilterTestCase(page); + await verifyFilter2TestCase(page, true); + const url = page.url(); + await page.reload(); + + await expect(page.url()).toBe(url); + + await page.getByTestId('advanced-filter').click(); + await page.click('[value="testPlatforms"]'); + await page.waitForTimeout(200); + + await expect(page.getByTestId('platform-select-filter')).not.toBeVisible(); + + await page.reload(); + + await expect(page.locator('[value="tier"]')).not.toBeVisible(); + + // Apply domain globally + await page.locator('[data-testid="domain-dropdown"]').click(); + await page + .locator(`li[data-menu-id*='${domain.responseData?.['name']}']`) + .click(); + await sidebarClick(page, SidebarItem.DATA_QUALITY); + const getTestCaseList = page.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?*' + ); + await page.click('[data-testid="by-test-cases"]'); + await getTestCaseList; + await verifyFilterTestCase(page); + await verifyFilter2TestCase(page, true); + } finally { + await filterTable1.delete(apiContext); + await domain.delete(apiContext); + await afterAction(); + } +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceIngestion.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceIngestion.spec.ts index 8ae41d224d0..6046e3d3aa8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceIngestion.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceIngestion.spec.ts @@ -12,7 +12,7 @@ */ import test, { expect } from '@playwright/test'; -import { POSTGRES, REDSHIFT } from '../../constant/service'; +import { MYSQL, POSTGRES, REDSHIFT } from '../../constant/service'; import { GlobalSettingOptions } from '../../constant/settings'; import AirflowIngestionClass from '../../support/entity/ingestion/AirflowIngestionClass'; import BigQueryIngestionClass from '../../support/entity/ingestion/BigQueryIngestionClass'; @@ -83,7 +83,9 @@ services.forEach((ServiceClass) => { }); if ( - [POSTGRES.serviceType, REDSHIFT.serviceType].includes(service.serviceType) + [POSTGRES.serviceType, REDSHIFT.serviceType, MYSQL].includes( + service.serviceType + ) ) { test(`Service specific tests`, async ({ page }) => { await service.runAdditionalTests(page, test); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestCases.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestCases.spec.ts index 23674df4e52..79030666d1e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestCases.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestCases.spec.ts @@ -12,8 +12,12 @@ */ import { expect, test } from '@playwright/test'; import { TableClass } from '../../support/entity/TableClass'; -import { getApiContext, redirectToHomePage } from '../../utils/common'; -import { deleteTestCase } from '../../utils/testCases'; +import { + descriptionBox, + getApiContext, + redirectToHomePage, +} from '../../utils/common'; +import { deleteTestCase, visitDataQualityTab } from '../../utils/testCases'; // use the admin user to login test.use({ storageState: 'playwright/.auth/admin.json' }); @@ -231,3 +235,104 @@ test('Custom SQL Query', async ({ page }) => { await afterAction(); } }); + +test('Column Values To Be Not Null', async ({ page }) => { + test.slow(); + + const NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE = { + name: 'id_column_values_to_be_not_null', + displayName: 'ID Column Values To Be Not Null', + column: 'user_id', + type: 'columnValuesToBeNotNull', + label: 'Column Values To Be Not Null', + description: 'New table test case for columnValuesToBeNotNull', + }; + await redirectToHomePage(page); + const { afterAction, apiContext } = await getApiContext(page); + const table = new TableClass(); + await table.create(apiContext); + + await visitDataQualityTab(page, table); + await page.click('[data-testid="profiler-add-table-test-btn"]'); + await page.click('[data-testid="column"]'); + + try { + await test.step('Create', async () => { + const testDefinitionResponse = page.waitForResponse( + '/api/v1/dataQuality/testDefinitions?limit=*&entityType=COLUMN&testPlatform=OpenMetadata&supportedDataType=NUMERIC' + ); + await page.click('#tableTestForm_column'); + await page.click( + `[title="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.column}"]` + ); + await testDefinitionResponse; + await page.fill( + '#tableTestForm_testName', + NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name + ); + + await page.fill( + '#tableTestForm_testTypeId', + NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.type + ); + await page.click( + `[title="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.label}"]` + ); + await page.fill( + descriptionBox, + NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.description + ); + + await page.click('[data-testid="submit-test"]'); + await page.waitForSelector('[data-testid="success-line"]'); + await page.waitForSelector('[data-testid="view-service-button"]'); + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases?fields=*' + ); + await page.click(`[data-testid="view-service-button"]`); + await testCaseResponse; + await page.click('[data-testid="profiler-tab-left-panel"]'); + + await expect( + page.locator( + `[data-testid="${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name}"]` + ) + ).toBeVisible(); + }); + + await test.step('Edit', async () => { + await page + .getByTestId(`edit-${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name}`) + .click(); + + await expect(page.locator('.ant-modal-title')).toHaveText( + `Edit ${NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name}` + ); + await expect(page.locator('#tableTestForm_name')).toHaveValue( + NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name + ); + + await page.locator('#tableTestForm_displayName').clear(); + await page.fill( + '#tableTestForm_displayName', + NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.displayName + ); + await page.getByText('New table test case for').first().click(); + await page.keyboard.type(' update'); + await page.getByRole('button', { name: 'Submit' }).click(); + + await expect(page.getByRole('alert')).toContainText( + 'Test case updated successfully.' + ); + + await page.getByTestId('content-wrapper').getByLabel('close').click(); + }); + + await test.step('Delete', async () => { + await deleteTestCase(page, NEW_COLUMN_TEST_CASE_WITH_NULL_TYPE.name); + }); + } finally { + await table.delete(apiContext); + await afterAction(); + } +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestSuite.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestSuite.spec.ts new file mode 100644 index 00000000000..73f7ac03fd4 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestSuite.spec.ts @@ -0,0 +1,223 @@ +/* + * 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 { expect, test } from '@playwright/test'; +import { SidebarItem } from '../../constant/sidebar'; +import { EntityTypeEndpoint } from '../../support/entity/Entity.interface'; +import { TableClass } from '../../support/entity/TableClass'; +import { UserClass } from '../../support/user/UserClass'; +import { + createNewPage, + descriptionBox, + redirectToHomePage, + toastNotification, + uuid, +} from '../../utils/common'; +import { addMultiOwner, removeOwnersFromList } from '../../utils/entity'; +import { sidebarClick } from '../../utils/sidebar'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +const table = new TableClass(); +const user1 = new UserClass(); +const user2 = new UserClass(); + +test.beforeAll(async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + await table.create(apiContext); + await user1.create(apiContext); + await user2.create(apiContext); + await table.createTestCase(apiContext); + await table.createTestCase(apiContext); + await afterAction(); +}); + +test.afterAll(async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + await table.delete(apiContext); + await user1.delete(apiContext); + await user2.delete(apiContext); + await afterAction(); +}); + +test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); +}); + +test('Logical TestSuite', async ({ page }) => { + const NEW_TEST_SUITE = { + name: `mysql_matrix-${uuid()}`, + description: 'mysql critical matrix', + }; + const testCaseName1 = table.testCasesResponseData?.[0]?.['name']; + const testCaseName2 = table.testCasesResponseData?.[1]?.['name']; + await sidebarClick(page, SidebarItem.DATA_QUALITY); + const testSuite = page.waitForResponse( + '/api/v1/dataQuality/testSuites/search/list?*testSuiteType=logical*' + ); + await page.click('[data-testid="by-test-suites"]'); + await testSuite; + + await test.step('Create', async () => { + await page.click('[data-testid="add-test-suite-btn"]'); + await page.fill('[data-testid="test-suite-name"]', NEW_TEST_SUITE.name); + await page.fill(descriptionBox, NEW_TEST_SUITE.description); + + await page.click('[data-testid="submit-button"]'); + const getTestCase = page.waitForResponse( + '/api/v1/search/query?q=*&index=test_case_search_index*' + ); + await page.fill('[data-testid="searchbar"]', testCaseName1); + await getTestCase; + + await page.click(`[data-testid="${testCaseName1}"]`); + await page.click('[data-testid="submit"]'); + + await page.waitForSelector('[data-testid="success-line"]', { + state: 'visible', + }); + + await expect(page.locator('[data-testid="success-line"]')).toContainText( + 'has been created successfully' + ); + + const testSuiteResponse = page.waitForResponse( + '/api/v1/dataQuality/testSuites/name/*' + ); + await page.click(`[data-testid="view-service-button"]`); + await testSuiteResponse; + }); + + await test.step( + 'User as Owner assign, update & delete for test suite', + async () => { + await addMultiOwner({ + page, + ownerNames: [user1.getUserName()], + activatorBtnDataTestId: 'edit-owner', + endpoint: EntityTypeEndpoint.TestSuites, + type: 'Users', + }); + await removeOwnersFromList({ + page, + ownerNames: [user1.getUserName()], + endpoint: EntityTypeEndpoint.TestSuites, + }); + await addMultiOwner({ + page, + ownerNames: [user2.getUserName()], + activatorBtnDataTestId: 'edit-owner', + endpoint: EntityTypeEndpoint.TestSuites, + type: 'Users', + }); + } + ); + + await test.step('Add test case to logical test suite', async () => { + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases?fields=*' + ); + await page.click('[data-testid="add-test-case-btn"]'); + await testCaseResponse; + + const getTestCase = page.waitForResponse( + '/api/v1/search/query?q=*&index=test_case_search_index*' + ); + await page.fill('[data-testid="searchbar"]', testCaseName2); + await getTestCase; + + await page.click(`[data-testid="${testCaseName2}"]`); + const updateTestCase = page.waitForResponse( + '/api/v1/dataQuality/testCases/logicalTestCases' + ); + await page.click('[data-testid="submit"]'); + await updateTestCase; + await page.waitForSelector('.ant-modal-content', { + state: 'detached', + }); + }); + + await test.step('Remove test case from logical test suite', async () => { + await page.click(`[data-testid="remove-${testCaseName1}"]`); + const removeTestCase1 = page.waitForResponse( + '/api/v1/dataQuality/testCases/logicalTestCases/*/*' + ); + await page.click('[data-testid="save-button"]'); + await removeTestCase1; + await page.click(`[data-testid="remove-${testCaseName2}"]`); + const removeTestCase2 = page.waitForResponse( + '/api/v1/dataQuality/testCases/logicalTestCases/*/*' + ); + await page.click('[data-testid="save-button"]'); + await removeTestCase2; + }); + + await test.step('Test suite filters', async () => { + const owner = user2.getUserName(); + const testSuite = page.waitForResponse( + '/api/v1/dataQuality/testSuites/search/list?*testSuiteType=logical*' + ); + await page.getByRole('link', { name: 'Test Suites' }).click(); + await testSuite; + + await page.click('[data-testid="owner-select-filter"]'); + await page.waitForSelector("[data-testid='select-owner-tabs']", { + state: 'visible', + }); + const getOwnerList = page.waitForResponse('/api/v1/users?*isBot=false*'); + await page.click('.ant-tabs [id*=tab-users]'); + await getOwnerList; + await page.waitForSelector(`[data-testid="loader"]`, { + state: 'detached', + }); + + const searchOwner = page.waitForResponse( + 'api/v1/search/query?q=*&index=user_search_index*' + ); + await page.fill('[data-testid="owner-select-users-search-bar"]', owner); + await searchOwner; + + const testSuiteByOwner = page.waitForResponse( + '/api/v1/dataQuality/testSuites/search/list?*owner=*' + ); + await page.click(`.ant-popover [title="${owner}"]`); + await testSuiteByOwner; + await page.waitForSelector(`[data-testid="${NEW_TEST_SUITE.name}"]`, { + state: 'visible', + }); + + await expect( + page.locator(`[data-testid="${NEW_TEST_SUITE.name}"]`) + ).toBeVisible(); + + await page.click(`[data-testid="${NEW_TEST_SUITE.name}"]`); + }); + + await test.step('Delete', async () => { + await page.click('[data-testid="manage-button"]'); + await page.click('[data-testid="delete-button"]'); + + // Click on Permanent/Hard delete option + await page.click('[data-testid="hard-delete-option"]'); + await page.fill('[data-testid="confirmation-text-input"]', 'DELETE'); + const deleteResponse = page.waitForResponse( + '/api/v1/dataQuality/testSuites/*?hardDelete=true&recursive=true' + ); + await page.click('[data-testid="confirm-button"]'); + await deleteResponse; + await toastNotification( + page, + `"${NEW_TEST_SUITE.name}" deleted successfully!` + ); + }); +}); 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 56073e0f5ed..a21e2615103 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 @@ -38,6 +38,7 @@ export enum EntityTypeEndpoint { API_COLLECTION = 'apiCollections', API_ENDPOINT = 'apiEndpoints', DATA_PRODUCT = 'dataProducts', + TestSuites = 'dataQuality/testSuites', } export type EntityDataType = { @@ -67,6 +68,15 @@ export enum ENTITY_PATH { } export type TestCaseData = { - parameterValues: unknown[]; - testDefinition: string; + parameterValues?: unknown[]; + name?: string; + entityLink?: string; + testDefinition?: string; + testSuite?: string; +}; + +export type TestSuiteData = { + name?: string; + executableEntityReference?: string; + description?: 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 cda4f17aab8..6e3c88b7921 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 @@ -11,10 +11,15 @@ * limitations under the License. */ import { APIRequestContext, Page } from '@playwright/test'; +import { Operation } from 'fast-json-patch'; import { SERVICE_TYPE } from '../../constant/service'; import { uuid } from '../../utils/common'; import { visitEntityPage } from '../../utils/entity'; -import { EntityTypeEndpoint, TestCaseData } from './Entity.interface'; +import { + EntityTypeEndpoint, + TestCaseData, + TestSuiteData, +} from './Entity.interface'; import { EntityClass } from './EntityClass'; export class TableClass extends EntityClass { @@ -167,7 +172,10 @@ export class TableClass extends EntityClass { }); } - async createTestSuiteAndPipelines(apiContext: APIRequestContext) { + async createTestSuiteAndPipelines( + apiContext: APIRequestContext, + testSuite?: TestSuiteData + ) { if (!this.entityResponseData) { await this.create(apiContext); } @@ -179,6 +187,7 @@ export class TableClass extends EntityClass { executableEntityReference: this.entityResponseData?.['fullyQualifiedName'], description: 'Playwright test suite for table', + ...testSuite, }, }) .then((res) => res.json()); @@ -240,13 +249,13 @@ export class TableClass extends EntityClass { data: { name: `pw-test-case-${uuid()}`, entityLink: `<#E::table::${this.entityResponseData?.['fullyQualifiedName']}>`, - testDefinition: - testCaseData?.testDefinition ?? 'tableRowCountToBeBetween', + testDefinition: 'tableRowCountToBeBetween', testSuite: this.testSuiteResponseData?.['fullyQualifiedName'], - parameterValues: testCaseData?.parameterValues ?? [ + parameterValues: [ { name: 'minValue', value: 12 }, { name: 'maxValue', value: 34 }, ], + ...testCaseData, }, }) .then((res) => res.json()); @@ -256,6 +265,43 @@ export class TableClass extends EntityClass { return testCase; } + async addTestCaseResult( + apiContext: APIRequestContext, + testCaseFqn: string, + testCaseResult: unknown + ) { + const testCaseResultResponse = await apiContext.put( + `/api/v1/dataQuality/testCases/${testCaseFqn}/testCaseResult`, + { data: testCaseResult } + ); + + return await testCaseResultResponse.json(); + } + + async patch({ + apiContext, + patchData, + }: { + apiContext: APIRequestContext; + patchData: Operation[]; + }) { + const response = await apiContext.patch( + `/api/v1/tables/name/${this.entityResponseData?.['fullyQualifiedName']}`, + { + data: patchData, + headers: { + 'Content-Type': 'application/json-patch+json', + }, + } + ); + + this.entityResponseData = await response.json(); + + return { + entity: this.entityResponseData, + }; + } + async delete(apiContext: APIRequestContext) { const serviceResponse = await apiContext.delete( `/api/v1/services/databaseServices/name/${encodeURIComponent( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/MySqlIngestionClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/MySqlIngestionClass.ts index c9be55968b3..7a80598b3f3 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/MySqlIngestionClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/MySqlIngestionClass.ts @@ -10,9 +10,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { expect, Page } from '@playwright/test'; +import { + expect, + Page, + PlaywrightTestArgs, + PlaywrightWorkerArgs, + TestType, +} from '@playwright/test'; import { env } from 'process'; -import { uuid } from '../../../utils/common'; +import { + getApiContext, + redirectToHomePage, + toastNotification, + uuid, +} from '../../../utils/common'; +import { visitEntityPage } from '../../../utils/entity'; +import { visitServiceDetailsPage } from '../../../utils/service'; import { checkServiceFieldSectionHighlighting, Services, @@ -22,6 +35,7 @@ import ServiceBaseClass from './ServiceBaseClass'; class MysqlIngestionClass extends ServiceBaseClass { name: string; tableFilter: string[]; + profilerTable = 'alert_entity'; constructor() { super( Services.Database, @@ -62,6 +76,84 @@ class MysqlIngestionClass extends ServiceBaseClass { } } + async runAdditionalTests( + page: Page, + test: TestType + ) { + await test.step('Add Profiler ingestion', async () => { + const { apiContext } = await getApiContext(page); + await redirectToHomePage(page); + await visitServiceDetailsPage( + page, + { + type: this.category, + name: this.serviceName, + displayName: this.serviceName, + }, + true + ); + + await page.click('[data-testid="ingestions"]'); + await page.waitForSelector('[data-testid="ingestion-details-container"]'); + await page.waitForTimeout(1000); + await page.click('[data-testid="add-new-ingestion-button"]'); + await page.waitForTimeout(1000); + await page.click('[data-menu-id*="profiler"]'); + + await page.waitForSelector('#root\\/profileSample'); + await page.fill('#root\\/profileSample', '10'); + await page.click('[data-testid="submit-btn"]'); + // Make sure we create ingestion with None schedule to avoid conflict between Airflow and Argo behavior + await this.scheduleIngestion(page); + + await page.click('[data-testid="view-service-button"]'); + + // Header available once page loads + await page.waitForSelector('[data-testid="data-assets-header"]'); + await page.getByTestId('loader').waitFor({ state: 'detached' }); + await page.getByTestId('ingestions').click(); + await page + .getByLabel('Ingestions') + .getByTestId('loader') + .waitFor({ state: 'detached' }); + + const response = await apiContext + .get( + `/api/v1/services/ingestionPipelines?service=${encodeURIComponent( + this.serviceName + )}&pipelineType=profiler&serviceType=databaseService&limit=1` + ) + .then((res) => res.json()); + + await page.click( + `[data-row-key*="${response.data[0].name}"] [data-testid="more-actions"]` + ); + await page.getByTestId('run-button').click(); + + await toastNotification(page, `Pipeline triggered successfully!`); + + await this.handleIngestionRetry('profiler', page); + }); + + await test.step('Validate profiler ingestion', async () => { + await visitEntityPage({ + page, + searchTerm: this.profilerTable, + dataTestId: `${this.serviceName}-${this.profilerTable}`, + }); + }); + + await page.getByTestId('profiler').click(); + await page + .getByTestId('profiler-tab-left-panel') + .getByText('Table Profile') + .click(); + + await expect( + page.locator('[data-testid="no-profiler-placeholder"]') + ).not.toBeVisible(); + } + async validateIngestionDetails(page: Page) { await page.waitForSelector('.ant-select-selection-item-content'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts index c5a408a4e7c..f1e9a4a4206 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ingestion/ServiceBaseClass.ts @@ -176,10 +176,10 @@ class ServiceBaseClass { .getByLabel('Ingestions') .getByTestId('loader') .waitFor({ state: 'detached' }); - + // need manual wait to settle down the deployed pipeline, before triggering the pipeline await page.waitForTimeout(2000); - + await page.getByTestId('more-actions').first().click(); await page.getByTestId('run-button').click(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/testCases.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/testCases.ts index af1e136e24a..625171ac35b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/testCases.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/testCases.ts @@ -11,6 +11,7 @@ * limitations under the License. */ import { expect, Page } from '@playwright/test'; +import { TableClass } from '../support/entity/TableClass'; export const deleteTestCase = async (page: Page, testCaseName: string) => { await page.getByTestId(`delete-${testCaseName}`).click(); @@ -18,7 +19,24 @@ export const deleteTestCase = async (page: Page, testCaseName: string) => { await expect(page.getByTestId('confirm-button')).toBeEnabled(); + const deleteResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases/*?hardDelete=true&recursive=true' + ); await page.getByTestId('confirm-button').click(); + await deleteResponse; await expect(page.getByRole('alert')).toHaveText(/deleted successfully!/); }; + +export const visitDataQualityTab = async (page: Page, table: TableClass) => { + await table.visitEntityPage(page); + await page.getByTestId('profiler').click(); + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases?fields=*' + ); + await page + .getByTestId('profiler-tab-left-panel') + .getByText('Data Quality') + .click(); + await testCaseResponse; +};