Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Feb 7, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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,
Expand Down Expand Up @@ -111,6 +112,8 @@ export const UserProfileIcon = () => {
useEffect(() => {
if (profilePicture) {
setIsImgUrlValid(true);
} else {
setIsImgUrlValid(false);
}
}, [profilePicture]);

Expand All @@ -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]
);
Expand Down Expand Up @@ -317,7 +327,8 @@ export const UserProfileIcon = () => {
{isImgUrlValid ? (
<img
alt="user"
className="app-bar-user-avatar"
className="app-bar-user-profile-pic"
harsh-vador marked this conversation as resolved.
Show resolved Hide resolved
data-testid="app-bar-user-profile-pic"
referrerPolicy="no-referrer"
src={profilePicture ?? ''}
onError={handleOnImageError}
Expand All @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading
Loading