fix(ui): tour functionality (#22711)

* fix: hide scrollbar from sidebar

* fix feed id

* fix tour

* fix tour test

* monir fix

* remove redundunt myData tests

* fix tourpage useEffect

* fix curated asset test
This commit is contained in:
Pranita Fulsundar 2025-08-04 12:17:25 +05:30 committed by GitHub
parent c7f50c9376
commit 802c922ec8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 84 additions and 144 deletions

View File

@ -35,6 +35,8 @@ const test = base.extend<{ page: Page }>({
}); });
base.beforeAll('Setup pre-requests', async ({ browser }) => { base.beforeAll('Setup pre-requests', async ({ browser }) => {
test.slow(true);
const { afterAction, apiContext } = await performAdminLogin(browser); const { afterAction, apiContext } = await performAdminLogin(browser);
await adminUser.create(apiContext); await adminUser.create(apiContext);
await adminUser.setAdminRole(apiContext); await adminUser.setAdminRole(apiContext);
@ -43,6 +45,8 @@ base.beforeAll('Setup pre-requests', async ({ browser }) => {
}); });
base.afterAll('Cleanup', async ({ browser }) => { base.afterAll('Cleanup', async ({ browser }) => {
test.slow(true);
const { afterAction, apiContext } = await performAdminLogin(browser); const { afterAction, apiContext } = await performAdminLogin(browser);
await adminUser.delete(apiContext); await adminUser.delete(apiContext);
await persona.delete(apiContext); await persona.delete(apiContext);
@ -50,7 +54,7 @@ base.afterAll('Cleanup', async ({ browser }) => {
}); });
test.describe('Curated Assets', () => { test.describe('Curated Assets', () => {
test.beforeEach(async ({ page }) => { test.beforeAll(async ({ page }) => {
test.slow(true); test.slow(true);
await redirectToHomePage(page); await redirectToHomePage(page);
@ -93,7 +97,7 @@ test.describe('Curated Assets', () => {
await selectOption( await selectOption(
page, page,
ruleLocator.locator('.rule--operator .ant-select'), ruleLocator.locator('.rule--operator .ant-select'),
'!=' 'Not in'
); );
await selectOption( await selectOption(

View File

@ -18,6 +18,9 @@ import { redirectToHomePage } from '../../utils/common';
const user = new UserClass(); const user = new UserClass();
const validateTourSteps = async (page: Page) => { const validateTourSteps = async (page: Page) => {
await page.waitForTimeout(1000);
await page.waitForSelector(`[data-tour-elem="badge"]`);
await expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('1'); await expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('1');
// step 1 // step 1
@ -30,8 +33,8 @@ const validateTourSteps = async (page: Page) => {
await expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('3'); await expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('3');
await page.getByTestId('customise-searchbox').fill('dim_a'); await page.getByTestId('searchBox').fill('dim_a');
await page.getByTestId('customise-searchbox').press('Enter'); await page.getByTestId('searchBox').press('Enter');
await expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('4'); await expect(page.locator(`[data-tour-elem="badge"]`)).toHaveText('4');
@ -108,7 +111,7 @@ const validateTourSteps = async (page: Page) => {
await page.getByTestId('saveButton').click(); await page.getByTestId('saveButton').click();
}; };
test.describe.skip('Tour should work properly', () => { test.describe('Tour should work properly', () => {
test.beforeAll(async ({ browser }) => { test.beforeAll(async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser); const { apiContext, afterAction } = await performAdminLogin(browser);
await user.create(apiContext); await user.create(apiContext);
@ -130,6 +133,9 @@ test.describe.skip('Tour should work properly', () => {
await page.locator('[data-testid="help-icon"]').click(); await page.locator('[data-testid="help-icon"]').click();
await page.getByRole('link', { name: 'Tour', exact: true }).click(); await page.getByRole('link', { name: 'Tour', exact: true }).click();
await page.waitForURL('**/tour'); await page.waitForURL('**/tour');
await page.waitForSelector('#feedWidgetData');
await validateTourSteps(page); await validateTourSteps(page);
}); });
@ -141,14 +147,17 @@ test.describe.skip('Tour should work properly', () => {
await page.getByText('Take a product tour to get started!').click(); await page.getByText('Take a product tour to get started!').click();
await page.waitForURL('**/tour'); await page.waitForURL('**/tour');
await page.waitForSelector('#feedWidgetData');
await validateTourSteps(page); await validateTourSteps(page);
}); });
test('Tour should work from URL directly', async ({ page }) => { test('Tour should work from URL directly', async ({ page }) => {
await expect(page.getByTestId('global-search-selector')).toBeVisible();
await page.goto('/tour'); await page.goto('/tour');
await page.waitForURL('**/tour'); await page.waitForURL('**/tour');
await page.waitForSelector('#feedWidgetData');
await validateTourSteps(page); await validateTourSteps(page);
}); });
}); });

View File

@ -1,122 +0,0 @@
/*
* 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 { TableClass } from '../../support/entity/TableClass';
import { UserClass } from '../../support/user/UserClass';
import { performAdminLogin } from '../../utils/admin';
import { redirectToHomePage, removeLandingBanner } from '../../utils/common';
import { verifyEntities } from '../../utils/myData';
const user = new UserClass();
const TableEntities = Array(20)
.fill(undefined)
.map(() => new TableClass());
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();
},
});
test.describe.serial('My Data page', () => {
test.beforeAll('Setup pre-requests', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await user.create(apiContext);
const tablePromises = TableEntities.map(async (table) => {
await table.create(apiContext);
await table.patch({
apiContext,
patchData: [
{
op: 'add',
path: '/owners/0',
value: {
id: user.responseData.id,
type: 'user',
deleted: false,
displayName: user.responseData.displayName,
fullyQualifiedName: user.responseData.fullyQualifiedName,
href: user.responseData['href'] ?? '',
name: user.responseData.name,
},
},
],
});
await table.followTable(apiContext, user.responseData.id);
});
await Promise.all(tablePromises);
await afterAction();
});
test.afterAll('Cleanup', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await user.delete(apiContext);
await Promise.all(TableEntities.map((table) => table.delete(apiContext)));
await afterAction();
});
test.beforeEach('Visit entity details page', async ({ page }) => {
await redirectToHomePage(page);
await removeLandingBanner(page);
});
test.skip('Verify my data widget', async ({ page }) => {
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
// Verify total count
await expect(
page.locator('[data-testid="my-data-total-count"]')
).toContainText('(20)');
await page
.locator('[data-testid="my-data-widget"] [data-testid="view-all-link"]')
.click();
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
// Verify entities
await verifyEntities(
page,
'/api/v1/search/query?q=*&index=all&from=0&size=25*',
TableEntities
);
});
test.skip('Verify following widget', async ({ page }) => {
// Verify total count
await expect(
page.locator('[data-testid="following-data-total-count"]')
).toContainText('(20)');
await page.locator('[data-testid="following-data"]').click();
// Verify entities
await verifyEntities(
page,
'/api/v1/search/query?q=*followers:*&index=all&from=0&size=25*',
TableEntities
);
});
});

View File

@ -178,7 +178,7 @@ const MyFeedWidgetInternal = ({
dataTestId="KnowledgePanel.ActivityFeed" dataTestId="KnowledgePanel.ActivityFeed"
header={widgetHeader} header={widgetHeader}
loading={loading}> loading={loading}>
<div className="feed-widget-container"> <div className="feed-widget-container" id="feedWidgetData">
<div className="feed-content flex-1"> <div className="feed-content flex-1">
{widgetBody} {widgetBody}
<WidgetFooter <WidgetFooter

View File

@ -312,6 +312,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
} }
.top-menu { .top-menu {

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Tour from '../../components/AppTour/Tour'; import Tour from '../../components/AppTour/Tour';
import { TOUR_SEARCH_TERM } from '../../constants/constants'; import { TOUR_SEARCH_TERM } from '../../constants/constants';
@ -31,6 +31,7 @@ const TourPage = () => {
updateTourSearch, updateTourSearch,
} = useTourProvider(); } = useTourProvider();
const { t } = useTranslation(); const { t } = useTranslation();
const [isTourReady, setIsTourReady] = useState(false);
const clearSearchTerm = () => { const clearSearchTerm = () => {
updateTourSearch(''); updateTourSearch('');
@ -38,6 +39,21 @@ const TourPage = () => {
useEffect(() => { useEffect(() => {
updateIsTourOpen(true); updateIsTourOpen(true);
let attempts = 0;
const maxAttempts = 10;
const waitForElement = () => {
const el = document.querySelector('#feedWidgetData');
if (el) {
setIsTourReady(true);
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(waitForElement, 100);
}
};
waitForElement();
}, []); }, []);
const currentPageComponent = useMemo(() => { const currentPageComponent = useMemo(() => {
@ -58,15 +74,17 @@ const TourPage = () => {
return ( return (
<> <>
<Tour
steps={getTourSteps({
searchTerm: TOUR_SEARCH_TERM,
clearSearchTerm,
updateActiveTab,
updateTourPage,
})}
/>
{currentPageComponent} {currentPageComponent}
{isTourReady && (
<Tour
steps={getTourSteps({
searchTerm: TOUR_SEARCH_TERM,
clearSearchTerm,
updateActiveTab,
updateTourPage,
})}
/>
)}
</> </>
); );
}; };

View File

@ -17,11 +17,18 @@ import TourPage from './TourPage.component';
const mockUseTourProvider = { const mockUseTourProvider = {
updateIsTourOpen: jest.fn(), updateIsTourOpen: jest.fn(),
currentTourPage: '', currentTourPage: CurrentTourPageType.MY_DATA_PAGE,
updateActiveTab: jest.fn(), updateActiveTab: jest.fn(),
updateTourPage: jest.fn(), updateTourPage: jest.fn(),
updateTourSearch: jest.fn(), updateTourSearch: jest.fn(),
}; };
const mockQuerySelector = jest.fn();
Object.defineProperty(document, 'querySelector', {
value: mockQuerySelector,
writable: true,
});
jest.mock('../../context/TourProvider/TourProvider', () => ({ jest.mock('../../context/TourProvider/TourProvider', () => ({
useTourProvider: jest.fn().mockImplementation(() => mockUseTourProvider), useTourProvider: jest.fn().mockImplementation(() => mockUseTourProvider),
})); }));
@ -53,6 +60,22 @@ jest.mock('../../utils/TourUtils', () => ({
})); }));
describe('TourPage component', () => { describe('TourPage component', () => {
beforeEach(() => {
jest.useFakeTimers();
mockQuerySelector.mockImplementation((selector) => {
if (selector === '#feedWidgetData') {
return document.createElement('div');
}
return null;
});
});
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
it('should render correctly', async () => { it('should render correctly', async () => {
render(<TourPage />); render(<TourPage />);
@ -69,7 +92,8 @@ describe('TourPage component', () => {
}); });
it('MyDataPage Component should be visible, if currentTourPage is myDataPage', async () => { it('MyDataPage Component should be visible, if currentTourPage is myDataPage', async () => {
(useTourProvider as jest.Mock).mockImplementationOnce(() => ({ (useTourProvider as jest.Mock).mockReset();
(useTourProvider as jest.Mock).mockImplementation(() => ({
...mockUseTourProvider, ...mockUseTourProvider,
currentTourPage: CurrentTourPageType.MY_DATA_PAGE, currentTourPage: CurrentTourPageType.MY_DATA_PAGE,
})); }));
@ -79,7 +103,8 @@ describe('TourPage component', () => {
}); });
it('ExplorePage Component should be visible, if currentTourPage is explorePage', async () => { it('ExplorePage Component should be visible, if currentTourPage is explorePage', async () => {
(useTourProvider as jest.Mock).mockImplementationOnce(() => ({ (useTourProvider as jest.Mock).mockReset();
(useTourProvider as jest.Mock).mockImplementation(() => ({
...mockUseTourProvider, ...mockUseTourProvider,
currentTourPage: CurrentTourPageType.EXPLORE_PAGE, currentTourPage: CurrentTourPageType.EXPLORE_PAGE,
})); }));
@ -91,7 +116,8 @@ describe('TourPage component', () => {
}); });
it('TableDetailsPage Component should be visible, if currentTourPage is datasetPage', async () => { it('TableDetailsPage Component should be visible, if currentTourPage is datasetPage', async () => {
(useTourProvider as jest.Mock).mockImplementationOnce(() => ({ (useTourProvider as jest.Mock).mockReset();
(useTourProvider as jest.Mock).mockImplementation(() => ({
...mockUseTourProvider, ...mockUseTourProvider,
currentTourPage: CurrentTourPageType.DATASET_PAGE, currentTourPage: CurrentTourPageType.DATASET_PAGE,
})); }));

View File

@ -41,7 +41,7 @@ export const getTourSteps = ({
/> />
</p> </p>
), ),
selector: '#feedData', selector: '#feedWidgetData',
stepInteraction: false, stepInteraction: false,
}, },
{ {