diff --git a/openmetadata-ui/src/main/resources/ui/playwright.config.ts b/openmetadata-ui/src/main/resources/ui/playwright.config.ts index 78205ad14a6d..819c40741060 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright.config.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright.config.ts @@ -30,9 +30,10 @@ export default defineConfig({ /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, + retries: process.env.CI ? 1 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 2 : undefined, + workers: process.env.CI ? 3 : undefined, + maxFailures: 30, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['list'], diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts index 823bf3867fb5..d54c818eb409 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts @@ -11,6 +11,7 @@ * limitations under the License. */ import { test } from '@playwright/test'; +import { isUndefined } from 'lodash'; import { CustomPropertySupportedEntityList } from '../../constant/customProperty'; import { ApiEndpointClass } from '../../support/entity/ApiEndpointClass'; import { ContainerClass } from '../../support/entity/ContainerClass'; @@ -113,6 +114,42 @@ entities.forEach((EntityClass) => { ); }); + // Run only if entity has children + if (!isUndefined(entity.childrenTabId)) { + test('Tag Add, Update and Remove for child entities', async ({ + page, + }) => { + await page.getByTestId(entity.childrenTabId ?? '').click(); + + await entity.tagChildren({ + page: page, + tag1: 'PersonalData.Personal', + tag2: 'PII.None', + rowId: entity.childrenSelectorId ?? '', + rowSelector: + entity.type === 'MlModel' ? 'data-testid' : 'data-row-key', + }); + }); + } + + // Run only if entity has children + if (!isUndefined(entity.childrenTabId)) { + test('Glossary Term Add, Update and Remove for child entities', async ({ + page, + }) => { + await page.getByTestId(entity.childrenTabId ?? '').click(); + + await entity.glossaryTermChildren({ + page: page, + glossaryTerm1: EntityDataClass.glossaryTerm1.responseData, + glossaryTerm2: EntityDataClass.glossaryTerm2.responseData, + rowId: entity.childrenSelectorId ?? '', + rowSelector: + entity.type === 'MlModel' ? 'data-testid' : 'data-row-key', + }); + }); + } + test(`Announcement create & delete`, async ({ page }) => { await entity.announcement( page, @@ -148,7 +185,7 @@ entities.forEach((EntityClass) => { await test.step(`Set ${titleText} Custom Property`, async () => { for (const type of properties) { - await entity.setCustomProperty( + await entity.updateCustomProperty( page, entity.customPropertyValue[type].property, entity.customPropertyValue[type].value diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.ts new file mode 100644 index 000000000000..44df42c91f7f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.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, test as base } from '@playwright/test'; +import { isUndefined } from 'lodash'; +import { ApiEndpointClass } from '../../support/entity/ApiEndpointClass'; +import { ContainerClass } from '../../support/entity/ContainerClass'; +import { DashboardClass } from '../../support/entity/DashboardClass'; +import { DashboardDataModelClass } from '../../support/entity/DashboardDataModelClass'; +import { EntityDataClass } from '../../support/entity/EntityDataClass'; +import { MlModelClass } from '../../support/entity/MlModelClass'; +import { PipelineClass } from '../../support/entity/PipelineClass'; +import { SearchIndexClass } from '../../support/entity/SearchIndexClass'; +import { StoredProcedureClass } from '../../support/entity/StoredProcedureClass'; +import { TableClass } from '../../support/entity/TableClass'; +import { TopicClass } from '../../support/entity/TopicClass'; +import { UserClass } from '../../support/user/UserClass'; +import { performAdminLogin } from '../../utils/admin'; +import { redirectToHomePage } from '../../utils/common'; + +const user = new UserClass(); + +const entities = [ + ApiEndpointClass, + TableClass, + StoredProcedureClass, + DashboardClass, + PipelineClass, + TopicClass, + MlModelClass, + ContainerClass, + SearchIndexClass, + DashboardDataModelClass, +] as const; + +// Create 2 page and authenticate 1 with admin and another with normal user +const test = base.extend<{ + page: Page; +}>({ + page: async ({ browser }, use) => { + const page = await browser.newPage(); + await user.login(page); + await use(page); + await page.close(); + }, +}); + +entities.forEach((EntityClass) => { + const entity = new EntityClass(); + + test.describe(entity.getType(), () => { + test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + + await user.create(apiContext); + await EntityDataClass.preRequisitesForTests(apiContext); + await entity.create(apiContext); + await afterAction(); + }); + + test.beforeEach('Visit entity details page', async ({ page }) => { + await redirectToHomePage(page); + await entity.visitEntityPage(page); + }); + + test('User as Owner Add, Update and Remove', async ({ page }) => { + test.slow(true); + + const OWNER1 = EntityDataClass.user1.getUserName(); + const OWNER2 = EntityDataClass.user2.getUserName(); + const OWNER3 = EntityDataClass.user3.getUserName(); + await entity.owner(page, [OWNER1, OWNER3], [OWNER2], undefined, false); + }); + + test('No edit owner permission', async ({ page }) => { + await page.reload(); + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + await expect(page.getByTestId('edit-owner')).not.toBeAttached(); + }); + + test('Tier Add, Update and Remove', async ({ page }) => { + await entity.tier( + page, + 'Tier1', + EntityDataClass.tierTag1.data.displayName + ); + }); + + test('Update description', async ({ page }) => { + await entity.descriptionUpdate(page); + }); + + test('Tag Add, Update and Remove', async ({ page }) => { + await entity.tag(page, 'PersonalData.Personal', 'PII.None'); + }); + + test('Glossary Term Add, Update and Remove', async ({ page }) => { + await entity.glossaryTerm( + page, + EntityDataClass.glossaryTerm1.responseData, + EntityDataClass.glossaryTerm2.responseData + ); + }); + + // Run only if entity has children + if (!isUndefined(entity.childrenTabId)) { + test('Tag Add, Update and Remove for child entities', async ({ + page, + }) => { + await page.getByTestId(entity.childrenTabId ?? '').click(); + + await entity.tagChildren({ + page, + tag1: 'PersonalData.Personal', + tag2: 'PII.None', + rowId: entity.childrenSelectorId ?? '', + rowSelector: + entity.type === 'MlModel' ? 'data-testid' : 'data-row-key', + }); + }); + } + + // Run only if entity has children + if (!isUndefined(entity.childrenTabId)) { + test('Glossary Term Add, Update and Remove for child entities', async ({ + page, + }) => { + await page.getByTestId(entity.childrenTabId ?? '').click(); + + await entity.glossaryTermChildren({ + page, + glossaryTerm1: EntityDataClass.glossaryTerm1.responseData, + glossaryTerm2: EntityDataClass.glossaryTerm2.responseData, + rowId: entity.childrenSelectorId ?? '', + rowSelector: + entity.type === 'MlModel' ? 'data-testid' : 'data-row-key', + }); + }); + } + + test(`UpVote & DownVote entity`, async ({ page }) => { + await entity.upVote(page); + await entity.downVote(page); + }); + + test(`Follow & Un-follow entity`, async ({ page }) => { + const entityName = entity.entityResponseData?.['displayName']; + await entity.followUnfollowEntity(page, entityName); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await user.delete(apiContext); + await entity.delete(apiContext); + await EntityDataClass.postRequisitesForTests(apiContext); + await afterAction(); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataSteward.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataSteward.spec.ts new file mode 100644 index 000000000000..a99c09e3ad1e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataSteward.spec.ts @@ -0,0 +1,190 @@ +/* + * 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 { Page, test as base } from '@playwright/test'; +import { isUndefined } from 'lodash'; +import { ApiEndpointClass } from '../../support/entity/ApiEndpointClass'; +import { ContainerClass } from '../../support/entity/ContainerClass'; +import { DashboardClass } from '../../support/entity/DashboardClass'; +import { DashboardDataModelClass } from '../../support/entity/DashboardDataModelClass'; +import { EntityDataClass } from '../../support/entity/EntityDataClass'; +import { MlModelClass } from '../../support/entity/MlModelClass'; +import { PipelineClass } from '../../support/entity/PipelineClass'; +import { SearchIndexClass } from '../../support/entity/SearchIndexClass'; +import { StoredProcedureClass } from '../../support/entity/StoredProcedureClass'; +import { TableClass } from '../../support/entity/TableClass'; +import { TopicClass } from '../../support/entity/TopicClass'; +import { UserClass } from '../../support/user/UserClass'; +import { performAdminLogin } from '../../utils/admin'; +import { redirectToHomePage } from '../../utils/common'; + +const user = new UserClass(); + +const entities = [ + ApiEndpointClass, + TableClass, + StoredProcedureClass, + DashboardClass, + PipelineClass, + TopicClass, + MlModelClass, + ContainerClass, + SearchIndexClass, + DashboardDataModelClass, +] as const; + +// Create 2 page and authenticate 1 with admin and another with normal user +const test = base.extend<{ + page: Page; +}>({ + page: async ({ browser }, use) => { + const page = await browser.newPage(); + await user.login(page); + await use(page); + await page.close(); + }, +}); + +entities.forEach((EntityClass) => { + const entity = new EntityClass(); + + test.describe(entity.getType(), () => { + test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + + await user.create(apiContext); + + const dataStewardRoleResponse = await apiContext.get( + '/api/v1/roles/name/DataSteward' + ); + + const dataStewardRole = await dataStewardRoleResponse.json(); + + await user.patch({ + apiContext, + patchData: [ + { + op: 'add', + path: '/roles/0', + value: { + id: dataStewardRole.id, + type: 'role', + name: dataStewardRole.name, + }, + }, + ], + }); + + await EntityDataClass.preRequisitesForTests(apiContext); + await entity.create(apiContext); + await afterAction(); + }); + + test.beforeEach('Visit entity details page', async ({ page }) => { + await redirectToHomePage(page); + await entity.visitEntityPage(page); + }); + + test('User as Owner Add, Update and Remove', async ({ page }) => { + test.slow(true); + + const OWNER1 = EntityDataClass.user1.getUserName(); + const OWNER2 = EntityDataClass.user2.getUserName(); + const OWNER3 = EntityDataClass.user3.getUserName(); + await entity.owner(page, [OWNER1, OWNER3], [OWNER2]); + }); + + test('Team as Owner Add, Update and Remove', async ({ page }) => { + const OWNER1 = EntityDataClass.team1.data.displayName; + const OWNER2 = EntityDataClass.team2.data.displayName; + await entity.owner(page, [OWNER1], [OWNER2], 'Teams'); + }); + + test('Tier Add, Update and Remove', async ({ page }) => { + await entity.tier( + page, + 'Tier1', + EntityDataClass.tierTag1.data.displayName + ); + }); + + test('Update description', async ({ page }) => { + await entity.descriptionUpdate(page); + }); + + test('Tag Add, Update and Remove', async ({ page }) => { + await entity.tag(page, 'PersonalData.Personal', 'PII.None'); + }); + + test('Glossary Term Add, Update and Remove', async ({ page }) => { + await entity.glossaryTerm( + page, + EntityDataClass.glossaryTerm1.responseData, + EntityDataClass.glossaryTerm2.responseData + ); + }); + + // Run only if entity has children + if (!isUndefined(entity.childrenTabId)) { + test('Tag Add, Update and Remove for child entities', async ({ + page, + }) => { + await page.getByTestId(entity.childrenTabId ?? '').click(); + + await entity.tagChildren({ + page, + tag1: 'PersonalData.Personal', + tag2: 'PII.None', + rowId: entity.childrenSelectorId ?? '', + rowSelector: + entity.type === 'MlModel' ? 'data-testid' : 'data-row-key', + }); + }); + } + + // Run only if entity has children + if (!isUndefined(entity.childrenTabId)) { + test('Glossary Term Add, Update and Remove for child entities', async ({ + page, + }) => { + await page.getByTestId(entity.childrenTabId ?? '').click(); + + await entity.glossaryTermChildren({ + page, + glossaryTerm1: EntityDataClass.glossaryTerm1.responseData, + glossaryTerm2: EntityDataClass.glossaryTerm2.responseData, + rowId: entity.childrenSelectorId ?? '', + rowSelector: + entity.type === 'MlModel' ? 'data-testid' : 'data-row-key', + }); + }); + } + + test(`UpVote & DownVote entity`, async ({ page }) => { + await entity.upVote(page); + await entity.downVote(page); + }); + + test(`Follow & Un-follow entity`, async ({ page }) => { + const entityName = entity.entityResponseData?.['displayName']; + await entity.followUnfollowEntity(page, entityName); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await user.delete(apiContext); + await entity.delete(apiContext); + await EntityDataClass.postRequisitesForTests(apiContext); + await afterAction(); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 61acba1ca6b5..9b5bddf8db60 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -360,7 +360,7 @@ test.describe('Glossary tests', () => { await sidebarClick(page, SidebarItem.GLOSSARY); await selectActiveGlossary(page, glossary2.data.displayName); - await goToAssetsTab(page, glossaryTerm3.data.displayName, '2'); + await goToAssetsTab(page, glossaryTerm3.data.displayName, 2); // Check if the selected asset are present const assetContainer = await page.locator( @@ -426,7 +426,11 @@ test.describe('Glossary tests', () => { await redirectToHomePage(page); await sidebarClick(page, SidebarItem.GLOSSARY); await selectActiveGlossary(page, glossary1.data.displayName); - await goToAssetsTab(page, glossaryTerm1.data.displayName); + await goToAssetsTab( + page, + glossaryTerm1.data.displayName, + assets.length + ); await renameGlossaryTerm(page, glossaryTerm1, newName); await verifyGlossaryTermAssets( page, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryVersionPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryVersionPage.spec.ts index 915ea5787da5..025efe5bd44c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryVersionPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryVersionPage.spec.ts @@ -82,6 +82,7 @@ test('Glossary', async ({ page }) => { resultTestId: 'glossary-right-panel-owner-link', endpoint: EntityTypeEndpoint.Glossary, isSelectableInsideForm: true, + type: 'Users', }); await page.reload(); @@ -109,6 +110,7 @@ test('Glossary', async ({ page }) => { activatorBtnDataTestId: 'Add', resultTestId: 'glossary-reviewer-name', endpoint: EntityTypeEndpoint.Glossary, + type: 'Users', }); await page.reload(); @@ -177,6 +179,7 @@ test('GlossaryTerm', async ({ page }) => { resultTestId: 'glossary-right-panel-owner-link', endpoint: EntityTypeEndpoint.Glossary, isSelectableInsideForm: true, + type: 'Users', }); await page.reload(); @@ -204,6 +207,7 @@ test('GlossaryTerm', async ({ page }) => { activatorBtnDataTestId: 'Add', resultTestId: 'glossary-reviewer-name', endpoint: EntityTypeEndpoint.GlossaryTerm, + type: 'Users', }); await page.reload(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts index b272050c2462..00aaade71ce5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts @@ -136,7 +136,7 @@ entities.forEach((EntityClass) => { await test.step(`Set ${titleText} Custom Property`, async () => { for (const type of properties) { - await entity.setCustomProperty( + await entity.updateCustomProperty( page, entity.customPropertyValue[type].property, entity.customPropertyValue[type].value diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/auth.setup.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/auth.setup.ts index 2a93974f6b07..b96c4c4a84ed 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/auth.setup.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/auth.setup.ts @@ -13,7 +13,7 @@ import { test as setup } from '@playwright/test'; import { JWT_EXPIRY_TIME_MAP } from '../constant/login'; import { AdminClass } from '../support/user/AdminClass'; -import { getApiContext } from '../utils/common'; +import { getApiContext, getToken } from '../utils/common'; import { updateJWTTokenExpiryTime } from '../utils/login'; const adminFile = 'playwright/.auth/admin.json'; @@ -26,6 +26,14 @@ setup('authenticate as admin', async ({ page }) => { const { apiContext, afterAction } = await getApiContext(page); await updateJWTTokenExpiryTime(apiContext, JWT_EXPIRY_TIME_MAP['4 hours']); await afterAction(); + await admin.logout(page); + await page.waitForURL('**/signin'); + await admin.login(page); + await page.waitForURL('**/my-data'); + + const token = await getToken(page); + // eslint-disable-next-line no-console + console.log(token); // End of authentication steps. await page.context().storageState({ path: adminFile }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiCollectionClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiCollectionClass.ts index 4924f16e005d..1a568d805d10 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiCollectionClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiCollectionClass.ts @@ -238,29 +238,29 @@ export class ApiCollectionClass extends EntityClass { type: 'Teams' | 'Users' = 'Users' ): Promise { if (type === 'Teams') { - await addOwner( + await addOwner({ page, - owner1[0], + owner: owner1[0], type, - this.endpoint, - 'data-assets-header' - ); - await updateOwner( + endpoint: this.endpoint, + dataTestId: 'data-assets-header', + }); + await updateOwner({ page, - owner2[0], + owner: owner2[0], type, - this.endpoint, - 'data-assets-header' - ); + endpoint: this.endpoint, + dataTestId: 'data-assets-header', + }); await this.verifyOwnerPropagation(page, owner2[0]); - await removeOwner( + await removeOwner({ page, - this.endpoint, - owner2[0], + endpoint: this.endpoint, + ownerName: owner2[0], type, - 'data-assets-header' - ); + dataTestId: 'data-assets-header', + }); } else { await addMultiOwner({ page, @@ -279,13 +279,13 @@ export class ApiCollectionClass extends EntityClass { type, }); await this.verifyOwnerPropagation(page, owner2[0]); - await removeOwner( + await removeOwner({ page, - this.endpoint, - owner2[0], + endpoint: this.endpoint, + ownerName: owner2[0], type, - 'data-assets-header' - ); + dataTestId: 'data-assets-header', + }); } } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiEndpointClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiEndpointClass.ts index 15e4de607042..0c7b6b0c49ce 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiEndpointClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiEndpointClass.ts @@ -38,56 +38,58 @@ export class ApiEndpointClass extends EntityClass { private apiEndpointName = `pw-api-endpoint-${uuid()}`; private fqn = `${this.service.name}.${this.apiCollection.name}.${this.apiEndpointName}`; - entity = { - name: this.apiEndpointName, - apiCollection: `${this.service.name}.${this.apiCollection.name}`, - endpointURL: 'https://sandbox-beta.open-metadata.org/swagger.json', - requestSchema: { - schemaType: 'JSON', - schemaFields: [ + children = [ + { + name: 'default', + dataType: 'RECORD', + fullyQualifiedName: `${this.fqn}.default`, + tags: [], + children: [ { - name: 'default', + name: 'name', dataType: 'RECORD', - fullyQualifiedName: `${this.fqn}.default`, + fullyQualifiedName: `${this.fqn}.default.name`, tags: [], children: [ { - name: 'name', - dataType: 'RECORD', - fullyQualifiedName: `${this.fqn}.default.name`, - tags: [], - children: [ - { - name: 'first_name', - dataType: 'STRING', - description: 'Description for schema field first_name', - fullyQualifiedName: `${this.fqn}.default.name.first_name`, - tags: [], - }, - { - name: 'last_name', - dataType: 'STRING', - fullyQualifiedName: `${this.fqn}.default.name.last_name`, - tags: [], - }, - ], - }, - { - name: 'age', - dataType: 'INT', - fullyQualifiedName: `${this.fqn}.default.age`, + name: 'first_name', + dataType: 'STRING', + description: 'Description for schema field first_name', + fullyQualifiedName: `${this.fqn}.default.name.first_name`, tags: [], }, { - name: 'club_name', + name: 'last_name', dataType: 'STRING', - fullyQualifiedName: `${this.fqn}.default.club_name`, + fullyQualifiedName: `${this.fqn}.default.name.last_name`, tags: [], }, ], }, + { + name: 'age', + dataType: 'INT', + fullyQualifiedName: `${this.fqn}.default.age`, + tags: [], + }, + { + name: 'club_name', + dataType: 'STRING', + fullyQualifiedName: `${this.fqn}.default.club_name`, + tags: [], + }, ], }, + ]; + + entity = { + name: this.apiEndpointName, + apiCollection: `${this.service.name}.${this.apiCollection.name}`, + endpointURL: 'https://sandbox-beta.open-metadata.org/swagger.json', + requestSchema: { + schemaType: 'JSON', + schemaFields: this.children, + }, responseSchema: { schemaType: 'JSON', schemaFields: [ @@ -143,6 +145,8 @@ export class ApiEndpointClass extends EntityClass { super(EntityTypeEndpoint.API_ENDPOINT); this.service.name = name ?? this.service.name; this.type = 'ApiEndpoint'; + this.childrenTabId = 'schema'; + this.childrenSelectorId = this.children[0].name; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts index bb0a4d156b85..0a498ce47a89 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts @@ -33,19 +33,22 @@ export class DashboardDataModelClass extends EntityClass { }, }, }; + + children = [ + { + name: 'country_name', + dataType: 'VARCHAR', + dataLength: 256, + dataTypeDisplay: 'varchar', + description: 'Name of the country.', + }, + ]; + entity = { name: `pw-dashboard-data-model-${uuid()}`, displayName: `pw-dashboard-data-model-${uuid()}`, service: this.service.name, - columns: [ - { - name: 'country_name', - dataType: 'VARCHAR', - dataLength: 256, - dataTypeDisplay: 'varchar', - description: 'Name of the country.', - }, - ], + columns: this.children, dataModelType: 'SupersetDataModel', }; @@ -56,6 +59,8 @@ export class DashboardDataModelClass extends EntityClass { super(EntityTypeEndpoint.DataModel); this.service.name = name ?? this.service.name; this.type = 'Dashboard Data Model'; + this.childrenTabId = 'model'; + this.childrenSelectorId = this.children[0].name; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DatabaseClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DatabaseClass.ts index fd38de72b408..caf7eacd6b4d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DatabaseClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DatabaseClass.ts @@ -214,29 +214,29 @@ export class DatabaseClass extends EntityClass { type: 'Teams' | 'Users' = 'Users' ): Promise { if (type === 'Teams') { - await addOwner( + await addOwner({ page, - owner1[0], + owner: owner1[0], type, - this.endpoint, - 'data-assets-header' - ); - await updateOwner( + endpoint: this.endpoint, + dataTestId: 'data-assets-header', + }); + await updateOwner({ page, - owner2[0], + owner: owner2[0], type, - this.endpoint, - 'data-assets-header' - ); + endpoint: this.endpoint, + dataTestId: 'data-assets-header', + }); await this.verifyOwnerPropagation(page, owner2[0]); - await removeOwner( + await removeOwner({ page, - this.endpoint, - owner2[0], + endpoint: this.endpoint, + ownerName: owner2[0], type, - 'data-assets-header' - ); + dataTestId: 'data-assets-header', + }); } else { await addMultiOwner({ page, @@ -255,13 +255,13 @@ export class DatabaseClass extends EntityClass { type, }); await this.verifyOwnerPropagation(page, owner2[0]); - await removeOwner( + await removeOwner({ page, - this.endpoint, - owner2[0], + endpoint: this.endpoint, + ownerName: owner2[0], type, - 'data-assets-header' - ); + dataTestId: 'data-assets-header', + }); } } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityClass.ts index 190061a33288..6ea8fc8a68dd 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityClass.ts @@ -23,7 +23,9 @@ import { addMultiOwner, addOwner, assignGlossaryTerm, + assignGlossaryTermToChildren, assignTag, + assignTagToChildren, assignTier, createAnnouncement, createInactiveAnnouncement, @@ -32,8 +34,10 @@ import { followEntity, hardDeleteEntity, removeGlossaryTerm, + removeGlossaryTermFromChildren, removeOwner, removeTag, + removeTagsFromChildren, removeTier, replyAnnouncement, softDeleteEntity, @@ -50,6 +54,8 @@ import { EntityTypeEndpoint, ENTITY_PATH } from './Entity.interface'; export class EntityClass { type: string; + childrenTabId?: string; + childrenSelectorId?: string; endpoint: EntityTypeEndpoint; cleanupUser: (apiContext: APIRequestContext) => Promise; @@ -120,54 +126,59 @@ export class EntityClass { page: Page, owner1: string[], owner2: string[], - type: 'Teams' | 'Users' = 'Users' + type: 'Teams' | 'Users' = 'Users', + isEditPermission = true ) { if (type === 'Teams') { - await addOwner( + await addOwner({ page, - owner1[0], + owner: owner1[0], type, - this.endpoint, - 'data-assets-header' - ); - await updateOwner( - page, - owner2[0], - type, - this.endpoint, - 'data-assets-header' - ); - await removeOwner( - page, - this.endpoint, - owner2[0], - type, - 'data-assets-header' - ); - } else { - await addMultiOwner({ - page, - ownerNames: owner1, - activatorBtnDataTestId: 'edit-owner', - resultTestId: 'data-assets-header', endpoint: this.endpoint, - type, + dataTestId: 'data-assets-header', }); + if (isEditPermission) { + await updateOwner({ + page, + owner: owner2[0], + type, + endpoint: this.endpoint, + dataTestId: 'data-assets-header', + }); + await removeOwner({ + page, + endpoint: this.endpoint, + ownerName: owner2[0], + type, + dataTestId: 'data-assets-header', + }); + } + } else { await addMultiOwner({ page, - ownerNames: owner2, + ownerNames: owner1, activatorBtnDataTestId: 'edit-owner', resultTestId: 'data-assets-header', endpoint: this.endpoint, type, }); - await removeOwner( - page, - this.endpoint, - owner2[0], - type, - 'data-assets-header' - ); + if (isEditPermission) { + await addMultiOwner({ + page, + ownerNames: owner2, + activatorBtnDataTestId: 'edit-owner', + resultTestId: 'data-assets-header', + endpoint: this.endpoint, + type, + }); + await removeOwner({ + page, + endpoint: this.endpoint, + ownerName: owner2[0], + type, + dataTestId: 'data-assets-header', + }); + } } } @@ -197,6 +208,41 @@ export class EntityClass { .isVisible(); } + async tagChildren({ + page, + tag1, + tag2, + rowId, + rowSelector = 'data-row-key', + }: { + page: Page; + tag1: string; + tag2: string; + rowId: string; + rowSelector?: string; + }) { + await assignTagToChildren({ page, tag: tag1, rowId, rowSelector }); + await assignTagToChildren({ + page, + tag: tag2, + rowId, + rowSelector, + action: 'Edit', + }); + await removeTagsFromChildren({ + page, + tags: [tag1, tag2], + rowId, + rowSelector, + }); + + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('tags-container') + .getByTestId('Add') + .isVisible(); + } + async glossaryTerm( page: Page, glossaryTerm1: GlossaryTerm['responseData'], @@ -213,6 +259,46 @@ export class EntityClass { .isVisible(); } + async glossaryTermChildren({ + page, + glossaryTerm1, + glossaryTerm2, + rowId, + rowSelector = 'data-row-key', + }: { + page: Page; + glossaryTerm1: GlossaryTerm['responseData']; + glossaryTerm2: GlossaryTerm['responseData']; + rowId: string; + rowSelector?: string; + }) { + await assignGlossaryTermToChildren({ + page, + glossaryTerm: glossaryTerm1, + rowId, + rowSelector, + }); + await assignGlossaryTermToChildren({ + page, + glossaryTerm: glossaryTerm2, + rowId, + rowSelector, + action: 'Edit', + }); + await removeGlossaryTermFromChildren({ + page, + glossaryTerms: [glossaryTerm1, glossaryTerm2], + rowId, + rowSelector, + }); + + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('glossary-container') + .getByTestId('Add') + .isVisible(); + } + async upVote(page: Page) { await upVote(page, this.endpoint); } @@ -267,26 +353,6 @@ export class EntityClass { await hardDeleteEntity(page, displayName ?? entityName, this.endpoint); } - async setCustomProperty( - page: Page, - propertydetails: CustomProperty, - value: string - ) { - await setValueForProperty({ - page, - propertyName: propertydetails.name, - value, - propertyType: propertydetails.propertyType.name, - endpoint: this.endpoint, - }); - await validateValueForProperty({ - page, - propertyName: propertydetails.name, - value, - propertyType: propertydetails.propertyType.name, - }); - } - async updateCustomProperty( page: Page, propertydetails: CustomProperty, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/MlModelClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/MlModelClass.ts index 4ccad160d373..f9f6a6d6693e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/MlModelClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/MlModelClass.ts @@ -38,18 +38,21 @@ export class MlModelClass extends EntityClass { }, }, }; + + children = [ + { + name: 'sales', + dataType: 'numerical', + description: 'Sales amount', + }, + ]; + entity = { name: `pw-mlmodel-${uuid()}`, displayName: `pw-mlmodel-${uuid()}`, service: this.service.name, algorithm: 'Time Series', - mlFeatures: [ - { - name: 'sales', - dataType: 'numerical', - description: 'Sales amount', - }, - ], + mlFeatures: this.children, }; serviceResponseData: ResponseDataType; @@ -59,6 +62,8 @@ export class MlModelClass extends EntityClass { super(EntityTypeEndpoint.MlModel); this.service.name = name ?? this.service.name; this.type = 'MlModel'; + this.childrenTabId = 'features'; + this.childrenSelectorId = `feature-card-${this.children[0].name}`; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/PipelineClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/PipelineClass.ts index 4c830e25b224..e3fe80275e1b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/PipelineClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/PipelineClass.ts @@ -30,11 +30,14 @@ export class PipelineClass extends EntityClass { }, }, }; + + children = [{ name: 'snowflake_task' }]; + entity = { name: `pw-pipeline-${uuid()}`, displayName: `pw-pipeline-${uuid()}`, service: this.service.name, - tasks: [{ name: 'snowflake_task' }], + tasks: this.children, }; serviceResponseData: unknown; @@ -44,6 +47,8 @@ export class PipelineClass extends EntityClass { super(EntityTypeEndpoint.Pipeline); this.service.name = name ?? this.service.name; this.type = 'Pipeline'; + this.childrenTabId = 'tasks'; + this.childrenSelectorId = this.children[0].name; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/SearchIndexClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/SearchIndexClass.ts index 40985e9e9622..7400c73363b2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/SearchIndexClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/SearchIndexClass.ts @@ -33,11 +33,59 @@ export class SearchIndexClass extends EntityClass { }, }, }; + private searchIndexName = `pw-search-index-${uuid()}`; + private fqn = `${this.service.name}.${this.searchIndexName}`; + + children = [ + { + name: 'name', + dataType: 'TEXT', + dataTypeDisplay: 'text', + description: 'Table Entity Name.', + fullyQualifiedName: `${this.fqn}.name`, + tags: [], + }, + { + name: 'description', + dataType: 'TEXT', + dataTypeDisplay: 'text', + description: 'Table Entity Description.', + fullyQualifiedName: `${this.fqn}.description`, + tags: [], + }, + { + name: 'columns', + dataType: 'NESTED', + dataTypeDisplay: 'nested', + description: 'Table Columns.', + fullyQualifiedName: `${this.fqn}.columns`, + tags: [], + children: [ + { + name: 'name', + dataType: 'TEXT', + dataTypeDisplay: 'text', + description: 'Column Name.', + fullyQualifiedName: `${this.fqn}.columns.name`, + tags: [], + }, + { + name: 'description', + dataType: 'TEXT', + dataTypeDisplay: 'text', + description: 'Column Description.', + fullyQualifiedName: `${this.fqn}.columns.description`, + tags: [], + }, + ], + }, + ]; + entity = { - name: `pw-search-index-${uuid()}`, + name: this.searchIndexName, displayName: `pw-search-index-${uuid()}`, service: this.service.name, - fields: [], + fields: this.children, }; serviceResponseData: unknown; @@ -47,6 +95,8 @@ export class SearchIndexClass extends EntityClass { super(EntityTypeEndpoint.SearchIndex); this.service.name = name ?? this.service.name; this.type = 'SearchIndex'; + this.childrenTabId = 'fields'; + this.childrenSelectorId = this.children[0].fullyQualifiedName; } async create(apiContext: APIRequestContext) { 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 281d86aa6296..50d3f83e2887 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 @@ -44,54 +44,56 @@ export class TableClass extends EntityClass { name: `pw-database-schema-${uuid()}`, database: `${this.service.name}.${this.database.name}`, }; + children = [ + { + name: 'user_id', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: + 'Unique identifier for the user of your Shopify POS or your Shopify admin.', + }, + { + name: 'shop_id', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: + 'The ID of the store. This column is a foreign key reference to the shop_id column in the dim.shop table.', + }, + { + name: 'name', + dataType: 'VARCHAR', + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'Name of the staff member.', + children: [ + { + name: 'first_name', + dataType: 'VARCHAR', + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'First name of the staff member.', + }, + { + name: 'last_name', + dataType: 'VARCHAR', + dataLength: 100, + dataTypeDisplay: 'varchar', + }, + ], + }, + { + name: 'email', + dataType: 'VARCHAR', + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'Email address of the staff member.', + }, + ]; + entity = { name: `pw-table-${uuid()}`, description: 'description', - columns: [ - { - name: 'user_id', - dataType: 'NUMERIC', - dataTypeDisplay: 'numeric', - description: - 'Unique identifier for the user of your Shopify POS or your Shopify admin.', - }, - { - name: 'shop_id', - dataType: 'NUMERIC', - dataTypeDisplay: 'numeric', - description: - 'The ID of the store. This column is a foreign key reference to the shop_id column in the dim.shop table.', - }, - { - name: 'name', - dataType: 'VARCHAR', - dataLength: 100, - dataTypeDisplay: 'varchar', - description: 'Name of the staff member.', - children: [ - { - name: 'first_name', - dataType: 'VARCHAR', - dataLength: 100, - dataTypeDisplay: 'varchar', - description: 'First name of the staff member.', - }, - { - name: 'last_name', - dataType: 'VARCHAR', - dataLength: 100, - dataTypeDisplay: 'varchar', - }, - ], - }, - { - name: 'email', - dataType: 'VARCHAR', - dataLength: 100, - dataTypeDisplay: 'varchar', - description: 'Email address of the staff member.', - }, - ], + columns: this.children, databaseSchema: `${this.service.name}.${this.database.name}.${this.schema.name}`, }; @@ -107,6 +109,8 @@ export class TableClass extends EntityClass { super(EntityTypeEndpoint.Table); this.service.name = name ?? this.service.name; this.type = 'Table'; + this.childrenTabId = 'schema'; + this.childrenSelectorId = `${this.entity.databaseSchema}.${this.entity.name}.${this.children[0].name}`; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TopicClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TopicClass.ts index 4157c66e2865..a581a17778ab 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TopicClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TopicClass.ts @@ -33,57 +33,60 @@ export class TopicClass extends EntityClass { }; private topicName = `pw-topic-${uuid()}`; private fqn = `${this.service.name}.${this.topicName}`; - entity = { - name: this.topicName, - service: this.service.name, - messageSchema: { - schemaText: `{"type":"object","required":["name","age","club_name"],"properties":{"name":{"type":"object","required":["first_name","last_name"], - "properties":{"first_name":{"type":"string"},"last_name":{"type":"string"}}},"age":{"type":"integer"},"club_name":{"type":"string"}}}`, - schemaType: 'JSON', - schemaFields: [ + + children = [ + { + name: 'default', + dataType: 'RECORD', + fullyQualifiedName: `${this.fqn}.default`, + tags: [], + children: [ { - name: 'default', + name: 'name', dataType: 'RECORD', - fullyQualifiedName: `${this.fqn}.default`, + fullyQualifiedName: `${this.fqn}.default.name`, tags: [], children: [ { - name: 'name', - dataType: 'RECORD', - fullyQualifiedName: `${this.fqn}.default.name`, - tags: [], - children: [ - { - name: 'first_name', - dataType: 'STRING', - description: 'Description for schema field first_name', - fullyQualifiedName: `${this.fqn}.default.name.first_name`, - tags: [], - }, - { - name: 'last_name', - dataType: 'STRING', - fullyQualifiedName: `${this.fqn}.default.name.last_name`, - tags: [], - }, - ], - }, - { - name: 'age', - dataType: 'INT', - fullyQualifiedName: `${this.fqn}.default.age`, + name: 'first_name', + dataType: 'STRING', + description: 'Description for schema field first_name', + fullyQualifiedName: `${this.fqn}.default.name.first_name`, tags: [], }, { - name: 'club_name', + name: 'last_name', dataType: 'STRING', - fullyQualifiedName: `${this.fqn}.default.club_name`, + fullyQualifiedName: `${this.fqn}.default.name.last_name`, tags: [], }, ], }, + { + name: 'age', + dataType: 'INT', + fullyQualifiedName: `${this.fqn}.default.age`, + tags: [], + }, + { + name: 'club_name', + dataType: 'STRING', + fullyQualifiedName: `${this.fqn}.default.club_name`, + tags: [], + }, ], }, + ]; + + entity = { + name: this.topicName, + service: this.service.name, + messageSchema: { + schemaText: `{"type":"object","required":["name","age","club_name"],"properties":{"name":{"type":"object","required":["first_name","last_name"], + "properties":{"first_name":{"type":"string"},"last_name":{"type":"string"}}},"age":{"type":"integer"},"club_name":{"type":"string"}}}`, + schemaType: 'JSON', + schemaFields: this.children, + }, partitions: 128, }; @@ -94,6 +97,8 @@ export class TopicClass extends EntityClass { super(EntityTypeEndpoint.Topic); this.service.name = name ?? this.service.name; this.type = 'Topic'; + this.childrenTabId = 'schema'; + this.childrenSelectorId = this.children[0].name; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 9384ca57bbb2..6ac04160417c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -141,13 +141,14 @@ export const assignDomain = async ( ) => { await page.getByTestId('add-domain').click(); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); + const searchDomain = page.waitForResponse( + `/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*` + ); await page .getByTestId('selectable-list') .getByTestId('searchbar') .fill(domain.name); - await page.waitForResponse( - `/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*` - ); + await searchDomain; await page.getByRole('listitem', { name: domain.displayName }).click(); await expect(page.getByTestId('domain-link')).toContainText( @@ -162,13 +163,14 @@ export const updateDomain = async ( await page.getByTestId('add-domain').click(); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); await page.getByTestId('selectable-list').getByTestId('searchbar').clear(); + const searchDomain = page.waitForResponse( + `/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*` + ); await page .getByTestId('selectable-list') .getByTestId('searchbar') .fill(domain.name); - await page.waitForResponse( - `/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*` - ); + await searchDomain; await page.getByRole('listitem', { name: domain.displayName }).click(); await expect(page.getByTestId('domain-link')).toContainText( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts index 32e7b5a35b70..c465706c9f10 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts @@ -185,8 +185,9 @@ export const setValueForProperty = async (data: { )}*`; await page.route(searchApi, (route) => route.continue()); await page.locator('#entityReference').clear(); + const searchEntity = page.waitForResponse(searchApi); await page.locator('#entityReference').fill(val); - await page.waitForResponse(searchApi); + await searchEntity; await page.locator(`[data-testid="${val}"]`).click(); } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts index 207ff9308570..c0fdc7bb2356 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts @@ -32,13 +32,14 @@ import { addOwner } from './entity'; export const assignDomain = async (page: Page, domain: Domain['data']) => { await page.getByTestId('add-domain').click(); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); + const searchDomain = page.waitForResponse( + `/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*` + ); await page .getByTestId('selectable-list') .getByTestId('searchbar') .fill(domain.name); - await page.waitForResponse( - `/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*` - ); + await searchDomain; await page.getByRole('listitem', { name: domain.displayName }).click(); await expect(page.getByTestId('domain-link')).toContainText( @@ -50,13 +51,14 @@ export const updateDomain = async (page: Page, domain: Domain['data']) => { await page.getByTestId('add-domain').click(); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); await page.getByTestId('selectable-list').getByTestId('searchbar').clear(); + const searchDomain = page.waitForResponse( + `/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*` + ); await page .getByTestId('selectable-list') .getByTestId('searchbar') .fill(domain.name); - await page.waitForResponse( - `/api/v1/search/query?q=*${encodeURIComponent(domain.name)}*` - ); + await searchDomain; await page.getByRole('listitem', { name: domain.displayName }).click(); await expect(page.getByTestId('domain-link')).toContainText( @@ -150,14 +152,14 @@ const fillCommonFormItems = async ( await page.locator('[data-testid="display-name"]').fill(entity.displayName); await page.fill(descriptionBox, entity.description); if (!isEmpty(entity.owners) && !isUndefined(entity.owners)) { - await addOwner( + await addOwner({ page, - entity.owners[0].name, - entity.owners[0].type as 'Users' | 'Teams', - EntityTypeEndpoint.Domain, - 'owner-container', - 'add-owner' - ); + owner: entity.owners[0].name, + type: entity.owners[0].type as 'Users' | 'Teams', + endpoint: EntityTypeEndpoint.Domain, + dataTestId: 'owner-container', + initiatorId: 'add-owner', + }); } }; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index 2ae4eaa1f699..33fa03350e1f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -41,14 +41,21 @@ export const visitEntityPage = async (data: { await page.getByTestId('searchBox').clear(); }; -export const addOwner = async ( - page: Page, - owner: string, - type: 'Teams' | 'Users' = 'Users', - endpoint: EntityTypeEndpoint, - dataTestId?: string, - initiatorId = 'edit-owner' -) => { +export const addOwner = async ({ + page, + owner, + endpoint, + type = 'Users', + dataTestId, + initiatorId = 'edit-owner', +}: { + page: Page; + owner: string; + endpoint: EntityTypeEndpoint; + type?: 'Teams' | 'Users'; + dataTestId?: string; + initiatorId?: string; +}) => { await page.getByTestId(initiatorId).click(); if (type === 'Users') { const userListResponse = page.waitForResponse( @@ -67,12 +74,13 @@ export const addOwner = async ( await page.getByRole('tab', { name: type }).click(); } + const searchUser = page.waitForResponse( + `/api/v1/search/query?q=*${encodeURIComponent(owner)}*` + ); await page .getByTestId(`owner-select-${lowerCase(type)}-search-bar`) .fill(owner); - await page.waitForResponse( - `/api/v1/search/query?q=*${encodeURIComponent(owner)}*` - ); + await searchUser; if (type === 'Teams') { const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`); @@ -91,22 +99,30 @@ export const addOwner = async ( ); }; -export const updateOwner = async ( - page: Page, - owner: string, - type: 'Teams' | 'Users' = 'Users', - endpoint: EntityTypeEndpoint, - dataTestId?: string -) => { +export const updateOwner = async ({ + page, + owner, + endpoint, + type = 'Users', + dataTestId, +}: { + page: Page; + owner: string; + endpoint: EntityTypeEndpoint; + type?: 'Teams' | 'Users'; + dataTestId?: string; +}) => { await page.getByTestId('edit-owner').click(); await page.getByRole('tab', { name: type }).click(); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); + + const searchUser = page.waitForResponse( + `/api/v1/search/query?q=*${encodeURIComponent(owner)}*` + ); await page .getByTestId(`owner-select-${lowerCase(type)}-search-bar`) .fill(owner); - await page.waitForResponse( - `/api/v1/search/query?q=*${encodeURIComponent(owner)}*` - ); + await searchUser; if (type === 'Teams') { const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`); @@ -125,13 +141,19 @@ export const updateOwner = async ( ); }; -export const removeOwner = async ( - page: Page, - endpoint: EntityTypeEndpoint, - ownerName: string, - type: 'Teams' | 'Users' = 'Users', - dataTestId?: string -) => { +export const removeOwner = async ({ + page, + endpoint, + ownerName, + type = 'Users', + dataTestId, +}: { + page: Page; + endpoint: EntityTypeEndpoint; + ownerName: string; + type?: 'Teams' | 'Users'; + dataTestId?: string; +}) => { await page.getByTestId('edit-owner').click(); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); @@ -280,10 +302,11 @@ export const assignTag = async ( .getByTestId(action === 'Add' ? 'add-tag' : 'edit-button') .click(); - await page.locator('#tagsForm_tags').fill(tag); - await page.waitForResponse( + const searchTags = page.waitForResponse( `/api/v1/search/query?q=*${encodeURIComponent(tag)}*` ); + await page.locator('#tagsForm_tags').fill(tag); + await searchTags; await page.getByTestId(`tag-${tag}`).click(); await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); @@ -298,6 +321,53 @@ export const assignTag = async ( ).toBeVisible(); }; +export const assignTagToChildren = async ({ + page, + tag, + rowId, + action = 'Add', + rowSelector = 'data-row-key', +}: { + page: Page; + tag: string; + rowId: string; + action?: 'Add' | 'Edit'; + rowSelector?: string; +}) => { + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('tags-container') + .getByTestId(action === 'Add' ? 'add-tag' : 'edit-button') + .click(); + + const searchTags = page.waitForResponse( + `/api/v1/search/query?q=*${encodeURIComponent(tag)}*` + ); + + await page.locator('#tagsForm_tags').fill(tag); + + await searchTags; + + await page.getByTestId(`tag-${tag}`).click(); + + const patchRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' + ); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + + await patchRequest; + + await expect( + page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('tags-container') + .getByTestId(`tag-${tag}`) + ).toBeVisible(); +}; + export const removeTag = async (page: Page, tags: string[]) => { for (const tag of tags) { await page @@ -312,8 +382,8 @@ export const removeTag = async (page: Page, tags: string[]) => { .locator('svg') .click(); - const patchRequest = page.waitForRequest( - (request) => request.method() === 'PATCH' + const patchRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' ); await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); @@ -330,6 +400,49 @@ export const removeTag = async (page: Page, tags: string[]) => { } }; +export const removeTagsFromChildren = async ({ + page, + rowId, + tags, + rowSelector = 'data-row-key', +}: { + page: Page; + tags: string[]; + rowId: string; + rowSelector?: string; +}) => { + for (const tag of tags) { + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('tags-container') + .getByTestId('edit-button') + .click(); + + await page + .getByTestId('tag-selector') + .getByTestId(`selected-tag-${tag}`) + .getByTestId('remove-tags') + .click(); + + const patchTagRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' + ); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + + await patchTagRequest; + + await expect( + page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('tags-container') + .getByTestId(`tag-${tag}`) + ).not.toBeVisible(); + } +}; + type GlossaryTermOption = { displayName: string; name: string; @@ -347,10 +460,12 @@ export const assignGlossaryTerm = async ( .getByTestId(action === 'Add' ? 'add-tag' : 'edit-button') .click(); - await page.locator('#tagsForm_tags').fill(glossaryTerm.displayName); - await page.waitForResponse( + const searchGlossaryTerm = page.waitForResponse( `/api/v1/search/query?q=*${encodeURIComponent(glossaryTerm.displayName)}*` ); + + await page.locator('#tagsForm_tags').fill(glossaryTerm.displayName); + await searchGlossaryTerm; await page.getByTestId(`tag-${glossaryTerm.fullyQualifiedName}`).click(); await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); @@ -365,6 +480,50 @@ export const assignGlossaryTerm = async ( ).toBeVisible(); }; +export const assignGlossaryTermToChildren = async ({ + page, + glossaryTerm, + action = 'Add', + rowId, + rowSelector = 'data-row-key', +}: { + page: Page; + glossaryTerm: GlossaryTermOption; + rowId: string; + action?: 'Add' | 'Edit'; + rowSelector?: string; +}) => { + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('glossary-container') + .getByTestId(action === 'Add' ? 'add-tag' : 'edit-button') + .click(); + + const searchGlossaryTerm = page.waitForResponse( + `/api/v1/search/query?q=*${encodeURIComponent(glossaryTerm.displayName)}*` + ); + await page.locator('#tagsForm_tags').fill(glossaryTerm.displayName); + await searchGlossaryTerm; + await page.getByTestId(`tag-${glossaryTerm.fullyQualifiedName}`).click(); + + const patchRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' + ); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + + await patchRequest; + + await expect( + page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('glossary-container') + .getByTestId(`tag-${glossaryTerm.fullyQualifiedName}`) + ).toBeVisible(); +}; + export const removeGlossaryTerm = async ( page: Page, glossaryTerms: GlossaryTermOption[] @@ -383,8 +542,8 @@ export const removeGlossaryTerm = async ( .locator('svg') .click(); - const patchRequest = page.waitForRequest( - (request) => request.method() === 'PATCH' + const patchRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' ); await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); @@ -401,16 +560,63 @@ export const removeGlossaryTerm = async ( } }; +export const removeGlossaryTermFromChildren = async ({ + page, + glossaryTerms, + rowId, + rowSelector = 'data-row-key', +}: { + page: Page; + glossaryTerms: GlossaryTermOption[]; + rowId: string; + rowSelector?: string; +}) => { + for (const tag of glossaryTerms) { + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('glossary-container') + .getByTestId('edit-button') + .click(); + + await page + .getByTestId('glossary-container') + .getByTestId(new RegExp(tag.name)) + .getByTestId('remove-tags') + .locator('svg') + .click(); + + const patchRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' + ); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + + await patchRequest; + + expect( + page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('glossary-container') + .getByTestId(`tag-${tag.fullyQualifiedName}`) + ).not.toBeVisible(); + } +}; + export const upVote = async (page: Page, endPoint: string) => { + const patchRequest = page.waitForResponse(`/api/v1/${endPoint}/*/vote`); + await page.getByTestId('up-vote-btn').click(); - await page.waitForResponse(`/api/v1/${endPoint}/*/vote`); + await patchRequest; await expect(page.getByTestId('up-vote-count')).toContainText('1'); }; export const downVote = async (page: Page, endPoint: string) => { + const patchRequest = page.waitForResponse(`/api/v1/${endPoint}/*/vote`); await page.getByTestId('down-vote-btn').click(); - await page.waitForResponse(`/api/v1/${endPoint}/*/vote`); + await patchRequest; await expect(page.getByTestId('down-vote-count')).toContainText('1'); }; @@ -484,8 +690,11 @@ const announcementForm = async ( ); await page.locator('#announcement-submit').scrollIntoViewIfNeeded(); + const announcementSubmit = page.waitForResponse( + '/api/v1/feed?entityLink=*type=Announcement*' + ); await page.click('#announcement-submit'); - await page.waitForResponse('/api/v1/feed?entityLink=*type=Announcement*'); + await announcementSubmit; await page.click('.Toastify__close-button'); }; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts index 2243eaded7d7..dab69b6afa75 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -73,7 +73,7 @@ export const selectActiveGlossaryTerm = async ( export const goToAssetsTab = async ( page: Page, displayName: string, - count = '0' + count = 0 ) => { await selectActiveGlossaryTerm(page, displayName); await page.getByTestId('assets').click(); @@ -81,7 +81,7 @@ export const goToAssetsTab = async ( await expect( page.getByTestId('assets').getByTestId('filter-count') - ).toContainText(count); + ).toContainText(`${count}`); }; export const removeReviewer = async ( @@ -190,10 +190,16 @@ export const addTeamAsReviewer = async ( await page.fill('[data-testid="owner-select-teams-search-bar"]', teamName); await teamsSearchResponse; - await page.click(`.ant-popover [title="${teamName}"]`); + const ownerItem = page.locator(`.ant-popover [title="${teamName}"]`); - if (!isSelectableInsideForm) { - await page.waitForRequest((request) => request.method() === 'PATCH'); + if (isSelectableInsideForm) { + await ownerItem.click(); + } else { + const patchRequest = page.waitForRequest( + (request) => request.method() === 'PATCH' + ); + await ownerItem.click(); + await patchRequest; } await expect( @@ -261,6 +267,7 @@ export const createGlossary = async ( resultTestId: 'reviewers-container', endpoint: EntityTypeEndpoint.Glossary, isSelectableInsideForm: true, + type: 'Users', }); } else { await addTeamAsReviewer( @@ -349,16 +356,18 @@ export const deleteGlossary = async (page: Page, glossary: GlossaryData) => { await page.fill('[data-testid="confirmation-text-input"]', 'DELETE'); - await page.click('[data-testid="confirm-button"]'); - - // Wait for the API response and verify the status code - await page.waitForResponse( + const deleteGlossary = page.waitForResponse( (response) => response.url().includes('/api/v1/glossaries/') && response.request().method() === 'DELETE' && response.status() === 200 ); + await page.click('[data-testid="confirm-button"]'); + + // Wait for the API response and verify the status code + await deleteGlossary; + // Display toast notification await expect(page.locator('.toast-notification')).toHaveText( '"Glossary" deleted successfully!' @@ -450,6 +459,7 @@ export const fillGlossaryTermDetails = async ( resultTestId: 'owner-container', endpoint: EntityTypeEndpoint.GlossaryTerm, isSelectableInsideForm: true, + type: 'Users', }); } }; @@ -619,11 +629,7 @@ export const verifyGlossaryTermAssets = async ( await redirectToHomePage(page); await sidebarClick(page, SidebarItem.GLOSSARY); await selectActiveGlossary(page, glossary.displayName); - await goToAssetsTab( - page, - glossaryTermData.displayName, - assetsLength.toString() - ); + await goToAssetsTab(page, glossaryTermData.displayName, assetsLength); }; export const renameGlossaryTerm = async ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index c84df7862686..500cc4fa27f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -317,7 +317,7 @@ const DataModelDetails = ({ label: ( ), diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx index 36f34d4f6e31..4c68b747df0c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx @@ -315,6 +315,9 @@ function AlertDetailsPage({