From d4f643bd10b35efa9447369dee9caec168e9d7ce Mon Sep 17 00:00:00 2001 From: Pranita Fulsundar Date: Tue, 5 Aug 2025 22:25:53 +0530 Subject: [PATCH] test(ui): e2e tests for data contracts (#22737) * add test for create data contract * add test * add test * fix tests * add test for data contract export and delete * refactor test --- .../ui/playwright/constant/dataContracts.ts | 52 +++ .../e2e/Features/CuratedAssets.spec.ts | 10 +- .../Features/DescriptionSuggestion.spec.ts | 4 +- .../e2e/Pages/DataContracts.spec.ts | 425 ++++++++++++++++++ .../DataAssetsHeader.component.tsx | 1 + .../AddDataContract/AddDataContract.tsx | 8 +- .../ContractDetailFormTab.tsx | 5 +- .../ContractDetailTab/ContractDetail.tsx | 10 +- .../ContractSemanticFormTab.tsx | 4 + .../ContractViewSwitchTab.component.tsx | 2 + 10 files changed, 506 insertions(+), 15 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/constant/dataContracts.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/dataContracts.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/dataContracts.ts new file mode 100644 index 00000000000..00e05abc1ab --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/dataContracts.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2025 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 { uuid } from '../utils/common'; + +export const DATA_CONTRACT_DETAILS = { + name: `data_contract_${uuid()}`, + description: 'new data contract description', +}; + +export const DATA_CONTRACT_SEMANTICS1 = { + name: `data_contract_semantic_1_${uuid()}`, + description: 'new data contract semantic description 1', + rules: [ + { + field: 'Owners', + operator: 'Is', + }, + { + field: 'Description', + operator: 'Is Set', + }, + ], +}; + +export const DATA_CONTRACT_SEMANTICS2 = { + name: `data_contract_semantic_2_${uuid()}`, + description: 'new data contract semantic description 2', + rules: [ + { + field: 'Name', + operator: 'Is Set', + }, + ], +}; + +export 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', +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts index bb41dfeddbb..2b482b6118a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CuratedAssets.spec.ts @@ -112,19 +112,13 @@ test.describe('Curated Assets', () => { await selectOption( page, ruleLocator2.locator('.rule--field .ant-select'), - 'Description' + 'Deleted' ); await selectOption( page, ruleLocator2.locator('.rule--operator .ant-select'), - '==' - ); - - await selectOption( - page, - ruleLocator2.locator('.rule--value .ant-select'), - 'Complete' + 'Is not null' ); await expect(page.locator('[data-testid="saveButton"]')).toBeEnabled(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DescriptionSuggestion.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DescriptionSuggestion.spec.ts index 115ff19231b..ce9b512b8f5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DescriptionSuggestion.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DescriptionSuggestion.spec.ts @@ -157,9 +157,7 @@ test.describe('Description Suggestions Table Entity', () => { state: 'detached', }); - await expect(page.locator('.ant-badge .ant-badge-count')).toContainText( - '6' - ); + await expect(page.locator('.ant-badge [title="6"]')).toBeVisible(); await expect( page.locator( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts new file mode 100644 index 00000000000..2dcf6d29afe --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataContracts.spec.ts @@ -0,0 +1,425 @@ +/* + * Copyright 2025 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 { + DATA_CONTRACT_DETAILS, + DATA_CONTRACT_SEMANTICS1, + DATA_CONTRACT_SEMANTICS2, + NEW_TABLE_TEST_CASE, +} from '../../constant/dataContracts'; +import { EntityTypeEndpoint } from '../../support/entity/Entity.interface'; +import { TableClass } from '../../support/entity/TableClass'; +import { Glossary } from '../../support/glossary/Glossary'; +import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; +import { ClassificationClass } from '../../support/tag/ClassificationClass'; +import { TagClass } from '../../support/tag/TagClass'; +import { UserClass } from '../../support/user/UserClass'; +import { selectOption } from '../../utils/advancedSearch'; +import { + clickOutside, + createNewPage, + redirectToHomePage, + toastNotification, +} from '../../utils/common'; +import { addOwner } from '../../utils/entity'; + +test.use({ storageState: 'playwright/.auth/admin.json' }); + +test.describe('Data Contracts', () => { + const table = new TableClass(); + const user = new UserClass(); + const testClassification = new ClassificationClass(); + const testTag = new TagClass({ + classification: testClassification.data.name, + }); + const testGlossary = new Glossary(); + const testGlossaryTerm = new GlossaryTerm(testGlossary); + + test.beforeAll('Setup pre-requests', async ({ browser }) => { + test.slow(true); + + const { apiContext, afterAction } = await createNewPage(browser); + await table.create(apiContext); + await user.create(apiContext); + await testClassification.create(apiContext); + await testTag.create(apiContext); + await testGlossary.create(apiContext); + await testGlossaryTerm.create(apiContext); + await afterAction(); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + test.slow(true); + + const { apiContext, afterAction } = await createNewPage(browser); + await table.delete(apiContext); + await user.delete(apiContext); + await testClassification.delete(apiContext); + await testTag.delete(apiContext); + await testGlossary.delete(apiContext); + await testGlossaryTerm.delete(apiContext); + await afterAction(); + }); + + test('Create Data Contract and validate', async ({ page }) => { + test.slow(true); + + await test.step('Redirect to Home Page and visit entity', async () => { + await redirectToHomePage(page); + await table.visitEntityPage(page); + }); + + await test.step( + 'Open contract section and start adding contract', + async () => { + await page.click('[data-testid="contract"]'); + + await expect(page.getByTestId('no-data-placeholder')).toBeVisible(); + await expect(page.getByTestId('add-contract-button')).toBeVisible(); + + await page.getByTestId('add-contract-button').click(); + + await expect(page.getByTestId('add-contract-card')).toBeVisible(); + } + ); + + await test.step('Fill Contract Details form', async () => { + await page.getByTestId('contract-name').fill(DATA_CONTRACT_DETAILS.name); + await page.fill( + '.om-block-editor[contenteditable="true"]', + DATA_CONTRACT_DETAILS.description + ); + + await page.getByTestId('add-owner').click(); + await page.getByRole('tab', { name: 'Users' }).click(); + await page + .getByTestId('owner-select-users-search-bar') + .fill(user.responseData.displayName); + await page + .getByRole('listitem', { + name: user.responseData.displayName, + exact: true, + }) + .click(); + await page.getByTestId('selectable-list-update-btn').click(); + + await expect( + page + .getByTestId('owner-link') + .getByTestId(user.responseData.displayName) + ).toBeVisible(); + }); + + await test.step('Fill Contract Schema form', async () => { + await page.getByRole('button', { name: 'Schema' }).click(); + + await page + .locator('input[type="checkbox"][aria-label="Select all"]') + .check(); + + await expect( + page.getByRole('checkbox', { name: 'Select all' }) + ).toBeChecked(); + }); + + await test.step('Fill first Contract Semantics form', async () => { + await page.getByRole('button', { name: 'Semantics' }).click(); + + await expect(page.getByTestId('add-semantic-button')).toBeDisabled(); + + await page.fill('#semantics_0_name', DATA_CONTRACT_SEMANTICS1.name); + await page.fill( + '#semantics_0_description', + DATA_CONTRACT_SEMANTICS1.description + ); + + await expect(page.locator('#semantics_0_enabled')).toHaveAttribute( + 'aria-checked', + 'true' + ); + + const ruleLocator = page.locator('.group').nth(0); + await selectOption( + page, + ruleLocator.locator('.group--field .ant-select'), + DATA_CONTRACT_SEMANTICS1.rules[0].field + ); + await selectOption( + page, + ruleLocator.locator('.rule--operator .ant-select'), + DATA_CONTRACT_SEMANTICS1.rules[0].operator + ); + await selectOption( + page, + ruleLocator.locator('.rule--value .ant-select'), + 'admin' + ); + await page.getByRole('button', { name: 'Add New Rule' }).click(); + + await expect(page.locator('.group--conjunctions')).toBeVisible(); + + const ruleLocator2 = page.locator('.rule').nth(1); + await selectOption( + page, + ruleLocator2.locator('.rule--field .ant-select'), + DATA_CONTRACT_SEMANTICS1.rules[1].field + ); + await selectOption( + page, + ruleLocator2.locator('.rule--operator .ant-select'), + DATA_CONTRACT_SEMANTICS1.rules[1].operator + ); + await page.getByTestId('save-semantic-button').click(); + + await expect( + page + .getByTestId('contract-semantics-card-0') + .locator('.semantic-form-item-title') + ).toContainText(DATA_CONTRACT_SEMANTICS1.name); + await expect( + page + .getByTestId('contract-semantics-card-0') + .locator('.semantic-form-item-description') + ).toContainText(DATA_CONTRACT_SEMANTICS1.description); + + await page.locator('.expand-collapse-icon').click(); + + await expect( + page.locator('.semantic-rule-editor-view-only') + ).toBeVisible(); + }); + + await test.step('Add second semantic and delete it', async () => { + await page.getByTestId('add-semantic-button').click(); + await page.fill('#semantics_1_name', DATA_CONTRACT_SEMANTICS2.name); + await page.fill( + '#semantics_1_description', + DATA_CONTRACT_SEMANTICS2.description + ); + const ruleLocator3 = page.locator('.group').nth(2); + await selectOption( + page, + ruleLocator3.locator('.group--field .ant-select'), + DATA_CONTRACT_SEMANTICS2.rules[0].field + ); + await selectOption( + page, + ruleLocator3.locator('.rule--operator .ant-select'), + DATA_CONTRACT_SEMANTICS2.rules[0].operator + ); + await page.getByTestId('save-semantic-button').click(); + + await expect( + page + .getByTestId('contract-semantics-card-1') + .locator('.semantic-form-item-title') + ).toContainText(DATA_CONTRACT_SEMANTICS2.name); + await expect( + page + .getByTestId('contract-semantics-card-1') + .locator('.semantic-form-item-description') + ).toContainText(DATA_CONTRACT_SEMANTICS2.description); + + await page.getByTestId('delete-semantic-1').click(); + + await expect( + page.getByTestId('contract-semantics-card-1') + ).not.toBeVisible(); + }); + + await test.step('Fill Contract Quality form', async () => { + await page.getByRole('button', { name: 'Quality' }).click(); + + // Fill Contract Quality form + + await page.getByTestId('add-test-button').click(); + + await expect(page.getByRole('dialog')).toBeVisible(); + + await page.fill( + '[data-testid="test-case-name"]', + NEW_TABLE_TEST_CASE.name + ); + + await page.click('#testCaseFormV1_testTypeId'); + await page.waitForSelector(`text=${NEW_TABLE_TEST_CASE.label}`); + await page.click(`text=${NEW_TABLE_TEST_CASE.label}`); + await page.fill( + '#testCaseFormV1_params_columnName', + NEW_TABLE_TEST_CASE.field + ); + + await page.click('[data-testid="tags-selector"] input'); + await page.fill('[data-testid="tags-selector"] input', testTag.data.name); + await page + .getByTestId(`tag-${testTag.responseData.fullyQualifiedName}`) + .click(); + + await clickOutside(page); + + await page.click('[data-testid="glossary-terms-selector"] input'); + await page.fill( + '[data-testid="glossary-terms-selector"] input', + testGlossaryTerm.data.name + ); + + await page + .getByTestId(`tag-${testGlossaryTerm.responseData.fullyQualifiedName}`) + .click(); + + await clickOutside(page); + + const testCaseResponse = page.waitForResponse( + '/api/v1/dataQuality/testCases' + ); + await page.click('[data-testid="create-btn"]'); + await testCaseResponse; + + await page.waitForTimeout(100); + + await expect( + page + .locator('.ant-table-cell') + .filter({ hasText: NEW_TABLE_TEST_CASE.name }) + ).toBeVisible(); + }); + + await test.step('Save contract and validate', async () => { + const saveContractResponse = page.waitForResponse( + '/api/v1/dataContracts' + ); + await page.getByTestId('save-contract-btn').click(); + await saveContractResponse; + + await toastNotification(page, 'Data contract saved successfully'); + + await expect( + page + .getByTestId('contract-card-title-container') + .filter({ hasText: 'Contract Status' }) + ).not.toBeVisible(); + + const runNowResponse = page.waitForResponse( + '/api/v1/dataContracts/*/validate' + ); + await page.getByTestId('contract-run-now-button').click(); + await runNowResponse; + + await toastNotification( + page, + 'Contract validation trigger successfully.' + ); + + await page.reload(); + + await expect( + page.getByTestId('contract-card-title-container').filter({ + hasText: 'Contract Status', + }) + ).toBeVisible(); + await expect( + page.getByTestId('contract-status-card-item-Semantics-status') + ).toContainText('Failed'); + await expect( + page.getByTestId('data-contract-latest-result-btn') + ).toContainText('Contract Failed'); + + await addOwner({ + page, + owner: 'admin', + type: 'Users', + endpoint: EntityTypeEndpoint.Table, + dataTestId: 'data-assets-header', + }); + + await page.getByTestId('contract-run-now-button').click(); + await runNowResponse; + + await toastNotification( + page, + 'Contract validation trigger successfully.' + ); + + await page.reload(); + + await expect( + page.getByTestId('contract-status-card-item-Semantics-status') + ).toContainText('Passed'); + }); + + await test.step('Edit contract and validate', async () => { + await page.getByTestId('contract-edit-button').click(); + + await page.getByRole('tab', { name: 'Quality' }).click(); + + await page + .locator('input[type="checkbox"][aria-label="Select all"]') + .check(); + + await expect( + page.getByRole('checkbox', { name: 'Select all' }) + ).toBeChecked(); + + await page.getByTestId('save-contract-btn').click(); + + await toastNotification(page, 'Data contract saved successfully'); + + const runNowResponse = page.waitForResponse( + '/api/v1/dataContracts/*/validate' + ); + await page.getByTestId('contract-run-now-button').click(); + await runNowResponse; + + await toastNotification( + page, + 'Contract validation trigger successfully.' + ); + + await page.reload(); + }); + + await test.step('Verify YAML view', async () => { + await page.getByTestId('contract-view-switch-tab-yaml').click(); + + await expect(page.getByTestId('code-mirror-container')).toBeVisible(); + await expect( + page + .getByTestId('code-mirror-container') + .getByTestId('query-copy-button') + ).toBeVisible(); + }); + + await test.step('Export YAML', async () => { + const downloadPromise = page.waitForEvent('download'); + + await page.getByTestId('export-contract-button').click(); + const download = await downloadPromise; + // Wait for the download process to complete and save the downloaded file somewhere. + await download.saveAs('downloads/' + download.suggestedFilename()); + }); + + await test.step('Delete contract', async () => { + const deleteContractResponse = page.waitForResponse( + 'api/v1/dataContracts/*?hardDelete=true' + ); + + await page.getByTestId('delete-contract-button').click(); + await deleteContractResponse; + + await toastNotification(page, '"Contract" deleted successfully!'); + + await expect(page.getByTestId('no-data-placeholder')).toBeVisible(); + await expect(page.getByTestId('add-contract-button')).toBeVisible(); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx index 8c00e3e1be8..da707f31c11 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx @@ -432,6 +432,7 @@ export const DataAssetsHeader = ({ `data-contract-latest-result-button ${dataContract?.latestResult?.status}` )} + data-testid="data-contract-latest-result-btn" icon={getDataContractStatusIcon(dataContract?.latestResult?.status)} onClick={() => { navigate( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx index f5774ae0ab6..bbc726303a2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx @@ -224,7 +224,8 @@ const AddDataContract: React.FC<{ {t('label.cancel')}