From f7a752946176f198ed99a3b35e84b1de81f1a53c Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Thu, 8 Aug 2024 10:43:36 +0530 Subject: [PATCH] playwright: migrate custom metric spec (#16690) * playwright: migrate custom metric spec * fixed customMetric failure --- .../cypress/e2e/Features/CustomMetric.spec.ts | 341 ------------------ .../ui/playwright/constant/common.ts | 32 ++ .../e2e/Features/CustomMetric.spec.ts | 97 +++++ .../ui/playwright/utils/customMetric.ts | 171 +++++++++ 4 files changed, 300 insertions(+), 341 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/CustomMetric.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/constant/common.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomMetric.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/utils/customMetric.ts diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/CustomMetric.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/CustomMetric.spec.ts deleted file mode 100644 index 09ece9488e2..00000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/CustomMetric.spec.ts +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright 2023 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - interceptURL, - toastNotification, - verifyResponseStatusCode, -} from '../../common/common'; -import { createEntityTable, hardDeleteService } from '../../common/EntityUtils'; -import { visitEntityDetailsPage } from '../../common/Utils/Entity'; -import { getToken } from '../../common/Utils/LocalStorage'; -import { - INVALID_NAMES, - NAME_MIN_MAX_LENGTH_VALIDATION_ERROR_1_128, - NAME_VALIDATION_ERROR, - uuid, -} from '../../constants/constants'; -import { EntityType } from '../../constants/Entity.interface'; -import { DATABASE_SERVICE } from '../../constants/EntityConstant'; -import { SERVICE_CATEGORIES } from '../../constants/service.constants'; - -const TABLE_CUSTOM_METRIC = { - name: `tableCustomMetric-${uuid()}`, - expression: `SELECT * FROM ${DATABASE_SERVICE.entity.name}`, -}; -const COLUMN_CUSTOM_METRIC = { - name: `tableCustomMetric-${uuid()}`, - column: DATABASE_SERVICE.entity.columns[0].name, - expression: `SELECT * FROM ${DATABASE_SERVICE.entity.name}`, -}; - -const validateForm = (isColumnMetric = false) => { - // error messages - cy.get('#name_help').scrollIntoView().should('contain', 'Name is required'); - - cy.get('#expression_help') - .scrollIntoView() - .should('contain', 'SQL Query is required'); - if (isColumnMetric) { - cy.get('#columnName_help') - .scrollIntoView() - .should('contain', 'Column is required'); - } - - // max length validation - cy.get('#name').scrollIntoView().type(INVALID_NAMES.MAX_LENGTH); - cy.get('#name_help').should( - 'contain', - NAME_MIN_MAX_LENGTH_VALIDATION_ERROR_1_128 - ); - - // with special char validation - cy.get('#name') - .should('be.visible') - .clear() - .type(INVALID_NAMES.WITH_SPECIAL_CHARS); - cy.get('#name_help').should('contain', NAME_VALIDATION_ERROR); - cy.get('#name').clear(); -}; - -type CustomMetricDetails = { - term: string; - serviceName: string; - entity: EntityType.Table; - isColumnMetric?: boolean; - metric: { - name: string; - column?: string; - expression: string; - }; -}; - -const createCustomMetric = ({ - term, - serviceName, - entity, - isColumnMetric = false, - metric, -}: CustomMetricDetails) => { - interceptURL('PUT', '/api/v1/tables/*/customMetric', 'createCustomMetric'); - interceptURL( - 'GET', - '/api/v1/tables/name/*?fields=customMetrics%2Ccolumns&include=all', - 'getCustomMetric' - ); - visitEntityDetailsPage({ - term, - serviceName, - entity, - }); - // Click on create custom metric button - cy.get('[data-testid="profiler"]').click(); - - verifyResponseStatusCode('@getCustomMetric', 200); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains(isColumnMetric ? 'Column Profile' : 'Table Profile') - .click(); - - cy.get('[data-testid="profiler-add-table-test-btn"]').click(); - cy.get('[data-testid="custom-metric"]').click(); - - // validate redirection and cancel button - cy.get('[data-testid="heading"]').first().should('be.visible'); - cy.get( - `[data-testid=${ - isColumnMetric - ? 'profiler-tab-container' - : 'table-profiler-chart-container' - }]` - ).should('be.visible'); - cy.get('[data-testid="cancel-button"]').click(); - verifyResponseStatusCode('@getCustomMetric', 200); - cy.url().should('include', 'profiler'); - cy.get('[data-testid="heading"]') - .first() - .invoke('text') - .should('equal', isColumnMetric ? 'Column Profile' : 'Table Profile'); - - // Click on create custom metric button - cy.get('[data-testid="profiler-add-table-test-btn"]').click(); - cy.get('[data-testid="custom-metric"]').click(); - cy.get('[data-testid="submit-button"]').click(); - - validateForm(isColumnMetric); - - // fill form and submit - cy.get('#name').type(metric.name); - if (isColumnMetric) { - cy.get('#columnName').click(); - cy.get(`[title="${metric.column}"]`).click(); - } - metric.expression && - cy.get('.CodeMirror-scroll').click().type(metric.expression); - cy.get('[data-testid="submit-button"]').click(); - verifyResponseStatusCode('@createCustomMetric', 200); - toastNotification(`${metric.name} created successfully.`); - verifyResponseStatusCode('@getCustomMetric', 200); - - // verify the created custom metric - cy.url().should('include', 'profiler'); - cy.get('[data-testid="heading"]') - .first() - .invoke('text') - .should('equal', isColumnMetric ? 'Column Profile' : 'Table Profile'); - cy.get(`[data-testid="${metric.name}-custom-metrics"]`) - .scrollIntoView() - .should('be.visible'); -}; - -const editCustomMetric = ({ - term, - serviceName, - entity, - isColumnMetric = false, - metric, -}: CustomMetricDetails) => { - interceptURL( - 'GET', - '/api/v1/tables/name/*?fields=customMetrics%2Ccolumns&include=all', - 'getCustomMetric' - ); - interceptURL('PUT', '/api/v1/tables/*/customMetric', 'editCustomMetric'); - visitEntityDetailsPage({ - term, - serviceName, - entity, - }); - cy.get('[data-testid="profiler"]').click(); - - verifyResponseStatusCode('@getCustomMetric', 200); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains(isColumnMetric ? 'Column Profile' : 'Table Profile') - .click(); - if (isColumnMetric) { - metric.column && - cy.get('[data-row-key="user_id"]').contains(metric.column).click(); - } - cy.get(`[data-testid="${metric.name}-custom-metrics"]`) - .scrollIntoView() - .should('be.visible'); - cy.get(`[data-testid="${metric.name}-custom-metrics-menu"]`).click(); - cy.get(`[data-menu-id*="edit"]`).click(); - - // validate cancel button - cy.get('.ant-modal-content').should('be.visible'); - cy.get('.ant-modal-footer').contains('Cancel').click(); - cy.get('.ant-modal-content').should('not.exist'); - - // edit expression and submit - cy.get(`[data-testid="${metric.name}-custom-metrics-menu"]`).click(); - cy.get(`[data-menu-id*="edit"]`).click(); - cy.get('.CodeMirror-scroll').click().type('updated'); - cy.get('.ant-modal-footer').contains('Save').click(); - cy.wait('@editCustomMetric').then(({ request }) => { - expect(request.body.expression).to.have.string('updated'); - }); - toastNotification(`${metric.name} updated successfully.`); -}; -const deleteCustomMetric = ({ - term, - serviceName, - entity, - metric, - isColumnMetric = false, -}: CustomMetricDetails) => { - interceptURL( - 'GET', - '/api/v1/tables/name/*?fields=customMetrics%2Ccolumns&include=all', - 'getCustomMetric' - ); - interceptURL( - 'DELETE', - isColumnMetric - ? `/api/v1/tables/*/customMetric/${metric.column}/${metric.name}*` - : `/api/v1/tables/*/customMetric/${metric.name}*`, - 'deleteCustomMetric' - ); - visitEntityDetailsPage({ - term, - serviceName, - entity, - }); - cy.get('[data-testid="profiler"]').click(); - - verifyResponseStatusCode('@getCustomMetric', 200); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains(isColumnMetric ? 'Column Profile' : 'Table Profile') - .click(); - if (isColumnMetric) { - metric.column && - cy.get('[data-row-key="user_id"]').contains(metric.column).click(); - } - cy.get(`[data-testid="${metric.name}-custom-metrics"]`) - .scrollIntoView() - .should('be.visible'); - cy.get(`[data-testid="${metric.name}-custom-metrics-menu"]`).click(); - cy.get(`[data-menu-id*="delete"]`).click(); - cy.get('.ant-modal-header').should('contain', metric.name); - cy.get('[data-testid="confirmation-text-input"]').type('DELETE'); - cy.get('[data-testid="confirm-button"]').click(); - verifyResponseStatusCode('@deleteCustomMetric', 200); - toastNotification(`"${metric.name}" deleted successfully!`); -}; - -describe('Custom Metric', { tags: 'Observability' }, () => { - before(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - - createEntityTable({ - token, - ...DATABASE_SERVICE, - tables: [DATABASE_SERVICE.entity], - }); - }); - }); - - 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(); - }); - - it('Create table custom metric', () => { - createCustomMetric({ - term: DATABASE_SERVICE.entity.name, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - metric: TABLE_CUSTOM_METRIC, - }); - }); - - it("Edit table custom metric's expression", () => { - editCustomMetric({ - term: DATABASE_SERVICE.entity.name, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - metric: TABLE_CUSTOM_METRIC, - }); - }); - - it('Delete table custom metric', () => { - deleteCustomMetric({ - term: DATABASE_SERVICE.entity.name, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - metric: TABLE_CUSTOM_METRIC, - }); - }); - - it('Create column custom metric', () => { - createCustomMetric({ - term: DATABASE_SERVICE.entity.name, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - metric: COLUMN_CUSTOM_METRIC, - isColumnMetric: true, - }); - }); - - it("Edit column custom metric's expression", () => { - editCustomMetric({ - term: DATABASE_SERVICE.entity.name, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - metric: COLUMN_CUSTOM_METRIC, - isColumnMetric: true, - }); - }); - - it('Delete column custom metric', () => { - deleteCustomMetric({ - term: DATABASE_SERVICE.entity.name, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - metric: COLUMN_CUSTOM_METRIC, - isColumnMetric: true, - }); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/common.ts new file mode 100644 index 00000000000..2c84694b3e4 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/common.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ +export const TAG_INVALID_NAMES = { + MIN_LENGTH: 'c', + MAX_LENGTH: 'a87439625b1c2d3e4f5061728394a5b6c7d8e90a1b2c3d4e5f67890ab', + WITH_SPECIAL_CHARS: '!@#$%^&*()', +}; + +export const INVALID_NAMES = { + MAX_LENGTH: + 'a87439625b1c2d3e4f5061728394a5b6c7d8e90a1b2c3d4e5f67890aba87439625b1c2d3e4f5061728394a5b6c7d8e90a1b2c3d4e5f67890abName can be a maximum of 128 characters', + WITH_SPECIAL_CHARS: '::normalName::', +}; + +export const NAME_VALIDATION_ERROR = + 'Name must contain only letters, numbers, underscores, hyphens, periods, parenthesis, and ampersands.'; + +export const NAME_MIN_MAX_LENGTH_VALIDATION_ERROR = + 'Name size must be between 2 and 64'; + +export const NAME_MAX_LENGTH_VALIDATION_ERROR = + 'Name size must be between 1 and 128'; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomMetric.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomMetric.spec.ts new file mode 100644 index 00000000000..8b6a54dce3e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomMetric.spec.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import test from '@playwright/test'; +import { TableClass } from '../../support/entity/TableClass'; +import { getApiContext, redirectToHomePage, uuid } from '../../utils/common'; +import { + createCustomMetric, + deleteCustomMetric, +} from '../../utils/customMetric'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +test('Table custom metric', async ({ page }) => { + const table = new TableClass(); + await redirectToHomePage(page); + const { afterAction, apiContext } = await getApiContext(page); + await table.create(apiContext); + + const TABLE_CUSTOM_METRIC = { + name: `tableCustomMetric-${uuid()}`, + expression: `SELECT * FROM ${table.entity.name}`, + }; + + await test.step('Create', async () => { + const profilerResponse = page.waitForResponse( + `/api/v1/tables/${table.entityResponseData?.['fullyQualifiedName']}/tableProfile/latest` + ); + await table.visitEntityPage(page); + await page.click('[data-testid="profiler"]'); + await profilerResponse; + await page.waitForTimeout(1000); + await createCustomMetric({ + page, + metric: TABLE_CUSTOM_METRIC, + }); + }); + + await test.step('Delete', async () => { + await deleteCustomMetric({ + page, + metric: TABLE_CUSTOM_METRIC, + }); + }); + + await table.delete(apiContext); + await afterAction(); +}); + +test('Column custom metric', async ({ page }) => { + const table = new TableClass(); + await redirectToHomePage(page); + const { afterAction, apiContext } = await getApiContext(page); + await table.create(apiContext); + + const COLUMN_CUSTOM_METRIC = { + name: `columnCustomMetric-${uuid()}`, + column: table.entity.columns[0].name, + expression: `SELECT * FROM ${table.entity.name}`, + }; + + await test.step('Create', async () => { + const profilerResponse = page.waitForResponse( + `/api/v1/tables/${table.entityResponseData?.['fullyQualifiedName']}/tableProfile/latest` + ); + await table.visitEntityPage(page); + await page.click('[data-testid="profiler"]'); + await profilerResponse; + await page.waitForTimeout(1000); + await createCustomMetric({ + page, + metric: COLUMN_CUSTOM_METRIC, + isColumnMetric: true, + }); + }); + + await test.step('Delete', async () => { + await deleteCustomMetric({ + page, + metric: COLUMN_CUSTOM_METRIC, + isColumnMetric: true, + }); + }); + + await table.delete(apiContext); + await afterAction(); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customMetric.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customMetric.ts new file mode 100644 index 00000000000..aec19a4749a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customMetric.ts @@ -0,0 +1,171 @@ +/* + * 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 } from '@playwright/test'; +import { + INVALID_NAMES, + NAME_MAX_LENGTH_VALIDATION_ERROR, + NAME_VALIDATION_ERROR, +} from '../constant/common'; + +type CustomMetricDetails = { + page: Page; + isColumnMetric?: boolean; + metric: { + name: string; + column?: string; + expression: string; + }; +}; + +const validateForm = async (page: Page, isColumnMetric = false) => { + // error messages + await expect(page.locator('#name_help')).toHaveText('Name is required'); + + await expect(page.locator('#expression_help')).toHaveText( + 'SQL Query is required.' + ); + + if (isColumnMetric) { + await expect(page.locator('#columnName_help')).toHaveText( + 'Column is required.' + ); + } + + // max length validation + await page.locator('#name').fill(INVALID_NAMES.MAX_LENGTH); + + await expect(page.locator('#name_help')).toHaveText( + NAME_MAX_LENGTH_VALIDATION_ERROR + ); + + // with special char validation + await page.locator('#name').fill(INVALID_NAMES.WITH_SPECIAL_CHARS); + + await expect(page.locator('#name_help')).toHaveText(NAME_VALIDATION_ERROR); + + await page.locator('#name').clear(); +}; + +export const createCustomMetric = async ({ + page, + isColumnMetric = false, + metric, +}: CustomMetricDetails) => { + await page + .getByRole('menuitem', { + name: isColumnMetric ? 'Column Profile' : 'Table Profile', + }) + .click(); + await page.locator('[data-testid="profiler-add-table-test-btn"]').click(); + await page.locator('[data-testid="custom-metric"]').click(); + + const customMetricResponse = page.waitForResponse( + '/api/v1/tables/name/*?fields=customMetrics%2Ccolumns&include=all' + ); + + // validate redirection and cancel button + await expect(page.locator('[data-testid="heading"]')).toBeVisible(); + await expect( + page.locator( + `[data-testid="${ + isColumnMetric + ? 'profiler-tab-container' + : 'table-profiler-chart-container' + }"]` + ) + ).toBeVisible(); + + await page.locator('[data-testid="cancel-button"]').click(); + await customMetricResponse; + + await expect(page).toHaveURL(/profiler/); + await expect( + page.getByRole('heading', { + name: isColumnMetric ? 'Column Profile' : 'Table Profile', + }) + ).toBeVisible(); + + // Click on create custom metric button + await page.click('[data-testid="profiler-add-table-test-btn"]'); + await page.click('[data-testid="custom-metric"]'); + await page.click('[data-testid="submit-button"]'); + + await validateForm(page, isColumnMetric); + + // fill form and submit + await page.fill('#name', metric.name); + if (isColumnMetric) { + await page.click('#columnName'); + await page.click(`[title="${metric.column}"]`); + } + if (metric.expression) { + await page.click('.CodeMirror-scroll'); + await page.keyboard.type(metric.expression); + } + const createMetricResponse = page.waitForResponse( + '/api/v1/tables/*/customMetric' + ); + await page.click('[data-testid="submit-button"]'); + await createMetricResponse; + + await expect(page.locator('.Toastify__toast-body')).toHaveText( + new RegExp(`${metric.name} created successfully.`) + ); + + await page.locator('.Toastify__close-button').click(); + + // verify the created custom metric + await expect(page).toHaveURL(/profiler/); + await expect( + page.getByRole('heading', { + name: isColumnMetric ? 'Column Profile' : 'Table Profile', + }) + ).toBeVisible(); + + await expect( + page.locator(`[data-testid="${metric.name}-custom-metrics"]`) + ).toBeVisible(); +}; + +export const deleteCustomMetric = async ({ + page, + metric, + isColumnMetric = false, +}) => { + await page + .locator(`[data-testid="${metric.name}-custom-metrics"]`) + .scrollIntoViewIfNeeded(); + + await expect( + page.locator(`[data-testid="${metric.name}-custom-metrics"]`) + ).toBeVisible(); + + await page.click(`[data-testid="${metric.name}-custom-metrics-menu"]`); + await page.click(`[data-menu-id*="delete"]`); + + await expect(page.locator('.ant-modal-header')).toContainText(metric.name); + + await page.fill('[data-testid="confirmation-text-input"]', 'DELETE'); + const deleteMetricResponse = page.waitForResponse( + isColumnMetric + ? `/api/v1/tables/*/customMetric/${metric.column}/${metric.name}*` + : `/api/v1/tables/*/customMetric/${metric.name}*` + ); + await page.click('[data-testid="confirm-button"]'); + await deleteMetricResponse; + + // Verifying the deletion + await expect(page.getByRole('alert').first()).toHaveText( + `"${metric.name}" deleted successfully!` + ); +};