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: add tests for user components #1374

Merged
merged 11 commits into from
Apr 11, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { createMockRouter } from '@/utils/testing/mocks';
import { renderWithProviders } from '@/utils/testing/renderWithProviders';
import { UserFactory } from '@/utils/factories/user';
import { fireEvent, waitFor } from '@testing-library/react';
import TeamsDialog, { TeamsDialogProps } from '@/components/Users/User/TeamsDialog/TeamsDialog';
import { TeamCheckedFactory } from '@/utils/factories/team';
import React from 'react';
import useUpdateUserTeams from '@/hooks/teams/useUpdateUserTeams';
import { UseMutationResult } from '@tanstack/react-query';
import { TeamUserRoles } from '@/utils/enums/team.user.roles';

const user = UserFactory.create();
const router = createMockRouter({ query: { userId: user._id } });

const render = (props: Partial<TeamsDialogProps> = {}) =>
renderWithProviders(
<TeamsDialog
teamsList={TeamCheckedFactory.createMany(3)}
setIsOpen={jest.fn()}
isOpen
confirmationLabel="confirm"
title="Title"
joinedAt="2022-01-01T00:00:00+00:00"
{...props}
/>,
{ routerOptions: router },
);

const mockUseUpdateUserTeams = useUpdateUserTeams as jest.Mock<Partial<UseMutationResult>>;

jest.mock('@/hooks/teams/useUpdateUserTeams');

describe('Components/Users/User/TeamsDialog', () => {
beforeEach(() => {
mockUseUpdateUserTeams.mockReturnValue({
mutate: jest.fn(),
} as Partial<UseMutationResult>);
});

it('should render correctly', () => {
// Arrange
const teamsList = TeamCheckedFactory.createMany(3);

// Act
const { getAllByTestId, getByTestId } = render({ teamsList });

// Assert
expect(getAllByTestId('checkboxTeamItem')).toHaveLength(teamsList.length);
expect(getByTestId('searchInput')).toBeInTheDocument();
});

it('should call useUpdateUserTeams mutate function', async () => {
// Arrange
const updateUserTeamsMutation = jest.fn();

mockUseUpdateUserTeams.mockReturnValueOnce({
mutate: updateUserTeamsMutation,
} as Partial<UseMutationResult>);

const teamsList = TeamCheckedFactory.createMany(3, [
{ isChecked: false },
{ isChecked: true },
{ isChecked: false },
]);

const mutatedTeamList = [
{
user: user._id,
role: TeamUserRoles.MEMBER,
team: teamsList[1]._id,
isNewJoiner: false,
canBeResponsible: true,
},
];

// Act
const { getByTestId } = render({ teamsList });

fireEvent.click(getByTestId('dialogFooterSubmit'));

// Assert
await waitFor(() => {
expect(updateUserTeamsMutation).toBeCalled();
expect(updateUserTeamsMutation).toBeCalledWith(mutatedTeamList);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SearchInput from '@/components/Primitives/Inputs/SearchInput/SearchInput'
import Separator from '@/components/Primitives/Separator/Separator';
import useUpdateUserTeams from '@/hooks/teams/useUpdateUserTeams';

type TeamsDialogProps = {
export type TeamsDialogProps = {
teamsList: TeamChecked[];
setIsOpen: Dispatch<SetStateAction<boolean>>;
isOpen: boolean;
Expand Down Expand Up @@ -125,7 +125,7 @@ const TeamsDialog = ({
>
<Flex css={{ px: '$32' }} direction="column" gap={20}>
{filteredTeams?.map((team) => (
<Flex key={team._id} align="center">
<Flex key={team._id} align="center" data-testid="checkboxTeamItem">
<Flex css={{ flex: 1 }}>
<Checkbox
checked={team.isChecked}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fireEvent, waitFor } from '@testing-library/react';
import { renderWithProviders } from '@/utils/testing/renderWithProviders';
import { USERS_ROUTE } from '@/utils/routes';
import { UserFactory } from '@/utils/factories/user';
import UserHeader, { UserHeaderProps } from '@/components/Users/User/Header/Header';
import UserHeader, { UserHeaderProps } from '@/components/Users/User/UserHeader/UserHeader';

const { mockRouter } = libraryMocks.mockNextRouter({ pathname: '/users' });
const render = (props: Partial<UserHeaderProps> = {}) =>
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/components/Users/UsersList/UserItem/UserItem.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { renderWithProviders } from '@/utils/testing/renderWithProviders';
import { UserFactory, UserWithTeamsFactory } from '@/utils/factories/user';
import UserItem, {
UserItemProps,
getTeamsCountText,
} from '@/components/Users/UsersList/UserItem/UserItem';

const render = (props: Partial<UserItemProps> = {}) =>
renderWithProviders(<UserItem userWithTeams={UserWithTeamsFactory.create()} {...props} />, {
sessionOptions: { user: UserFactory.create({ isSAdmin: true }) },
});

describe('Components/Users/User/UsersList/UserItem/UserItem', () => {
it('should render correctly', () => {
const userWithTeams = UserWithTeamsFactory.create();

// Act
const { getByTestId, getByText } = render({ userWithTeams });

// Assert
expect(getByTestId('userTitle')).toBeInTheDocument();
expect(getByText(userWithTeams.user.email)).toBeInTheDocument();
expect(getByTestId('userItemActions')).toBeInTheDocument();

if (userWithTeams.teamsNames)
expect(getByText(getTeamsCountText(userWithTeams.teamsNames))).toBeInTheDocument();
});
});
49 changes: 23 additions & 26 deletions frontend/src/components/Users/UsersList/UserItem/UserItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,34 @@ import Icon from '@/components/Primitives/Icons/Icon/Icon';
import Flex from '@/components/Primitives/Layout/Flex/Flex';
import Text from '@/components/Primitives/Text/Text';
import Tooltip from '@/components/Primitives/Tooltips/Tooltip/Tooltip';
import UserItemActions from '@/components/Users/UsersList/UserItem/UserItemActions/UserItemActions';
import UserTitle from '@/components/Users/UsersList/UserItem/UserTitle/UserTitle';
import useCurrentSession from '@/hooks/useCurrentSession';
import { InnerContainer } from '@/styles/pages/pages.styles';
import { UserWithTeams } from '@/types/user/user';
import UserTitle from '@/components/Users/UsersList/UserItem/UserTitle/UserTitle';
import UserItemActions from '@/components/Users/UsersList/UserItem/UserItemActions/UserItemActions';

type UserItemProps = {
export type UserItemProps = {
userWithTeams: UserWithTeams;
};

export const getTeamsCountText = (teamNames: string[]) => {
if (teamNames.length === 1) {
return 'in 1 team';
}
if (teamNames && teamNames.length > 1) {
return `in ${teamNames.length} teams`;
}
return 'no teams';
};

const UserItem = React.memo<UserItemProps>(({ userWithTeams }) => {
const { isSAdmin } = useCurrentSession();
const { teamsNames, user } = userWithTeams;

const getTeamsCountText = () => {
if (teamsNames?.length === 1) {
return 'in 1 team';
}
if (teamsNames && teamsNames?.length > 1) {
return `in ${teamsNames.length} teams`;
}
return 'no teams';
};

const teamsSeparatedByComma = teamsNames?.join(', ') || '';

return (
<Flex direction="column">
<Flex direction="column" data-testid="userItem">
<InnerContainer align="center" elevation="1" gap="40">
<Flex align="center" gap="8" css={{ flex: '2' }}>
<Icon
Expand All @@ -43,32 +43,29 @@ const UserItem = React.memo<UserItemProps>(({ userWithTeams }) => {
flexShrink: '0',
}}
/>

<UserTitle user={user} hasPermissions={isSAdmin!} />
</Flex>

<Flex align="center" justify="start" css={{ flex: '2' }}>
<Text color="primary300" size="sm">
{user.email}
</Text>
</Flex>

<Flex align="center" css={{ flex: '1' }}>
{!isSAdmin && user.isSAdmin && (
<Badge pill variant="success" size="sm">
SUPER ADMIN
</Badge>
)}
</Flex>

<Flex align="center" justify="end" css={{ flex: '1' }}>
<Tooltip content={teamsSeparatedByComma}>
<Text css={{ cursor: 'default' }} fontWeight="bold" size="sm">
{getTeamsCountText()}
</Text>
</Tooltip>
</Flex>

{teamsNames && (
<Flex align="center" justify="end" css={{ flex: '1' }}>
<Tooltip content={teamsSeparatedByComma}>
<Text css={{ cursor: 'default' }} fontWeight="bold" size="sm">
{getTeamsCountText(teamsNames)}
</Text>
</Tooltip>
</Flex>
)}
{isSAdmin && (
<Flex align="center" justify="end" css={{ flex: '2' }}>
<UserItemActions user={user} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from 'react';
import { libraryMocks } from '@/utils/testing/mocks';
import { fireEvent, waitFor } from '@testing-library/react';
import { renderWithProviders } from '@/utils/testing/renderWithProviders';
import { UserFactory } from '@/utils/factories/user';
import UserItemActions, {
UserItemActionsProps,
} from '@/components/Users/UsersList/UserItem/UserItemActions/UserItemActions';
import { ROUTES } from '@/utils/routes';
import useUpdateUser from '@/hooks/users/useUpdateUser';
import { UseMutationResult } from '@tanstack/react-query';
import useDeleteUser from '@/hooks/users/useDeleteUser';

const { mockRouter } = libraryMocks.mockNextRouter({ pathname: '/users' });

const render = (props: Partial<UserItemActionsProps> = {}) =>
renderWithProviders(<UserItemActions user={UserFactory.create()} {...props} />, {
routerOptions: mockRouter,
});

const mockUseUpdateUser = useUpdateUser as jest.Mock<Partial<UseMutationResult>>;
const mockUseDeleteUser = useDeleteUser as jest.Mock<Partial<UseMutationResult>>;

jest.mock('@/hooks/users/useUpdateUser');
jest.mock('@/hooks/users/useDeleteUser');

describe('Components/Users/User/UsersList/UserItem/UserItemActions', () => {
beforeEach(() => {
mockUseUpdateUser.mockReturnValue({
mutateAsync: jest.fn(),
} as Partial<UseMutationResult>);

mockUseDeleteUser.mockReturnValue({
mutate: jest.fn(),
} as Partial<UseMutationResult>);
});

it('should render correctly', () => {
// Act
const { getByTestId, getAllByRole } = render();

// Assert
expect(getByTestId('configurationSwitch')).toBeInTheDocument();
expect(getAllByRole('button')).toHaveLength(2);
});

it('should redirect to user page', async () => {
// Arrange
const user = UserFactory.create();

// Act
const { getAllByRole } = render({ user });

const userDetailsBtn = getAllByRole('button')[0];

expect(userDetailsBtn.querySelector('svg > use')).toHaveAttribute('href', '#edit');

fireEvent.click(userDetailsBtn);

// Assert
await waitFor(() => {
expect(mockRouter.push).toHaveBeenCalledWith(
ROUTES.UserPage(user._id),
ROUTES.UserPage(user._id),
expect.anything(),
);
});
});

it('should handle super admin status change', async () => {
// Arrange
const user = UserFactory.create();

const updateUserMutation = jest.fn();

mockUseUpdateUser.mockReturnValueOnce({
mutate: updateUserMutation,
} as Partial<UseMutationResult>);

// Act
const { getByRole } = render({ user });

// Assert
fireEvent.click(getByRole('switch'));

await waitFor(() => {
expect(updateUserMutation).toBeCalledWith({ _id: user._id, isSAdmin: !user.isSAdmin });
});
});

it('should handle delete user action', async () => {
// Arrange
const user = UserFactory.create();

const deleteUserMutation = jest.fn();

mockUseDeleteUser.mockReturnValueOnce({
mutate: deleteUserMutation,
} as Partial<UseMutationResult>);

// Act
const { getAllByRole, getByRole, getByText } = render({ user });

const deleteUserBtn = getAllByRole('button')[1];

// Assert
expect(deleteUserBtn.querySelector('svg > use')).toHaveAttribute('href', '#trash-alt');

fireEvent.click(deleteUserBtn);

await waitFor(() => {
expect(getByRole('alertdialog')).toBeInTheDocument();
});

fireEvent.click(getByText('Delete'));

await waitFor(() => {
expect(deleteUserMutation).toBeCalledWith({ id: user._id });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ROUTES } from '@/utils/routes';
import useUpdateUser from '@/hooks/users/useUpdateUser';
import useDeleteUser from '@/hooks/users/useDeleteUser';

type UserItemActionsProps = {
export type UserItemActionsProps = {
user: User;
};

Expand Down Expand Up @@ -49,7 +49,7 @@ const UserItemActions = React.memo(({ user }: UserItemActionsProps) => {
};

return (
<Flex justify="end">
<Flex justify="end" data-testid="userItemActions">
<Flex css={{ alignItems: 'center' }} gap={24}>
<ConfigurationSwitch
handleCheckedChange={handleSuperAdminChange}
Expand Down
Loading