From 785e450e286d559b3923e4c3a35e7eb3c33c5a31 Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Sat, 10 Aug 2024 21:39:27 +0530 Subject: [PATCH] MINOR: fix the widget placement when adding new one (#17329) * fix the widget placement when adding new one * fix the widget not adding in the right side add placeholders * added unit test for the same * fix sonar --- .../e2e/Flow/CustomizeLandingPage.spec.ts | 184 ++++++++++++++++++ .../support/persona/PersonaClass.ts | 76 ++++++++ .../ui/playwright/support/user/UserClass.ts | 13 ++ .../playwright/utils/customizeLandingPage.ts | 118 +++++++++++ .../AddWidgetModal/AddWidgetTabContent.tsx | 1 - .../ui/src/mocks/CustomizablePage.mock.ts | 39 +++- .../CustomizableLandingPageUtils.test.tsx | 26 ++- .../utils/CustomizableLandingPageUtils.tsx | 48 ++++- .../ui/src/utils/CustomizePageClassBase.ts | 2 +- 9 files changed, 496 insertions(+), 11 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeLandingPage.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/support/persona/PersonaClass.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeLandingPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeLandingPage.spec.ts new file mode 100644 index 00000000000..7a9ee664917 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/CustomizeLandingPage.spec.ts @@ -0,0 +1,184 @@ +/* + * 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 { PersonaClass } from '../../support/persona/PersonaClass'; +import { UserClass } from '../../support/user/UserClass'; +import { performAdminLogin } from '../../utils/admin'; +import { redirectToHomePage, toastNotification } from '../../utils/common'; +import { + checkAllDefaultWidgets, + navigateToCustomizeLandingPage, + removeAndCheckWidget, + setUserDefaultPersona, +} from '../../utils/customizeLandingPage'; + +const adminUser = new UserClass(); +const persona = new PersonaClass(); +const persona2 = new PersonaClass(); + +const test = base.extend<{ adminPage: Page; userPage: Page }>({ + adminPage: async ({ browser }, use) => { + const adminPage = await browser.newPage(); + await adminUser.login(adminPage); + await use(adminPage); + await adminPage.close(); + }, +}); + +base.beforeAll('Setup pre-requests', async ({ browser }) => { + const { afterAction, apiContext } = await performAdminLogin(browser); + await adminUser.create(apiContext); + await adminUser.setAdminRole(apiContext); + await persona.create(apiContext, [adminUser.responseData.id]); + await persona2.create(apiContext); + await afterAction(); +}); + +base.afterAll('Cleanup', async ({ browser }) => { + const { afterAction, apiContext } = await performAdminLogin(browser); + await adminUser.delete(apiContext); + await persona.delete(apiContext); + await persona2.delete(apiContext); + await afterAction(); +}); + +test.describe('Customize Landing Page Flow', () => { + test('Check all default widget present', async ({ adminPage }) => { + await redirectToHomePage(adminPage); + await adminPage.getByTestId('welcome-screen-close-btn').click(); + await checkAllDefaultWidgets(adminPage); + }); + + test('Remove and check widget', async ({ adminPage }) => { + await redirectToHomePage(adminPage); + await setUserDefaultPersona(adminPage, persona.responseData.displayName); + await navigateToCustomizeLandingPage(adminPage, { + personaName: persona.responseData.name, + customPageDataResponse: 404, + }); + + await removeAndCheckWidget(adminPage, { + widgetTestId: 'activity-feed-widget', + widgetKey: 'KnowledgePanel.ActivityFeed', + }); + await removeAndCheckWidget(adminPage, { + widgetTestId: 'following-widget', + widgetKey: 'KnowledgePanel.Following', + }); + await removeAndCheckWidget(adminPage, { + widgetTestId: 'kpi-widget', + widgetKey: 'KnowledgePanel.KPI', + }); + + const saveResponse = adminPage.waitForResponse('/api/v1/docStore'); + await adminPage.click('[data-testid="save-button"]'); + await saveResponse; + + await toastNotification(adminPage, 'Page layout created successfully.'); + await redirectToHomePage(adminPage); + + // Check if removed widgets are not present on landing adminPage + await expect( + adminPage.locator('[data-testid="activity-feed-widget"]') + ).not.toBeVisible(); + await expect( + adminPage.locator('[data-testid="following-widget"]') + ).not.toBeVisible(); + await expect( + adminPage.locator('[data-testid="kpi-widget"]') + ).not.toBeVisible(); + }); + + test('Remove and add the widget in the same placeholder', async ({ + adminPage, + }) => { + await redirectToHomePage(adminPage); + + const feedResponse = adminPage.waitForResponse( + '/api/v1/feed?type=Conversation&*' + ); + await navigateToCustomizeLandingPage(adminPage, { + personaName: persona2.responseData.name, + customPageDataResponse: 404, + }); + + await feedResponse; + + await adminPage.waitForSelector('[data-testid="activity-feed-widget"]'); + + const followingElementStyle = await adminPage + .locator('[id="KnowledgePanel.Following"]') + .evaluate((node) => { + const computedStyle = window.getComputedStyle(node); + + return { + transform: computedStyle.transform, + }; + }); + + // Remove and check the placement of Following widget. + await adminPage.click( + '[data-testid="following-widget"] [data-testid="remove-widget-button"]' + ); + + await adminPage.waitForSelector('[data-testid="following-widget"]', { + state: 'detached', + }); + + await adminPage.waitForSelector( + '[data-testid*="KnowledgePanel.Following"][data-testid$="EmptyWidgetPlaceholder"]' + ); + + // Add KPI widget in the same placeholder + const getWidgetList = adminPage.waitForResponse( + 'api/v1/docStore?fqnPrefix=KnowledgePanel&*' + ); + await adminPage.click( + '[data-testid="KnowledgePanel.Following.EmptyWidgetPlaceholder"] [data-testid="add-widget-button"]' + ); + + await getWidgetList; + + await adminPage.waitForSelector('[role="dialog"].ant-modal'); + + expect(adminPage.locator('[role="dialog"].ant-modal')).toBeVisible(); + + await adminPage.click('[data-testid="KPI-widget-tab-label"]'); + + await adminPage + .locator('.ant-tabs-tabpane-active [data-testid="add-widget-button"]') + .click(); + + await adminPage.waitForSelector('[role="dialog"].ant-modal', { + state: 'detached', + }); + + const kpiElement = adminPage.locator('[id^="KnowledgePanel.KPI-"]'); + const kpiElementStyle = await kpiElement.evaluate((node) => { + const computedStyle = window.getComputedStyle(node); + + return { + transform: computedStyle.transform, + }; + }); + + // Check if the KPI widget is added in the same placeholder,by their transform property or placement. + expect(kpiElementStyle.transform).toEqual(followingElementStyle.transform); + + const saveResponse = adminPage.waitForResponse('/api/v1/docStore'); + await adminPage.click('[data-testid="save-button"]'); + await saveResponse; + + await toastNotification(adminPage, 'Page layout created successfully.'); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/persona/PersonaClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/persona/PersonaClass.ts new file mode 100644 index 00000000000..0ee9ef441db --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/persona/PersonaClass.ts @@ -0,0 +1,76 @@ +/* + * 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 } from '@playwright/test'; +import { uuid } from '../../utils/common'; + +type ResponseDataType = { + name: string; + displayName: string; + description: string; + id?: string; + fullyQualifiedName?: string; + users?: string[]; +}; + +export class PersonaClass { + id = uuid(); + data: ResponseDataType; + responseData: ResponseDataType; + + constructor(data?: ResponseDataType) { + this.data = data ?? { + name: `PW%Persona-${this.id}`, + displayName: `PW Persona ${this.id}`, + description: 'playwright for persona description', + users: [], + }; + } + + get() { + return this.responseData; + } + + async create(apiContext: APIRequestContext, users?: string[]) { + const response = await apiContext.post('/api/v1/personas', { + data: { ...this.data, users }, + }); + const data = await response.json(); + this.responseData = data; + + return data; + } + + async delete(apiContext: APIRequestContext) { + const response = await apiContext.delete( + `/api/v1/personas/${this.responseData.id}?hardDelete=true&recursive=false` + ); + + return await response.json(); + } + + async patch(apiContext: APIRequestContext, data: Record[]) { + const response = await apiContext.patch( + `/api/v1/personas/${this.responseData.id}`, + { + data, + headers: { + 'Content-Type': 'application/json-patch+json', + }, + } + ); + + this.responseData = await response.json(); + + return await response.json(); + } +} diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts index 318e9afad2d..a7988dca0a2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts @@ -61,6 +61,19 @@ export class UserClass { }; } + async setAdminRole(apiContext: APIRequestContext) { + return this.patch({ + apiContext, + patchData: [ + { + op: 'replace', + path: '/isAdmin', + value: true, + }, + ], + }); + } + async delete(apiContext: APIRequestContext) { const response = await apiContext.delete( `/api/v1/users/${this.responseData.id}?recursive=false&hardDelete=true` diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts new file mode 100644 index 00000000000..a767a8f46b6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts @@ -0,0 +1,118 @@ +/* + * 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 { GlobalSettingOptions } from '../constant/settings'; +import { visitUserProfilePage } from './common'; +import { settingClick } from './sidebar'; + +export const navigateToCustomizeLandingPage = async ( + page: Page, + { personaName, customPageDataResponse } +) => { + const getPersonas = page.waitForResponse('/api/v1/personas*'); + + await settingClick(page, GlobalSettingOptions.CUSTOMIZE_LANDING_PAGE); + + await getPersonas; + + // Navigate to the customize landing page + await page.click( + `[data-testid="persona-details-card-${personaName}"] [data-testid="customize-page-button"]` + ); + + const getCustomPageDataResponse = await page.waitForResponse( + `/api/v1/docStore/name/persona.${encodeURIComponent( + personaName + )}.Page.LandingPage` + ); + + // await getCustomPageDataResponse; + expect(getCustomPageDataResponse.status()).toBe(customPageDataResponse); +}; + +export const removeAndCheckWidget = async ( + page: Page, + { widgetTestId, widgetKey } +) => { + // Click on remove widget button + await page.click( + `[data-testid="${widgetTestId}"] [data-testid="remove-widget-button"]` + ); + + // Check if widget does not exist + await page.waitForSelector(`[data-testid="${widgetTestId}"]`, { + state: 'detached', + }); + + // Check if empty widget placeholder is displayed in place of removed widget + await page.waitForSelector( + `[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"]` + ); + + // Remove empty widget placeholder + await page.click( + `[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"] [data-testid="remove-widget-button"]` + ); + + // Check if empty widget placeholder does not exist + await page.waitForSelector( + `[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"]`, + { state: 'detached' } + ); +}; + +export const checkAllDefaultWidgets = async ( + page: Page, + checkEmptyWidgetPlaceholder = false +) => { + await expect(page.getByTestId('activity-feed-widget')).toBeVisible(); + await expect(page.getByTestId('following-widget')).toBeVisible(); + await expect(page.getByTestId('recently-viewed-widget')).toBeVisible(); + await expect(page.getByTestId('data-assets-widget')).toBeVisible(); + await expect(page.getByTestId('my-data-widget')).toBeVisible(); + await expect(page.getByTestId('kpi-widget')).toBeVisible(); + await expect(page.getByTestId('total-assets-widget')).toBeVisible(); + + if (checkEmptyWidgetPlaceholder) { + await expect( + page.getByTestId('ExtraWidget.EmptyWidgetPlaceholder') + ).toBeVisible(); + } +}; + +export const setUserDefaultPersona = async ( + page: Page, + personaName: string +) => { + await visitUserProfilePage(page); + + await page + .locator( + '[data-testid="user-profile-details"] [data-testid="edit-persona"]' + ) + .click(); + + await page.waitForSelector( + '[role="tooltip"] [data-testid="selectable-list"]' + ); + + const setDefaultPersona = page.waitForResponse('/api/v1/users/*'); + + await page.getByTitle(personaName).click(); + + await setDefaultPersona; + + await expect( + page.locator('[data-testid="user-profile-details"]') + ).toContainText(personaName); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.tsx index f4c040236d8..2d02ad29415 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.tsx @@ -90,7 +90,6 @@ function AddWidgetTabContent({ placement="bottom" title={widgetAddable ? '' : t('message.can-not-add-widget')}>