test(ui): unit tests for users tab and user profile icon component (#15053)

* unit test case of Users tab

* unit test case of user profile icon component

* added test case of userProfileIcon

* code cleanup and add test case of user profile icon

* minor fix

* minor fix
This commit is contained in:
Harsh Vador 2024-02-07 16:56:57 +05:30 committed by GitHub
parent dcc91a8f37
commit d4ac43d0d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 413 additions and 23 deletions

View File

@ -26,6 +26,7 @@ import {
} from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { LIGHT_GREEN_COLOR } from '../../../constants/constants';
import { Transi18next } from '../../../utils/CommonUtils';
import { getRelativeTime } from '../../../utils/date-time/DateTimeUtils';
import { getEntityName } from '../../../utils/EntityUtils';
@ -50,7 +51,7 @@ const AppInstallVerifyCard = ({
<Space className="p-t-lg">
<AppLogo appName={appData?.fullyQualifiedName ?? ''} />
<Divider dashed className="w-44 app-card-divider">
<CheckCircleTwoTone twoToneColor="#4CAF50" />
<CheckCircleTwoTone twoToneColor={LIGHT_GREEN_COLOR} />
</Divider>
<Avatar
className="app-marketplace-avatar flex-center bg-white border"

View File

@ -17,6 +17,7 @@ import { AxiosError } from 'axios';
import { isEmpty, toString } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LIGHT_GREEN_COLOR } from '../../../constants/constants';
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
import { WidgetWidths } from '../../../enums/CustomizablePage.enum';
import { Document } from '../../../generated/entity/docStore/document';
@ -88,7 +89,7 @@ function AddWidgetModal({
<CheckOutlined
className="m-l-xs"
data-testid={`${widget.name}-check-icon`}
style={{ color: '#4CAF50' }}
style={{ color: LIGHT_GREEN_COLOR }}
/>
)}
</Space>

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { CheckOutlined } from '@ant-design/icons';
import { Dropdown, Tooltip, Typography } from 'antd';
import { Dropdown, Space, Tooltip, Typography } from 'antd';
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { isEmpty } from 'lodash';
import React, {
@ -27,6 +27,7 @@ import { ReactComponent as DropDownIcon } from '../../../assets/svg/DropDown.svg
import {
getTeamAndUserDetailsPath,
getUserPath,
LIGHT_GREEN_COLOR,
NO_DATA_PLACEHOLDER,
TERM_ADMIN,
TERM_USER,
@ -111,6 +112,8 @@ export const UserProfileIcon = () => {
useEffect(() => {
if (profilePicture) {
setIsImgUrlValid(true);
} else {
setIsImgUrlValid(false);
}
}, [profilePicture]);
@ -133,12 +136,19 @@ export const UserProfileIcon = () => {
const personaLabelRenderer = useCallback(
(item: EntityReference) => (
<span onClick={() => handleSelectedPersonaChange(item)}>
<Space
className="w-full"
data-testid="persona-label"
onClick={() => handleSelectedPersonaChange(item)}>
{getEntityName(item)}{' '}
{selectedPersona?.id === item.id && (
<CheckOutlined className="m-l-xs" style={{ color: '#4CAF50' }} />
<CheckOutlined
className="m-l-xs"
data-testid="check-outlined"
style={{ color: LIGHT_GREEN_COLOR }}
/>
)}
</span>
</Space>
),
[handleSelectedPersonaChange, selectedPersona]
);
@ -317,7 +327,8 @@ export const UserProfileIcon = () => {
{isImgUrlValid ? (
<img
alt="user"
className="app-bar-user-avatar"
className="app-bar-user-profile-pic"
data-testid="app-bar-user-profile-pic"
referrerPolicy="no-referrer"
src={profilePicture ?? ''}
onError={handleOnImageError}
@ -333,6 +344,7 @@ export const UserProfileIcon = () => {
</Tooltip>
<Typography.Text
className="text-grey-muted text-xs w-28"
data-testid="default-persona"
ellipsis={{ tooltip: true }}>
{isEmpty(selectedPersona)
? t('label.default')

View File

@ -0,0 +1,173 @@
/*
* 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 { act, fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { getImageWithResolutionAndFallback } from '../../../utils/ProfilerUtils';
import { useApplicationConfigContext } from '../../ApplicationConfigProvider/ApplicationConfigProvider';
import { useAuthContext } from '../../Auth/AuthProviders/AuthProvider';
import { mockPersonaData, mockUserData } from '../mocks/User.mocks';
import { UserProfileIcon } from './UserProfileIcon.component';
const mockLogout = jest.fn();
const mockUpdateSelectedPersona = jest.fn();
jest.mock('../../ApplicationConfigProvider/ApplicationConfigProvider', () => ({
useApplicationConfigContext: jest.fn().mockImplementation(() => ({
selectedPersona: {},
updateSelectedPersona: mockUpdateSelectedPersona,
})),
}));
jest.mock('../../../utils/EntityUtils', () => ({
getEntityName: jest.fn().mockReturnValue('Test User'),
}));
jest.mock('../../../utils/ProfilerUtils', () => ({
getImageWithResolutionAndFallback: jest
.fn()
.mockImplementation(() => 'valid-image-url'),
ImageQuality: jest.fn().mockReturnValue('6x'),
}));
jest.mock('../../common/AvatarComponent/Avatar', () =>
jest.fn().mockReturnValue(<div>Avatar</div>)
);
jest.mock('react-router-dom', () => ({
Link: jest
.fn()
.mockImplementation(({ children }: { children: React.ReactNode }) => (
<p data-testid="link">{children}</p>
)),
}));
jest.mock('../../Auth/AuthProviders/AuthProvider', () => ({
useAuthContext: jest.fn(() => ({
currentUser: mockUserData,
})),
onLogoutHandler: mockLogout,
}));
describe('UserProfileIcon', () => {
it('should render User Profile Icon', () => {
const { getByTestId } = render(<UserProfileIcon />);
expect(getByTestId('dropdown-profile')).toBeInTheDocument();
});
it('should display the user name', () => {
const { getByText } = render(<UserProfileIcon />);
expect(getByText('Test User')).toBeInTheDocument();
});
it('should display default in case of no persona is selected', () => {
const { getByText } = render(<UserProfileIcon />);
expect(getByText('label.default')).toBeInTheDocument();
});
it('should display image if profile pic is valid', () => {
const { getByTestId } = render(<UserProfileIcon />);
expect(getByTestId('app-bar-user-profile-pic')).toBeInTheDocument();
});
it('should not display profile pic if image url is invalid', () => {
(getImageWithResolutionAndFallback as jest.Mock).mockImplementation(
() => undefined
);
const { queryByTestId, getByText } = render(<UserProfileIcon />);
expect(queryByTestId('app-bar-user-profile-pic')).not.toBeInTheDocument();
expect(getByText('Avatar')).toBeInTheDocument();
});
it('should display the user team', () => {
(useApplicationConfigContext as jest.Mock).mockImplementation(() => ({
selectedPersona: {
id: '3362fe18-05ad-4457-9632-84f22887dda6',
type: 'team',
},
updateSelectedPersona: jest.fn(),
}));
const { getByTestId } = render(<UserProfileIcon />);
expect(getByTestId('default-persona')).toHaveTextContent('Test User');
});
it('should show empty placeholder when no teams data', async () => {
(useAuthContext as jest.Mock).mockImplementation(() => ({
currentUser: { ...mockUserData, teams: [] },
onLogoutHandler: mockLogout,
}));
const teamLabels = screen.queryAllByText('label.team-plural');
teamLabels.forEach((label) => {
expect(label).toHaveTextContent('--');
});
});
it('should show checked if selected persona is true', async () => {
(useAuthContext as jest.Mock).mockImplementation(() => ({
currentUser: {
...mockUserData,
personas: mockPersonaData,
},
onLogoutHandler: mockLogout,
}));
(useApplicationConfigContext as jest.Mock).mockImplementation(() => ({
selectedPersona: {
id: '0430976d-092a-46c9-90a8-61c6091a6f38',
type: 'persona',
},
updateSelectedPersona: jest.fn(),
}));
const { getByTestId } = render(<UserProfileIcon />);
await act(async () => {
userEvent.click(getByTestId('dropdown-profile'));
});
await act(async () => {
fireEvent.click(getByTestId('persona-label'));
});
expect(getByTestId('check-outlined')).toBeInTheDocument();
});
it('should not show checked if selected persona is true', async () => {
(useAuthContext as jest.Mock).mockImplementation(() => ({
currentUser: {
...mockUserData,
personas: mockPersonaData,
},
onLogoutHandler: mockLogout,
}));
(useApplicationConfigContext as jest.Mock).mockImplementation(() => ({
selectedPersona: {
id: 'test',
type: 'persona',
},
updateSelectedPersona: jest.fn(),
}));
const { getByTestId, queryByTestId } = render(<UserProfileIcon />);
await act(async () => {
userEvent.click(getByTestId('dropdown-profile'));
});
await act(async () => {
fireEvent.click(getByTestId('persona-label'));
});
expect(queryByTestId('check-outlined')).not.toBeInTheDocument();
});
});

View File

@ -0,0 +1,124 @@
/*
* 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 { act, fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { getUserById } from '../../../rest/userAPI';
import { mockUsersTabData } from '../mocks/User.mocks';
import { UsersTab } from './UsersTabs.component';
jest.mock('../../../rest/userAPI', () => ({
getUserById: jest
.fn()
.mockImplementation(() => Promise.resolve(mockUsersTabData)),
}));
jest.mock('../../common/PopOverCard/UserPopOverCard', () =>
jest.fn().mockReturnValue('Aaron Johnson')
);
jest.mock('react-router-dom', () => ({
Link: jest
.fn()
.mockImplementation(({ children }: { children: React.ReactNode }) => (
<p data-testid="link">{children}</p>
)),
useHistory: jest.fn(),
}));
const mockUsers = [
{
deleted: false,
displayName: 'Aaron Johnson',
fullyQualifiedName: 'aaron_johnson0',
href: 'http://localhost:8585/api/v1/users/f281e7fd-5fd3-4279-8a2d-ade80febd743',
id: 'f281e7fd-5fd3-4279-8a2d-ade80febd743',
name: 'aaron_johnson0',
type: 'user',
},
];
const mockOnRemoveUser = jest.fn();
describe('UsersTab', () => {
it('should renders Users Tab', async () => {
await act(async () => {
render(<UsersTab users={mockUsers} onRemoveUser={mockOnRemoveUser} />, {
wrapper: MemoryRouter,
});
});
expect(await screen.findByText('label.username')).toBeInTheDocument();
expect(await screen.findByText('label.team-plural')).toBeInTheDocument();
expect(await screen.findByText('label.role-plural')).toBeInTheDocument();
expect(await screen.findByText('label.action-plural')).toBeInTheDocument();
});
it('should display the user details', async () => {
await act(async () => {
render(<UsersTab users={mockUsers} onRemoveUser={mockOnRemoveUser} />, {
wrapper: MemoryRouter,
});
});
expect(await screen.findByText('Aaron Johnson')).toBeInTheDocument();
expect(await screen.findByText('Sales')).toBeInTheDocument();
expect(await screen.findByText('Data Steward')).toBeInTheDocument();
});
it('should render empty placeholder if no data', async () => {
(getUserById as jest.Mock).mockImplementation(() => Promise.resolve([])),
await act(async () => {
render(<UsersTab users={[]} onRemoveUser={mockOnRemoveUser} />, {
wrapper: MemoryRouter,
});
});
expect(
await screen.findByTestId('assign-error-placeholder-label.user')
).toBeInTheDocument();
});
it('should display the remove confirmation modal when remove button is clicked', async () => {
await act(async () => {
render(<UsersTab users={mockUsers} onRemoveUser={mockOnRemoveUser} />, {
wrapper: MemoryRouter,
});
});
await act(async () => {
fireEvent.click(screen.getByTestId('remove-user-btn'));
});
expect(
await screen.getByTestId('remove-confirmation-modal')
).toBeInTheDocument();
});
it('should close the remove confirmation modal when cancel button is clicked', async () => {
await act(async () => {
render(<UsersTab users={mockUsers} onRemoveUser={mockOnRemoveUser} />, {
wrapper: MemoryRouter,
});
});
await act(async () => {
userEvent.click(screen.getByTestId('remove-user-btn'));
});
await act(async () => {
userEvent.click(screen.getByText('label.cancel'));
});
expect(
screen.queryByTestId('remove-confirmation-modal')
).not.toBeInTheDocument();
});
});

View File

@ -134,20 +134,22 @@ export const UsersTab = ({ users, onRemoveUser }: UsersTabProps) => {
rowKey="fullyQualifiedName"
size="small"
/>
<Modal
cancelText={t('label.cancel')}
data-testid="remove-confirmation-modal"
okText={t('label.confirm')}
open={Boolean(removeUserDetails?.state)}
title={t('label.removing-user')}
onCancel={handleRemoveCancel}
onOk={handleRemoveConfirm}>
{t('message.are-you-sure-want-to-text', {
text: t('label.remove-entity-lowercase', {
entity: removeUserDetails?.user.name,
}),
})}
</Modal>
{Boolean(removeUserDetails?.state) && (
<Modal
cancelText={t('label.cancel')}
data-testid="remove-confirmation-modal"
okText={t('label.confirm')}
open={Boolean(removeUserDetails?.state)}
title={t('label.removing-user')}
onCancel={handleRemoveCancel}
onOk={handleRemoveConfirm}>
{t('message.are-you-sure-want-to-text', {
text: t('label.remove-entity-lowercase', {
entity: removeUserDetails?.user.name,
}),
})}
</Modal>
)}
</>
);
};

View File

@ -208,6 +208,71 @@ export const mockTeamsData = {
],
paging: { total: 7 },
};
export const mockUsersTabData = {
id: 'f281e7fd-5fd3-4279-8a2d-ade80febd743',
name: 'aaron_johnson0',
fullyQualifiedName: 'aaron_johnson0',
displayName: 'Aaron Johnson',
version: 1.5,
updatedAt: 1707198736848,
updatedBy: 'admin',
email: 'aaron_johnson0@gmail.com',
href: 'http://localhost:8585/api/v1/users/f281e7fd-5fd3-4279-8a2d-ade80febd743',
isAdmin: false,
teams: [
{
id: '35c03c5c-5160-41af-b08d-ef2b2b9e6adf',
type: 'team',
name: 'Sales',
fullyQualifiedName: 'Sales',
deleted: false,
href: 'http://localhost:8585/api/v1/teams/35c03c5c-5160-41af-b08d-ef2b2b9e6adf',
},
],
changeDescription: {
fieldsAdded: [
{
name: 'teams',
newValue:
'[{"id":"35c03c5c-5160-41af-b08d-ef2b2b9e6adf","type":"team","name":"Sales","fullyQualifiedName":"Sales","deleted":false}]',
},
],
fieldsUpdated: [],
fieldsDeleted: [
{
name: 'teams',
oldValue: '[{"id":"f39f326a-81a1-42ca-976]',
},
],
previousVersion: 1.4,
},
deleted: false,
roles: [
{
id: 'e4b20aef-c6c4-4416-aaae-f60185c7cac0',
type: 'role',
name: 'DataSteward',
fullyQualifiedName: 'DataSteward',
description: 'Users with Data Steward',
displayName: 'Data Steward',
deleted: false,
href: 'http://localhost:8585/api/v1/roles/e4b20aef-c6c4-4416-aaae-f60185c7cac0',
},
],
inheritedRoles: [
{
id: '5f1445a7-c299-4dde-8c5b-704c6cd68ee6',
type: 'role',
name: 'DataConsumer',
fullyQualifiedName: 'DataConsumer',
description:
'Users with Data Consumer role use different data assets for their day to day work.',
displayName: 'Data Consumer',
deleted: false,
href: 'http://localhost:8585/api/v1/roles/5f1445a7-c299-4dde-8c5b-704c6cd68ee6',
},
],
};
export const mockUserRole = {
data: [
@ -1039,3 +1104,14 @@ export const mockAccessData = {
tokenType: TokenType.PersonalAccessToken,
userId: '445291f4d62c1bae',
};
export const mockPersonaData = [
{
description: 'Person-04',
displayName: 'Person-04',
fullyQualifiedName: 'Person-04',
href: 'http://localhost:8585/api/v1/personas/0430976d-092a-46c9-90a8-61c6091a6f38',
id: '0430976d-092a-46c9-90a8-61c6091a6f38',
name: 'Person-04',
type: 'persona',
},
];

View File

@ -136,7 +136,7 @@ describe('FeedsWidget', () => {
const { getAllByRole } = render(<FeedsWidget {...widgetProps} />);
const tabs = getAllByRole('tab');
const selectedTab = tabs[index];
selectedTab.click();
fireEvent.click(selectedTab);
expect(selectedTab.getAttribute('aria-selected')).toBe('true');
});

View File

@ -42,6 +42,7 @@ export const GREEN_COLOR_OPACITY_30 = '#28A74530';
export const BORDER_COLOR = '#0000001a';
export const BLACK_COLOR = '#000000';
export const WHITE_COLOR = '#ffffff';
export const LIGHT_GREEN_COLOR = '#4CAF50';
export const DEFAULT_CHART_OPACITY = 1;
export const HOVER_CHART_OPACITY = 0.3;

View File

@ -700,7 +700,7 @@ a[href].link-text-grey,
padding-right: 0.25rem;
}
.app-bar-user-avatar {
.app-bar-user-profile-pic {
display: inline-block;
height: 36px;
width: 36px;