From 3a5539eb1c37f6580a6c99e1591cb5668fad47ed Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Sat, 31 Aug 2024 13:18:25 +0530 Subject: [PATCH] PW: migrate query entity spec to playwright (#17619) * PW: migrate query entity spec to playwright * migrated to playwright * minor test fix * fixed api waiting issue * fixed the api issue * fixed API response await * fixed await issue --- .../cypress/e2e/Features/QueryEntity.spec.ts | 354 ------------------ .../e2e/Features/QueryEntity.spec.ts | 267 +++++++++++++ .../resources/ui/playwright/utils/query.ts | 63 ++++ 3 files changed, 330 insertions(+), 354 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/QueryEntity.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/QueryEntity.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/utils/query.ts diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/QueryEntity.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/QueryEntity.spec.ts deleted file mode 100644 index 5e595961136..00000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/QueryEntity.spec.ts +++ /dev/null @@ -1,354 +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 { - descriptionBox, - interceptURL, - verifyResponseStatusCode, -} from '../../common/common'; -import { - createEntityTable, - createQueryByTableName, - generateRandomTable, - hardDeleteService, -} from '../../common/EntityUtils'; -import { visitEntityDetailsPage } from '../../common/Utils/Entity'; -import { getToken } from '../../common/Utils/LocalStorage'; -import { generateRandomUser } from '../../common/Utils/Owner'; -import { EntityType } from '../../constants/Entity.interface'; -import { - DATABASE_SERVICE, - DATABASE_SERVICE_DETAILS, -} from '../../constants/EntityConstant'; -import { SERVICE_CATEGORIES } from '../../constants/service.constants'; - -const queryTable = { - term: DATABASE_SERVICE.entity.name, - displayName: DATABASE_SERVICE.entity.name, - entity: EntityType.Table, - serviceName: DATABASE_SERVICE.service.name, - entityType: 'Table', -}; -const table1 = generateRandomTable(); -const table2 = generateRandomTable(); -const user1 = generateRandomUser(); -const user2 = generateRandomUser(); -const owner = `${user2.firstName}${user2.lastName}`; -const userIds: string[] = []; - -const DATA = { - ...queryTable, - query: `select * from table ${queryTable.term}`, - description: 'select all the field from table', - owner: 'Aaron Johnson', - tag: 'Personal', - queryUsedIn: { - table1: table1.name, - table2: table2.name, - }, -}; - -const queryFilters = ({ - key, - filter, - apiKey, -}: { - key: string; - filter: string; - apiKey: string; -}) => { - cy.get(`[data-testid="search-dropdown-${key}"]`).click(); - cy.get('[data-testid="search-input"]').type(filter); - verifyResponseStatusCode(apiKey, 200); - cy.get(`[data-testid="search-dropdown-${key}"]`).trigger('mouseout'); - cy.get(`[data-testid="drop-down-menu"] [title="${filter}"]`).click(); - cy.get('[data-testid="update-btn"]').click(); - verifyResponseStatusCode('@fetchQuery', 200); -}; - -describe('Query Entity', { tags: 'DataAssets' }, () => { - before(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - - createEntityTable({ - token, - ...DATABASE_SERVICE, - tables: [DATABASE_SERVICE.entity, table1, table2], - }); - // get Table by name and create query in the table - createQueryByTableName(token, table1); - - // Create a new user - cy.request({ - method: 'POST', - url: `/api/v1/users/signup`, - headers: { Authorization: `Bearer ${token}` }, - body: user1, - }).then((response) => { - userIds.push(response.body.id); - }); - cy.request({ - method: 'POST', - url: `/api/v1/users/signup`, - headers: { Authorization: `Bearer ${token}` }, - body: user2, - }).then((response) => { - userIds.push(response.body.id); - }); - }); - }); - - after(() => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - - hardDeleteService({ - token, - serviceFqn: DATABASE_SERVICE.service.name, - serviceType: SERVICE_CATEGORIES.DATABASE_SERVICES, - }); - - // Delete created user - userIds.forEach((userId) => { - cy.request({ - method: 'DELETE', - url: `/api/v1/users/${userId}?hardDelete=true&recursive=false`, - headers: { Authorization: `Bearer ${token}` }, - }); - }); - }); - }); - - beforeEach(() => { - cy.login(); - interceptURL( - 'GET', - '/api/v1/search/query?q=*&index=query_search_index*', - 'fetchQuery' - ); - }); - - it('Create query', () => { - interceptURL( - 'GET', - '/api/v1/search/query?q=*&from=0&size=15&index=table_search_index', - 'explorePageSearch' - ); - interceptURL('POST', '/api/v1/queries', 'createQuery'); - visitEntityDetailsPage({ - term: DATA.term, - serviceName: DATA.serviceName, - entity: DATA.entity, - }); - cy.get('[data-testid="table_queries"]').click(); - verifyResponseStatusCode('@fetchQuery', 200); - - cy.get('[data-testid="add-query-btn"]').click(); - - cy.get('[data-testid="code-mirror-container"]').type(DATA.query); - cy.get(descriptionBox).scrollIntoView().type(DATA.description); - cy.get('[data-testid="query-used-in"]').type(DATA.queryUsedIn.table1); - verifyResponseStatusCode('@explorePageSearch', 200); - cy.get(`[title="${DATA.queryUsedIn.table1}"]`).click(); - cy.clickOutside(); - - cy.get('[data-testid="save-btn"]').click(); - verifyResponseStatusCode('@createQuery', 201); - - cy.get('[data-testid="query-card"]').should('have.length.above', 0); - cy.get('[data-testid="query-card"]') - .contains(DATA.query) - .scrollIntoView() - .should('be.visible'); - }); - - it('Update owner, description and tag', () => { - interceptURL('GET', '/api/v1/users?*', 'getUsers'); - interceptURL('PATCH', '/api/v1/queries/*', 'patchQuery'); - interceptURL( - 'GET', - '/api/v1/search/query?q=*&from=0&size=15&index=table_search_index', - 'explorePageSearch' - ); - visitEntityDetailsPage({ - term: DATA.term, - serviceName: DATA.serviceName, - entity: DATA.entity, - }); - cy.get('[data-testid="table_queries"]').click(); - verifyResponseStatusCode('@fetchQuery', 200); - - cy.get('[data-testid="query-card"]').should('have.length.above', 0); - - // Update owner - cy.get(':nth-child(2) > [data-testid="edit-owner"]').click(); - verifyResponseStatusCode('@getUsers', 200); - cy.get('[data-testid="loader"]').should('not.exist'); - interceptURL('GET', `api/v1/search/query?q=*`, 'searchOwner'); - cy.get('[data-testid="owner-select-users-search-bar"]').type(owner); - verifyResponseStatusCode('@searchOwner', 200); - cy.get(`.ant-popover [title="${owner}"]`).click(); - cy.get('[data-testid="selectable-list-update-btn"]').click(); - verifyResponseStatusCode('@patchQuery', 200); - cy.get('[data-testid="owner-link"]').should('contain', owner); - - // Update Description - cy.get('[data-testid="edit-description"]').filter(':visible').click(); - cy.get(descriptionBox).clear().type('updated description'); - cy.get('[data-testid="save"]').click(); - verifyResponseStatusCode('@patchQuery', 200); - - // Update Tags - cy.get('[data-testid="entity-tags"] .ant-tag').filter(':visible').click(); - cy.get('[data-testid="tag-selector"]').type(DATA.tag); - cy.get('[data-testid="tag-PersonalData.Personal"]').click(); - cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click(); - verifyResponseStatusCode('@patchQuery', 200); - }); - - it('Verify query filter', () => { - visitEntityDetailsPage({ - term: DATA.term, - serviceName: DATA.serviceName, - entity: DATA.entity, - }); - cy.get('[data-testid="table_queries"]').click(); - verifyResponseStatusCode('@fetchQuery', 200); - const userName = `${user1.firstName}${user1.lastName}`; - interceptURL( - 'GET', - `/api/v1/search/query?*${encodeURI( - userName - )}*index=user_search_index,team_search_index*`, - 'searchUserName' - ); - queryFilters({ - filter: `${user1.firstName}${user1.lastName}`, - apiKey: '@searchUserName', - key: 'Owner', - }); - interceptURL( - 'GET', - `/api/v1/search/query?*${encodeURI( - owner - )}*index=user_search_index,team_search_index*`, - 'searchOwner' - ); - cy.get('[data-testid="no-data-placeholder"]').should('be.visible'); - queryFilters({ - filter: owner, - apiKey: '@searchOwner', - key: 'Owner', - }); - interceptURL( - 'GET', - '/api/v1/search/query?*None*index=tag_search_index*', - 'noneTagSearch' - ); - cy.get('[data-testid="query-card"]').should('have.length.above', 0); - queryFilters({ - filter: 'None', - apiKey: '@noneTagSearch', - key: 'Tag', - }); - interceptURL( - 'GET', - `/api/v1/search/query?*${DATA.tag}*index=tag_search_index*`, - 'personalTagSearch' - ); - cy.get('[data-testid="no-data-placeholder"]').should('be.visible'); - queryFilters({ - filter: DATA.tag, - apiKey: '@personalTagSearch', - key: 'Tag', - }); - cy.get('[data-testid="query-card"]').should('have.length.above', 0); - }); - - it('Update query and QueryUsedIn', () => { - interceptURL('GET', '/api/v1/users?&isBot=false&limit=15', 'getUsers'); - interceptURL('PATCH', '/api/v1/queries/*', 'patchQuery'); - interceptURL( - 'GET', - '/api/v1/search/query?q=*&from=0&size=15&index=table_search_index', - 'explorePageSearch' - ); - - visitEntityDetailsPage({ - term: DATA.term, - serviceName: DATA.serviceName, - entity: DATA.entity, - }); - cy.get('[data-testid="table_queries"]').click(); - verifyResponseStatusCode('@fetchQuery', 200); - - cy.get('[data-testid="query-btn"]').click(); - cy.get('[data-menu-id*="edit-query"]').click(); - cy.get('.CodeMirror-line') - .click() - .type(`{selectAll}{selectAll}${DATA.queryUsedIn.table1}`); - cy.get('[data-testid="edit-query-used-in"]').click(); - cy.wait('@explorePageSearch'); - cy.get('[data-testid="edit-query-used-in"]').type(DATA.queryUsedIn.table2); - verifyResponseStatusCode('@explorePageSearch', 200); - cy.get(`[title="${DATA.queryUsedIn.table2}"]`).click(); - cy.clickOutside(); - - cy.get('[data-testid="save-query-btn"]').click(); - verifyResponseStatusCode('@patchQuery', 200); - }); - - it('Visit full screen view of query', () => { - interceptURL('GET', '/api/v1/queries?*', 'fetchQuery'); - interceptURL('GET', '/api/v1/users?&isBot=false&limit=15', 'getUsers'); - interceptURL('GET', '/api/v1/queries/*', 'getQueryById'); - interceptURL( - 'GET', - '/api/v1/search/query?q=*&from=0&size=15&index=table_search_index', - 'explorePageSearch' - ); - - visitEntityDetailsPage({ - term: DATA.term, - serviceName: DATA.serviceName, - entity: DATA.entity, - }); - cy.get('[data-testid="table_queries"]').click(); - verifyResponseStatusCode('@fetchQuery', 200); - cy.get('[data-testid="query-entity-expand-button"]').click(); - verifyResponseStatusCode('@getQueryById', 200); - - cy.get('[data-testid="query-btn"]').click(); - cy.get('.ant-dropdown').should('be.visible'); - cy.get('[data-menu-id*="delete-query"]').click(); - cy.get('[data-testid="save-button"]').click(); - }); - - it('Verify query duration', () => { - visitEntityDetailsPage({ - term: table1.name, - serviceName: DATABASE_SERVICE_DETAILS.name, - entity: DATA.entity, - }); - - cy.get('[data-testid="table_queries"]').click(); - verifyResponseStatusCode('@fetchQuery', 200); - - // Validate that the duration is in sec or not - cy.get('[data-testid="query-run-duration"]') - .should('be.visible') - .should('contain', '6.199 sec'); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/QueryEntity.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/QueryEntity.spec.ts new file mode 100644 index 00000000000..a42157feba5 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/QueryEntity.spec.ts @@ -0,0 +1,267 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import test, { expect } from '@playwright/test'; +import { TableClass } from '../../support/entity/TableClass'; +import { UserClass } from '../../support/user/UserClass'; +import { + clickOutside, + createNewPage, + descriptionBox, + redirectToHomePage, +} from '../../utils/common'; +import { createQueryByTableName, queryFilters } from '../../utils/query'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +const table1 = new TableClass(); +const table2 = new TableClass(); +const table3 = new TableClass(); +const user1 = new UserClass(); +const user2 = new UserClass(); +const entityData = [table1, table2, table3, user1, user2]; +const queryData = { + query: `select * from table ${table1.entity.name}`, + description: 'select all the field from table', + owner: user1.getUserName(), + tagFqn: 'PersonalData.Personal', + tagName: 'Personal', + queryUsedIn: { + table1: table2.entity.name, + table2: table3.entity.name, + }, +}; + +test.beforeAll(async ({ browser }) => { + const { afterAction, apiContext } = await createNewPage(browser); + for (const entity of entityData) { + await entity.create(apiContext); + } + await createQueryByTableName({ + apiContext, + tableResponseData: table2.entityResponseData, + }); + await afterAction(); +}); + +test('Query Entity', async ({ page }) => { + test.slow(true); + + await redirectToHomePage(page); + await table1.visitEntityPage(page); + + await test.step('Create a new query entity', async () => { + const queryResponse = page.waitForResponse( + '/api/v1/search/query?q=*&index=query_search_index*' + ); + await page.click(`[data-testid="table_queries"]`); + await queryResponse; + await page.click(`[data-testid="add-query-btn"]`); + await page + .getByTestId('code-mirror-container') + .getByRole('textbox') + .fill(queryData.query); + await page.click(descriptionBox); + await page.keyboard.type(queryData.description); + + await page + .getByTestId('query-used-in') + .locator('div') + .filter({ hasText: 'Please Select a Query Used In' }) + .click(); + await page.keyboard.type(queryData.queryUsedIn.table1); + + await page.click(`[title="${queryData.queryUsedIn.table1}"]`); + await clickOutside(page); + + const createQueryResponse = page.waitForResponse('/api/v1/queries'); + await page.click('[data-testid="save-btn"]'); + await createQueryResponse; + await page.waitForURL('**/table_queries**'); + + await expect(page.locator(`text=${queryData.query}`)).toBeVisible(); + }); + + await test.step('Update owner, description and tag', async () => { + const ownerListResponse = page.waitForResponse('/api/v1/users?*'); + await page + .getByTestId( + 'entity-summary-resizable-right-panel-container entity-resizable-panel-container' + ) + .getByTestId('edit-owner') + .click(); + await ownerListResponse; + + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + const searchOwnerResponse = page.waitForResponse('api/v1/search/query?q=*'); + await page.fill( + '[data-testid="owner-select-users-search-bar"]', + queryData.owner + ); + await searchOwnerResponse; + await page.click(`.ant-popover [title="${queryData.owner}"]`); + const updateOwnerResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/queries/') && + response.request().method() === 'PATCH' + ); + await page.click('[data-testid="selectable-list-update-btn"]'); + await updateOwnerResponse; + + await expect(page.getByRole('link', { name: 'admin' })).toBeVisible(); + await expect( + page.getByRole('link', { name: queryData.owner }) + ).toBeVisible(); + + // Update Description + await page.click(`[data-testid="edit-description"]`); + await page.fill(descriptionBox, 'updated description'); + const updateDescriptionResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/queries/') && + response.request().method() === 'PATCH' + ); + await page.click(`[data-testid="save"]`); + await updateDescriptionResponse; + await page.waitForSelector('.ant-modal-body', { + state: 'detached', + }); + + // Update Tags + await page.getByTestId('add-tag').click(); + await page.locator('#tagsForm_tags').click(); + await page.locator('#tagsForm_tags').fill(queryData.tagFqn); + await page.getByTestId(`tag-${queryData.tagFqn}`).click(); + const updateTagResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/queries/') && + response.request().method() === 'PATCH' + ); + await page.getByTestId('saveAssociatedTag').click(); + await updateTagResponse; + }); + + await test.step('Update query and QueryUsedIn', async () => { + await page.click('[data-testid="query-btn"]'); + await page.click(`[data-menu-id*="edit-query"]`); + await page.click('.CodeMirror-line', { clickCount: 3 }); + await page.keyboard.press('Backspace'); + await page.keyboard.type(`${queryData.queryUsedIn.table1}`); + await page.click('[data-testid="edit-query-used-in"]'); + const tableSearchResponse = page.waitForResponse( + '/api/v1/search/query?q=*&index=table_search_index' + ); + await page.keyboard.type(queryData.queryUsedIn.table2); + await tableSearchResponse; + await page.click(`[title="${queryData.queryUsedIn.table2}"]`); + await clickOutside(page); + const updateQueryResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/queries/') && + response.request().method() === 'PATCH' + ); + await page.click('[data-testid="save-query-btn"]'); + await updateQueryResponse; + }); + + await test.step('Verify query filter', async () => { + const userName = user2.getUserName(); + await queryFilters({ + filter: userName, + apiKey: `/api/v1/search/query?*${encodeURI( + userName + )}*index=user_search_index,team_search_index*`, + key: 'Owner', + page, + }); + + await expect( + page.locator('[data-testid="no-data-placeholder"]') + ).toBeVisible(); + + await queryFilters({ + filter: queryData.owner, + apiKey: `/api/v1/search/query?*${encodeURI( + queryData.owner + )}*index=user_search_index,team_search_index*`, + key: 'Owner', + page, + }); + const queryCards = await page.$$('[data-testid="query-card"]'); + + expect(queryCards.length).toBeGreaterThan(0); + + await queryFilters({ + filter: 'None', + apiKey: '/api/v1/search/query?*None*index=tag_search_index*', + key: 'Tag', + page, + }); + + await expect( + page.locator('[data-testid="no-data-placeholder"]') + ).toBeVisible(); + + await queryFilters({ + filter: queryData.tagName, + apiKey: `/api/v1/search/query?*${queryData.tagName}*index=tag_search_index*`, + key: 'Tag', + page, + }); + + const updatedQueryCards = await page.$$('[data-testid="query-card"]'); + + expect(updatedQueryCards.length).toBeGreaterThan(0); + }); + + await test.step('Visit full screen view of query and Delete', async () => { + const queryResponse = page.waitForResponse('/api/v1/queries/*'); + await page.click(`[data-testid="query-entity-expand-button"]`); + await queryResponse; + + await page.click(`[data-testid="query-btn"]`); + await page.waitForSelector('.ant-dropdown', { state: 'visible' }); + await page.click(`[data-menu-id*="delete-query"]`); + await page.click(`[data-testid="save-button"]`); + await page.waitForResponse('/api/v1/queries/*'); + }); +}); + +test('Verify query duration', async ({ page }) => { + await redirectToHomePage(page); + await table2.visitEntityPage(page); + const queryResponse = page.waitForResponse( + '/api/v1/search/query?q=*&index=query_search_index*' + ); + await page.click(`[data-testid="table_queries"]`); + await queryResponse; + await page.waitForSelector('[data-testid="query-run-duration"]', { + state: 'visible', + }); + const durationText = await page.textContent( + '[data-testid="query-run-duration"]' + ); + + expect(durationText).toContain('6.199 sec'); +}); + +test.afterAll(async ({ browser }) => { + const { afterAction, apiContext } = await createNewPage(browser); + for (const entity of entityData) { + await entity.delete(apiContext); + } + await afterAction(); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/query.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/query.ts new file mode 100644 index 00000000000..da0db5c6720 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/query.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { APIRequestContext, Page } from '@playwright/test'; + +export const createQueryByTableName = async (data: { + apiContext: APIRequestContext; + tableResponseData: unknown; +}) => { + const { apiContext, tableResponseData } = data; + const queryResponse = await apiContext + .post('/api/v1/queries', { + data: { + query: `SELECT * FROM SALES-${tableResponseData?.['name']}`, + description: 'this is query description', + queryUsedIn: [ + { + id: tableResponseData?.['id'], + type: 'table', + }, + ], + duration: 6199, + queryDate: 1700225667191, + service: tableResponseData?.['service']?.['name'], + }, + }) + .then((response) => response.json()); + + return await queryResponse; +}; + +export const queryFilters = async ({ + key, + filter, + apiKey, + page, +}: { + key: string; + filter: string; + apiKey: string; + page: Page; +}) => { + await page.click(`[data-testid="search-dropdown-${key}"]`); + const searchInputResponse = page.waitForResponse(apiKey); + await page.fill('[data-testid="search-input"]', filter); + await searchInputResponse; + await page.hover(`[data-testid="search-dropdown-${key}"]`); + await page.click(`[data-testid="drop-down-menu"] [title="${filter}"]`); + const queryResponse = page.waitForResponse( + '/api/v1/search/query?q=*&index=query_search_index*' + ); + await page.click('[data-testid="update-btn"]'); + await queryResponse; +};