diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/GlossaryVersionPage.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/GlossaryVersionPage.spec.ts index 90c9573702c..a5c7445797b 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/GlossaryVersionPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/GlossaryVersionPage.spec.ts @@ -32,7 +32,8 @@ import { REVIEWER_DETAILS, } from '../../constants/Version.constants'; -describe( +// migrated to playwright +describe.skip( 'Glossary and glossary term version pages should work properly', { tags: 'Glossary' }, () => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/version.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/version.ts new file mode 100644 index 00000000000..1ca15e7ae1b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/version.ts @@ -0,0 +1,80 @@ +/* + * 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 GLOSSARY_PATCH_PAYLOAD = [ + { + op: 'add', + path: '/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.SpecialCategory', + }, + }, + { + op: 'add', + path: '/tags/1', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PII.Sensitive', + }, + }, + { + op: 'replace', + path: '/description', + value: 'Description for newly added glossary', + }, +]; + +export const GLOSSARY_TERM_PATCH_PAYLOAD = [ + { + op: 'add', + path: '/synonyms/0', + value: 'test-synonym', + }, + { + op: 'add', + path: '/references/0', + value: { + name: 'reference1', + endpoint: 'https://example.com', + }, + }, + { + op: 'add', + path: '/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.SpecialCategory', + }, + }, + { + op: 'add', + path: '/tags/1', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PII.Sensitive', + }, + }, + { + op: 'replace', + path: '/description', + value: 'Description for newly added glossaryTerm', + }, +]; 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 a36549d682e..bb3f322d4d4 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 @@ -24,6 +24,7 @@ import { TableClass } from '../../support/entity/TableClass'; import { TopicClass } from '../../support/entity/TopicClass'; import { createNewPage, + getApiContext, getAuthContext, getToken, redirectToHomePage, @@ -133,8 +134,7 @@ entities.forEach((EntityClass) => { // increase timeout as it using single test for multiple steps test.slow(true); - const token = await getToken(page); - const apiContext = await getAuthContext(token); + const { apiContext, afterAction } = await getApiContext(page); await entity.prepareCustomProperty(apiContext); await test.step(`Set ${titleText} Custom Property`, async () => { @@ -158,6 +158,7 @@ entities.forEach((EntityClass) => { }); await entity.cleanupCustomProperty(apiContext); + await afterAction(); }); } 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 new file mode 100644 index 00000000000..d3e085b2bba --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryVersionPage.spec.ts @@ -0,0 +1,220 @@ +/* + * 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 { GLOSSARY_PATCH_PAYLOAD } from '../../constant/version'; +import { EntityTypeEndpoint } from '../../support/entity/Entity.interface'; +import { Glossary } from '../../support/glossary/Glossary'; +import { UserClass } from '../../support/user/UserClass'; +import { + createNewPage, + getApiContext, + redirectToHomePage, +} from '../../utils/common'; +import { addOwner } from '../../utils/entity'; +import { addMultiOwner, setupGlossaryAndTerms } from '../../utils/glossary'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +const user = new UserClass(); +const reviewer = new UserClass(); + +test.beforeAll(async ({ browser }) => { + const { afterAction, apiContext } = await createNewPage(browser); + await user.create(apiContext); + await reviewer.create(apiContext); + await afterAction(); +}); + +test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); +}); + +test('Glossary', async ({ page }) => { + const glossary = new Glossary(); + const { afterAction, apiContext } = await getApiContext(page); + await glossary.create(apiContext); + await glossary.patch(apiContext, GLOSSARY_PATCH_PAYLOAD); + + await test.step('Version changes', async () => { + await glossary.visitPage(page); + + await page.click('[data-testid="version-button"]'); + + await expect( + page + .getByTestId('asset-description-container') + .getByTestId('markdown-parser') + .locator('span') + .filter({ hasText: 'Description' }) + ).toBeVisible(); + + await expect( + page.locator( + '.diff-added [data-testid="tag-PersonalData.SpecialCategory"]' + ) + ).toBeVisible(); + + await expect( + page.locator('.diff-added [data-testid="tag-PII.Sensitive"]') + ).toBeVisible(); + }); + + await test.step('Should display the owner & reviewer changes', async () => { + await glossary.visitPage(page); + + await expect(page.getByTestId('version-button')).toHaveText(/0.2/); + + await addOwner( + page, + user.getUserName(), + 'Users', + EntityTypeEndpoint.Glossary, + 'glossary-right-panel-owner-link' + ); + + await page.reload(); + const versionPageResponse = page.waitForResponse( + `/api/v1/glossaries/${glossary.responseData.id}/versions/0.2` + ); + await page.click('[data-testid="version-button"]'); + await versionPageResponse; + + await expect( + page.locator( + '[data-testid="glossary-right-panel-owner-link"] [data-testid="diff-added"]' + ) + ).toBeVisible(); + + await page.click('[data-testid="version-button"]'); + await versionPageResponse; + + await addMultiOwner({ + page, + ownerNames: [reviewer.getUserName()], + activatorBtnDataTestId: 'Add', + resultTestId: 'glossary-reviewer-name', + endpoint: EntityTypeEndpoint.Glossary, + }); + + await page.reload(); + await page.click('[data-testid="version-button"]'); + await versionPageResponse; + + await expect( + page.locator( + '[data-testid="glossary-reviewer"] [data-testid="diff-added"]' + ) + ).toBeVisible(); + }); + + await glossary.delete(apiContext); + await afterAction(); +}); + +test('GlossaryTerm', async ({ page }) => { + const { term1, term2, cleanup } = await setupGlossaryAndTerms(page); + + await test.step('Version changes', async () => { + await term2.visitPage(page); + + await page.click('[data-testid="version-button"]'); + + await expect( + page + .getByTestId('asset-description-container') + .getByTestId('markdown-parser') + .locator('span') + .filter({ hasText: 'Description' }) + ).toBeVisible(); + + await expect( + page.locator( + '.diff-added [data-testid="tag-PersonalData.SpecialCategory"]' + ) + ).toBeVisible(); + + await expect( + page.locator('.diff-added [data-testid="tag-PII.Sensitive"]') + ).toBeVisible(); + + await expect( + page.locator('[data-testid="test-synonym"].diff-added') + ).toBeVisible(); + + await expect( + page.locator(`[data-testid="${term1.data.displayName}"].diff-added`) + ).toBeVisible(); + + await expect( + page.locator('.diff-added [data-testid="reference-link-reference1"]') + ).toBeVisible(); + }); + + await test.step('Should display the owner & reviewer changes', async () => { + await term2.visitPage(page); + + await expect(page.getByTestId('version-button')).toHaveText(/0.2/); + + await addOwner( + page, + user.getUserName(), + 'Users', + EntityTypeEndpoint.GlossaryTerm, + 'glossary-right-panel-owner-link' + ); + await page.reload(); + const versionPageResponse = page.waitForResponse( + `/api/v1/glossaryTerms/${term2.responseData.id}/versions/0.2` + ); + await page.click('[data-testid="version-button"]'); + await versionPageResponse; + + await expect( + page.locator( + '[data-testid="glossary-right-panel-owner-link"] [data-testid="diff-added"]' + ) + ).toBeVisible(); + + await page.click('[data-testid="version-button"]'); + await versionPageResponse; + + await addMultiOwner({ + page, + ownerNames: [reviewer.getUserName()], + activatorBtnDataTestId: 'Add', + resultTestId: 'glossary-reviewer-name', + endpoint: EntityTypeEndpoint.GlossaryTerm, + }); + + await page.reload(); + await page.click('[data-testid="version-button"]'); + await versionPageResponse; + + await expect( + page.locator( + '[data-testid="glossary-reviewer"] [data-testid="diff-added"]' + ) + ).toBeVisible(); + }); + + await cleanup(); +}); + +test.afterAll(async ({ browser }) => { + const { afterAction, apiContext } = await createNewPage(browser); + + await user.delete(apiContext); + await reviewer.delete(apiContext); + await afterAction(); +}); 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 8a49d1e74c3..f75064ba130 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 @@ -13,8 +13,8 @@ import { APIRequestContext, Page } from '@playwright/test'; import { CustomPropertySupportedEntityList } from '../../constant/customProperty'; import { - CustomProperty, createCustomPropertyForEntity, + CustomProperty, setValueForProperty, validateValueForProperty, } from '../../utils/customProperty'; @@ -37,15 +37,15 @@ import { replyAnnouncement, softDeleteEntity, unFollowEntity, - upVote, updateDescription, updateDisplayNameForEntity, updateOwner, + upVote, validateFollowedEntityToWidget, } from '../../utils/entity'; import { Domain } from '../domain/Domain'; import { GlossaryTerm } from '../glossary/GlossaryTerm'; -import { ENTITY_PATH, EntityTypeEndpoint } from './Entity.interface'; +import { EntityTypeEndpoint, ENTITY_PATH } from './Entity.interface'; export class EntityClass { type: string; @@ -123,7 +123,7 @@ export class EntityClass { ) { await addOwner(page, owner1, type, this.endpoint, 'data-assets-header'); await updateOwner(page, owner2, type, this.endpoint, 'data-assets-header'); - await removeOwner(page, this.endpoint, 'data-assets-header'); + await removeOwner(page, this.endpoint, owner2, 'data-assets-header'); } async tier(page: Page, tier1: string, tier2: string) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/glossary/Glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/glossary/Glossary.ts index 9343893d1fd..14832434aa1 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/glossary/Glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/glossary/Glossary.ts @@ -10,8 +10,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { APIRequestContext } from '@playwright/test'; +import { APIRequestContext, expect, Page } from '@playwright/test'; import { uuid } from '../../utils/common'; +import { visitGlossaryPage } from '../../utils/glossary'; import { getRandomFirstName } from '../../utils/user'; type ResponseDataType = { @@ -43,6 +44,14 @@ export class Glossary { this.data.name = name ?? this.data.name; } + async visitPage(page: Page) { + await visitGlossaryPage(page, this.data.displayName); + + await expect(page.getByTestId('entity-header-display-name')).toHaveText( + this.data.displayName + ); + } + async create(apiContext: APIRequestContext) { const response = await apiContext.post('/api/v1/glossaries', { data: this.data, @@ -53,6 +62,22 @@ export class Glossary { return await response.json(); } + async patch(apiContext: APIRequestContext, data: Record[]) { + const response = await apiContext.patch( + `/api/v1/glossaries/${this.responseData.id}`, + { + data, + headers: { + 'Content-Type': 'application/json-patch+json', + }, + } + ); + + this.responseData = await response.json(); + + return await response.json(); + } + get() { return this.responseData; } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/glossary/GlossaryTerm.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/glossary/GlossaryTerm.ts index 64a3edf485d..61ee7234365 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/glossary/GlossaryTerm.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/glossary/GlossaryTerm.ts @@ -10,8 +10,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { APIRequestContext } from '@playwright/test'; +import { APIRequestContext, expect, Page } from '@playwright/test'; import { uuid } from '../../utils/common'; +import { visitGlossaryPage } from '../../utils/glossary'; import { getRandomLastName } from '../../utils/user'; type ResponseDataType = { @@ -23,7 +24,7 @@ type ResponseDataType = { synonyms: unknown[]; mutuallyExclusive: boolean; tags: unknown[]; - glossary: Record; + glossary: Record; id: string; fullyQualifiedName: string; }; @@ -45,6 +46,32 @@ export class GlossaryTerm { this.data.name = name ?? this.data.name; } + async visitPage(page: Page) { + await visitGlossaryPage(page, this.responseData.glossary.displayName); + const expandCollapseButtonText = await page + .locator('[data-testid="expand-collapse-all-button"]') + .textContent(); + const isExpanded = expandCollapseButtonText?.includes('Expand All'); + if (isExpanded) { + const glossaryTermListResponse = page.waitForResponse( + `/api/v1/glossaryTerms?*glossary=${this.responseData.glossary.id}*` + ); + await page.click('[data-testid="expand-collapse-all-button"]'); + await glossaryTermListResponse; + } + const glossaryTermResponse = page.waitForResponse( + `/api/v1/glossaryTerms/name/${encodeURIComponent( + this.responseData.fullyQualifiedName + )}?*` + ); + await page.getByTestId(this.data.displayName).click(); + await glossaryTermResponse; + + await expect(page.getByTestId('entity-header-display-name')).toHaveText( + this.data.displayName + ); + } + async create(apiContext: APIRequestContext) { const response = await apiContext.post('/api/v1/glossaryTerms', { data: this.data, @@ -55,6 +82,22 @@ export class GlossaryTerm { return await response.json(); } + async patch(apiContext: APIRequestContext, data: Record[]) { + const response = await apiContext.patch( + `/api/v1/glossaryTerms/${this.responseData.id}`, + { + data, + headers: { + 'Content-Type': 'application/json-patch+json', + }, + } + ); + + this.responseData = await response.json(); + + return await response.json(); + } + get() { return this.responseData; } 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 3dbbf2810ba..d4386328d9b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -109,6 +109,7 @@ export const updateOwner = async ( export const removeOwner = async ( page: Page, endpoint: EntityTypeEndpoint, + ownerName: string, dataTestId?: string ) => { await page.getByTestId('edit-owner').click(); @@ -120,8 +121,8 @@ export const removeOwner = async ( await page.getByTestId('remove-owner').locator('svg').click(); await patchRequest; - await expect(page.getByTestId(dataTestId ?? 'owner-link')).toContainText( - 'No Owner' + await expect(page.getByTestId(dataTestId ?? 'owner-link')).not.toContainText( + ownerName ); }; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts new file mode 100644 index 00000000000..d18270f78b5 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -0,0 +1,147 @@ +/* + * 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 { SidebarItem } from '../constant/sidebar'; +import { GLOSSARY_TERM_PATCH_PAYLOAD } from '../constant/version'; +import { EntityTypeEndpoint } from '../support/entity/Entity.interface'; +import { Glossary } from '../support/glossary/Glossary'; +import { GlossaryTerm } from '../support/glossary/GlossaryTerm'; +import { getApiContext, redirectToHomePage } from './common'; +import { sidebarClick } from './sidebar'; + +export const visitGlossaryPage = async (page: Page, glossaryName: string) => { + await redirectToHomePage(page); + const glossaryResponse = page.waitForResponse('/api/v1/glossaries?fields=*'); + await sidebarClick(page, SidebarItem.GLOSSARY); + await glossaryResponse; + await page.getByRole('menuitem', { name: glossaryName }).click(); +}; + +export const addMultiOwner = async (data: { + page: Page; + ownerNames: string | string[]; + activatorBtnDataTestId: string; + endpoint: EntityTypeEndpoint; + resultTestId?: string; + isSelectableInsideForm?: boolean; +}) => { + const { + page, + ownerNames, + activatorBtnDataTestId, + resultTestId = 'owner-link', + isSelectableInsideForm = false, + endpoint, + } = data; + const isMultipleOwners = Array.isArray(ownerNames); + const owners = isMultipleOwners ? ownerNames : [ownerNames]; + + const getUsers = page.waitForResponse('/api/v1/users?*isBot=false*'); + + await page.click(`[data-testid="${activatorBtnDataTestId}"]`); + + expect(page.locator("[data-testid='select-owner-tabs']")).toBeVisible(); + + await page.click('.ant-tabs [id*=tab-users]'); + await getUsers; + await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); + + if (isMultipleOwners) { + await page.click('[data-testid="clear-all-button"]'); + } + + for (const ownerName of owners) { + const searchOwner = page.waitForResponse( + 'api/v1/search/query?q=*&index=user_search_index*' + ); + await page.locator('[data-testid="owner-select-users-search-bar"]').clear(); + await page.fill('[data-testid="owner-select-users-search-bar"]', ownerName); + await searchOwner; + await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); + await page.getByRole('listitem', { name: ownerName }).click(); + } + + const patchResponse = page.waitForResponse(`/api/v1/${endpoint}/*`); + if (isMultipleOwners) { + await page.click('[data-testid="selectable-list-update-btn"]'); + } + + if (!isSelectableInsideForm) { + await patchResponse; + } + + for (const name of owners) { + await expect(page.locator(`[data-testid="${resultTestId}"]`)).toContainText( + name + ); + } +}; + +export const removeReviewer = async ( + page: Page, + endpoint: EntityTypeEndpoint +) => { + const patchResponse = page.waitForResponse(`/api/v1/${endpoint}/*`); + + await page.click('[data-testid="edit-reviewer-button"]'); + + await page.click('[data-testid="clear-all-button"]'); + + await page.click('[data-testid="selectable-list-update-btn"]'); + + await patchResponse; + + await expect( + page.locator('[data-testid="glossary-reviewer"] [data-testid="Add"]') + ).toBeVisible(); +}; + +// Create a glossary and two glossary terms, then link them with a related term relationship +export const setupGlossaryAndTerms = async (page: Page) => { + const glossary = new Glossary(); + const term1 = new GlossaryTerm(glossary.data.name); + const term2 = new GlossaryTerm(glossary.data.name); + + // Get API context for performing operations + const { apiContext, afterAction } = await getApiContext(page); + + // Create glossary and terms + await glossary.create(apiContext); + await term1.create(apiContext); + await term2.create(apiContext); + + // Prepare the payload for linking term2 as a related term to term1 + const relatedTermLink = { + op: 'add', + path: '/relatedTerms/0', + value: { + id: term1.responseData.id, + type: 'glossaryTerm', + displayName: term1.responseData.displayName, + name: term1.responseData.name, + }, + }; + + // Update term2 to include term1 as a related term + await term2.patch(apiContext, [ + ...GLOSSARY_TERM_PATCH_PAYLOAD, + relatedTermLink, + ]); + + const cleanup = async () => { + await glossary.delete(apiContext); + await afterAction(); + }; + + return { glossary, term1, term2, cleanup }; +};