fix reloading of user profile page on edit action perform (#15673)

* fix reloading of user profile page on data change

* supported unit test update handler in UserPage
This commit is contained in:
Ashish Gupta 2024-03-27 11:52:00 +05:30 committed by GitHub
parent 8073a80989
commit 024e19f97b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 321 additions and 140 deletions

View File

@ -74,9 +74,14 @@ jest.mock(
jest.mock(
'../../MyData/Persona/PersonaSelectableList/PersonaSelectableList.component',
() => ({
PersonaSelectableList: jest
.fn()
.mockReturnValue(<p>PersonaSelectableList</p>),
PersonaSelectableList: jest.fn().mockImplementation(({ onUpdate }) => (
<div>
<p>PersonaSelectableList</p>
<button onClick={() => onUpdate([])}>
SavePersonaSelectableListButton
</button>
</div>
)),
})
);
@ -133,16 +138,22 @@ jest.mock('../../PageLayoutV1/PageLayoutV1', () =>
);
jest.mock('../../common/EntityDescription/DescriptionV1', () => {
return jest.fn().mockReturnValue(<p>Description</p>);
return jest.fn().mockImplementation(({ onDescriptionUpdate }) => (
<div>
<span>Description</span>
<button onClick={() => onDescriptionUpdate('testDescription')}>
SaveDescriptionButton
</button>
</div>
));
});
const updateUserDetails = jest.fn();
const mockProp = {
queryFilters: {
myData: 'my-data',
following: 'following',
},
updateUserDetails,
updateUserDetails: jest.fn(),
handlePaginate: jest.fn(),
};
@ -190,14 +201,69 @@ describe('Test User Component', () => {
'UserProfileInheritedRoles'
);
const UserProfileRoles = await findByText(container, 'UserProfileRoles');
const UserProfileTeams = await findByText(container, 'UserProfileTeams');
const description = await findByText(container, 'Description');
expect(description).toBeInTheDocument();
expect(UserProfileRoles).toBeInTheDocument();
expect(UserProfileTeams).toBeInTheDocument();
expect(UserProfileInheritedRoles).toBeInTheDocument();
});
it('should call updateUserDetails on click of SaveDescriptionButton', async () => {
const { container } = render(
<Users userData={mockUserData} {...mockProp} />,
{
wrapper: MemoryRouter,
}
);
const collapsibleButton = await findByRole(container, 'img');
userEvent.click(collapsibleButton);
const saveDescriptionButton = await findByText(
container,
'SaveDescriptionButton'
);
userEvent.click(saveDescriptionButton);
expect(mockProp.updateUserDetails).toHaveBeenCalledWith(
{
description: 'testDescription',
},
'description'
);
});
it('should call updateUserDetails on click of SavePersonaSelectableListButton', async () => {
const { container } = render(
<Users userData={mockUserData} {...mockProp} />,
{
wrapper: MemoryRouter,
}
);
const collapsibleButton = await findByRole(container, 'img');
userEvent.click(collapsibleButton);
const savePersonaSelectableListButton = await findByText(
container,
'SavePersonaSelectableListButton'
);
userEvent.click(savePersonaSelectableListButton);
expect(mockProp.updateUserDetails).toHaveBeenCalledWith(
{
personas: [],
},
'personas'
);
});
it('Tab should not visible to normal user', async () => {
const { container } = render(
<Users userData={mockUserData} {...mockProp} />,

View File

@ -115,9 +115,9 @@ const Users = ({ userData, queryFilters, updateUserDetails }: Props) => {
const handlePersonaUpdate = useCallback(
async (personas: EntityReference[]) => {
await updateUserDetails({ ...userData, personas });
await updateUserDetails({ personas }, 'personas');
},
[updateUserDetails, userData]
[updateUserDetails]
);
const tabDataRender = useCallback(
@ -232,7 +232,7 @@ const Users = ({ userData, queryFilters, updateUserDetails }: Props) => {
const handleDescriptionChange = useCallback(
async (description: string) => {
await updateUserDetails({ description });
await updateUserDetails({ description }, 'description');
setIsDescriptionEdit(false);
},

View File

@ -21,7 +21,7 @@ export interface Props {
following: string;
};
handlePaginate: (page: string | number) => void;
updateUserDetails: (data: Partial<User>) => Promise<void>;
updateUserDetails: (data: Partial<User>, key: keyof User) => Promise<void>;
authenticationMechanism?: PersonalAccessToken;
}

View File

@ -105,7 +105,10 @@ const UserProfileDetails = ({
const handleDisplayNameSave = useCallback(async () => {
if (displayName !== userData.displayName) {
setIsLoading(true);
await updateUserDetails({ displayName: displayName ?? '' });
await updateUserDetails(
{ displayName: displayName ?? '' },
'displayName'
);
setIsLoading(false);
}
setIsDisplayNameEdit(false);
@ -260,9 +263,9 @@ const UserProfileDetails = ({
const handleDefaultPersonaUpdate = useCallback(
async (defaultPersona?: EntityReference) => {
await updateUserDetails({ ...userData, defaultPersona });
await updateUserDetails({ defaultPersona }, 'defaultPersona');
},
[updateUserDetails, userData]
[updateUserDetails]
);
const defaultPersonaRender = useMemo(

View File

@ -15,5 +15,5 @@ import { User } from '../../../../../generated/entity/teams/user';
export interface UserProfileDetailsProps {
userData: User;
updateUserDetails: (data: Partial<User>) => Promise<void>;
updateUserDetails: (data: Partial<User>, key: keyof User) => Promise<void>;
}

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { AuthProvider } from '../../../../../generated/settings/settings';
@ -63,7 +63,15 @@ jest.mock('../UserProfileImage/UserProfileImage.component', () => {
});
jest.mock('../../../../common/InlineEdit/InlineEdit.component', () => {
return jest.fn().mockReturnValue(<p>InlineEdit</p>);
return jest.fn().mockImplementation(({ onSave, children }) => (
<div data-testid="inline-edit">
<span>InlineEdit</span>
{children}
<button data-testid="display-name-save-button" onClick={onSave}>
DisplayNameButton
</button>
</div>
));
});
jest.mock('../../ChangePasswordForm', () => {
@ -73,9 +81,16 @@ jest.mock('../../ChangePasswordForm', () => {
jest.mock(
'../../../../MyData/Persona/PersonaSelectableList/PersonaSelectableList.component',
() => ({
PersonaSelectableList: jest
.fn()
.mockReturnValue(<p>PersonaSelectableList</p>),
PersonaSelectableList: jest.fn().mockImplementation(({ onUpdate }) => (
<div>
<span>PersonaSelectableList</span>
<button
data-testid="persona-save-button"
onClick={() => onUpdate(USER_DATA.defaultPersona)}>
PersonaSaveButton
</button>
</div>
)),
})
);
@ -208,4 +223,44 @@ describe('Test User Profile Details Component', () => {
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
});
it('should call updateUserDetails on click of DisplayNameButton', async () => {
render(<UserProfileDetails {...mockPropsData} />, {
wrapper: MemoryRouter,
});
act(() => {
fireEvent.click(screen.getByTestId('edit-displayName'));
});
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
act(() => {
fireEvent.change(screen.getByTestId('displayName'), {
target: { value: 'test' },
});
});
act(() => {
fireEvent.click(screen.getByTestId('display-name-save-button'));
});
expect(mockPropsData.updateUserDetails).toHaveBeenCalledWith(
{ displayName: 'test' },
'displayName'
);
});
it('should call updateUserDetails on click of PersonaSaveButton', async () => {
render(<UserProfileDetails {...mockPropsData} />, {
wrapper: MemoryRouter,
});
fireEvent.click(screen.getByTestId('persona-save-button'));
expect(mockPropsData.updateUserDetails).toHaveBeenCalledWith(
{ defaultPersona: USER_DATA.defaultPersona },
'defaultPersona'
);
});
});

View File

@ -98,14 +98,17 @@ const UserProfileRoles = ({
const isAdmin = selectedRoles.find(
(roleId) => roleId === toLower(TERM_ADMIN)
);
await updateUserDetails({
roles: updatedRoles.map((roleId) => {
const role = roles.find((r) => r.id === roleId);
await updateUserDetails(
{
roles: updatedRoles.map((roleId) => {
const role = roles.find((r) => r.id === roleId);
return { id: roleId, type: 'role', name: role?.name ?? '' };
}),
isAdmin: Boolean(isAdmin),
});
return { id: roleId, type: 'role', name: role?.name ?? '' };
}),
isAdmin: Boolean(isAdmin),
},
'roles'
);
setIsLoading(false);
setIsRolesEdit(false);
};

View File

@ -15,5 +15,5 @@ import { User } from '../../../../../generated/entity/teams/user';
export interface UserProfileRolesProps {
isUserAdmin?: boolean;
userRoles: User['roles'];
updateUserDetails: (data: Partial<User>) => Promise<void>;
updateUserDetails: (data: Partial<User>, key: keyof User) => Promise<void>;
}

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { useAuth } from '../../../../../hooks/authHooks';
import { getRoles } from '../../../../../rest/rolesAPIV1';
@ -29,7 +29,14 @@ jest.mock('../../../../../hooks/authHooks', () => ({
}));
jest.mock('../../../../common/InlineEdit/InlineEdit.component', () => {
return jest.fn().mockReturnValue(<p>InlineEdit</p>);
return jest.fn().mockImplementation(({ onSave }) => (
<div data-testid="inline-edit">
<span>InlineEdit</span>
<button data-testid="save" onClick={onSave}>
save
</button>
</div>
));
});
jest.mock('../../../../common/Chip/Chip.component', () => {
@ -101,6 +108,26 @@ describe('Test User Profile Roles Component', () => {
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
});
it('should call updateUserDetails on click save', async () => {
(useAuth as jest.Mock).mockImplementation(() => ({
isAdminUser: true,
}));
render(<UserProfileRoles {...mockPropsData} />);
fireEvent.click(screen.getByTestId('edit-roles-button'));
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
act(() => {
fireEvent.click(screen.getByTestId('save'));
});
expect(mockPropsData.updateUserDetails).toHaveBeenCalledWith(
{ roles: [], isAdmin: false },
'roles'
);
});
it('should call roles api on edit button action', async () => {
(useAuth as jest.Mock).mockImplementation(() => ({
isAdminUser: true,

View File

@ -40,9 +40,12 @@ const UserProfileTeams = ({
const handleTeamsSave = async () => {
setIsLoading(true);
await updateUserDetails({
teams: selectedTeams.map((teamId) => ({ id: teamId.id, type: 'team' })),
});
await updateUserDetails(
{
teams: selectedTeams.map((teamId) => ({ id: teamId.id, type: 'team' })),
},
'teams'
);
setIsLoading(false);
setIsTeamsEdit(false);

View File

@ -14,5 +14,5 @@ import { User } from '../../../../../generated/entity/teams/user';
export interface UserProfileTeamsProps {
teams: User['teams'];
updateUserDetails: (data: Partial<User>) => Promise<void>;
updateUserDetails: (data: Partial<User>, key: keyof User) => Promise<void>;
}

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { useAuth } from '../../../../../hooks/authHooks';
import { USER_DATA, USER_TEAMS } from '../../../../../mocks/User.mock';
@ -28,11 +28,18 @@ jest.mock('../../../../../hooks/authHooks', () => ({
}));
jest.mock('../../../../../utils/CommonUtils', () => ({
getNonDeletedTeams: jest.fn(),
getNonDeletedTeams: jest.fn().mockReturnValue([]),
}));
jest.mock('../../../../common/InlineEdit/InlineEdit.component', () => {
return jest.fn().mockReturnValue(<p>InlineEdit</p>);
return jest.fn().mockImplementation(({ onSave }) => (
<div data-testid="inline-edit">
<span>InlineEdit</span>
<button data-testid="save" onClick={onSave}>
save
</button>
</div>
));
});
jest.mock('../../../../common/Chip/Chip.component', () => {
@ -74,6 +81,25 @@ describe('Test User Profile Teams Component', () => {
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
});
it('should call updateUserDetails on click save', async () => {
render(<UserProfileTeams {...mockPropsData} teams={USER_DATA.teams} />);
const editButton = screen.getByTestId('edit-teams-button');
fireEvent.click(editButton);
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
act(() => {
fireEvent.click(screen.getByTestId('save'));
});
expect(mockPropsData.updateUserDetails).toHaveBeenCalledWith(
{ teams: [] },
'teams'
);
});
it('should not render edit button to non admin user', async () => {
(useAuth as jest.Mock).mockImplementation(() => ({
isAdminUser: false,

View File

@ -180,7 +180,7 @@ const PermissionProvider: FC<PermissionProviderProps> = ({ children }) => {
if (isEmpty(currentUser)) {
resetPermissions();
}
}, [currentUser]);
}, [currentUser?.teams, currentUser?.roles]);
const contextValues = useMemo(
() => ({

View File

@ -14,7 +14,7 @@
import { Typography } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEmpty } from 'lodash';
import { isEmpty, isUndefined } from 'lodash';
import Qs from 'qs';
import {
default as React,
@ -104,17 +104,30 @@ const UserPage = () => {
};
const updateUserDetails = useCallback(
async (data: Partial<User>) => {
async (data: Partial<User>, key: keyof User) => {
const updatedDetails = { ...userData, ...data };
const jsonPatch = compare(userData, updatedDetails);
try {
const response = await updateUserDetail(userData.id, jsonPatch);
if (response) {
if (userData.id === currentUser?.id) {
updateCurrentUser(response);
const newCurrentUserData = {
...currentUser,
[key]: response[key],
};
const newUserData = { ...userData, [key]: response[key] };
if (key === 'defaultPersona') {
if (isUndefined(response.defaultPersona)) {
// remove key from object if value is undefined
delete newCurrentUserData[key];
delete newUserData[key];
}
}
setUserData((prev) => ({ ...prev, ...response }));
if (userData.id === currentUser?.id) {
updateCurrentUser(newCurrentUserData as User);
}
setUserData(newUserData);
} else {
throw t('message.unexpected-error');
}

View File

@ -11,104 +11,32 @@
* limitations under the License.
*/
import { findByTestId, findByText, render } from '@testing-library/react';
import {
act,
findByTestId,
findByText,
fireEvent,
render,
screen,
} from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { EntityType } from '../../enums/entity.enum';
import { getUserByName } from '../../rest/userAPI';
import { useApplicationStore } from '../../hooks/useApplicationStore';
import { USER_DATA } from '../../mocks/User.mock';
import { getUserByName, updateUserDetail } from '../../rest/userAPI';
import UserPage from './UserPage.component';
const mockUserData = {
id: 'd6764107-e8b4-4748-b256-c86fecc66064',
name: 'xyz',
displayName: 'XYZ',
version: 0.1,
updatedAt: 1648704499857,
updatedBy: 'xyz',
email: 'xyz@gmail.com',
href: 'http://localhost:8585/api/v1/users/d6764107-e8b4-4748-b256-c86fecc66064',
isAdmin: false,
domain: {
id: '303ca53b-5050-4caa-9c4e-d4fdada76a53',
type: EntityType.DOMAIN,
name: 'Engineering',
fullyQualifiedName: 'Engineering',
description: 'description',
inherited: true,
href: 'http://localhost:8585/api/v1/domains/303ca53b-5050-4caa-9c4e-d4fdada76a53',
},
profile: {
images: {
image:
'https://lh3.googleusercontent.com/a-/AOh14Gh8NPux8jEPIuyPWOxAB1od9fGN188Kcp5HeXgc=s96-c',
image24:
'https://lh3.googleusercontent.com/a-/AOh14Gh8NPux8jEPIuyPWOxAB1od9fGN188Kcp5HeXgc=s24-c',
image32:
'https://lh3.googleusercontent.com/a-/AOh14Gh8NPux8jEPIuyPWOxAB1od9fGN188Kcp5HeXgc=s32-c',
image48:
'https://lh3.googleusercontent.com/a-/AOh14Gh8NPux8jEPIuyPWOxAB1od9fGN188Kcp5HeXgc=s48-c',
image72:
'https://lh3.googleusercontent.com/a-/AOh14Gh8NPux8jEPIuyPWOxAB1od9fGN188Kcp5HeXgc=s72-c',
image192:
'https://lh3.googleusercontent.com/a-/AOh14Gh8NPux8jEPIuyPWOxAB1od9fGN188Kcp5HeXgc=s192-c',
image512:
'https://lh3.googleusercontent.com/a-/AOh14Gh8NPux8jEPIuyPWOxAB1od9fGN188Kcp5HeXgc=s512-c',
},
},
teams: [
{
id: '3362fe18-05ad-4457-9632-84f22887dda6',
type: 'team',
name: 'Finance',
description: 'This is Finance description.',
displayName: 'Finance',
deleted: false,
href: 'http://localhost:8585/api/v1/teams/3362fe18-05ad-4457-9632-84f22887dda6',
},
{
id: '5069ddd4-d47e-4b2c-a4c4-4c849b97b7f9',
type: 'team',
name: 'Data_Platform',
description: 'This is Data_Platform description.',
displayName: 'Data_Platform',
deleted: false,
href: 'http://localhost:8585/api/v1/teams/5069ddd4-d47e-4b2c-a4c4-4c849b97b7f9',
},
{
id: '7182cc43-aebc-419d-9452-ddbe2fc4e640',
type: 'team',
name: 'Customer_Support',
description: 'This is Customer_Support description.',
displayName: 'Customer_Support',
deleted: false,
href: 'http://localhost:8585/api/v1/teams/7182cc43-aebc-419d-9452-ddbe2fc4e640',
},
],
owns: [],
follows: [],
deleted: false,
roles: [
{
id: 'ce4df2a5-aaf5-4580-8556-254f42574aa7',
type: 'role',
name: '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/ce4df2a5-aaf5-4580-8556-254f42574aa7',
},
],
};
jest.mock('../../components/MyData/LeftSidebar/LeftSidebar.component', () =>
jest.fn().mockReturnValue(<p>Sidebar</p>)
);
const mockUpdateCurrentUser = jest.fn();
jest.mock('../../hooks/useApplicationStore', () => {
return {
useApplicationStore: jest.fn(() => ({
isAuthDisabled: true,
currentUser: USER_DATA,
updateCurrentUser: mockUpdateCurrentUser,
})),
};
});
@ -124,20 +52,26 @@ jest.mock('../../components/common/Loader/Loader', () => {
});
jest.mock('../../components/Settings/Users/Users.component', () => {
return jest.fn().mockReturnValue(<p>User Component</p>);
return jest.fn().mockImplementation(({ updateUserDetails }) => (
<div>
<p>User Component</p>
<button
onClick={() =>
updateUserDetails({ defaultPersona: undefined }, 'defaultPersona')
}>
UserComponentSaveButton
</button>
</div>
));
});
jest.mock('../../rest/userAPI', () => ({
getUserByName: jest
getUserByName: jest.fn().mockImplementation(() => Promise.resolve(USER_DATA)),
updateUserDetail: jest
.fn()
.mockImplementation(() => Promise.resolve({ data: mockUserData })),
}));
jest.mock('../../rest/userAPI', () => ({
getUserByName: jest
.fn()
.mockImplementation(() => Promise.resolve({ data: mockUserData })),
updateUserDetail: jest.fn(),
.mockImplementation(() =>
Promise.resolve({ ...USER_DATA, defaultPersona: undefined })
),
}));
jest.mock('../../rest/feedsAPI', () => ({
@ -176,4 +110,55 @@ describe('Test the User Page', () => {
expect(errorPlaceholder).toBeInTheDocument();
});
it('Should call and update state data with patch api for defaultPersona', async () => {
const userData = { ...USER_DATA };
delete userData.defaultPersona;
await act(async () => {
render(<UserPage />, { wrapper: MemoryRouter });
});
await act(async () => {
fireEvent.click(screen.getByText('UserComponentSaveButton'));
});
expect(updateUserDetail).toHaveBeenCalledWith(USER_DATA.id, [
{
op: 'remove',
path: '/defaultPersona',
},
]);
expect(mockUpdateCurrentUser).toHaveBeenCalledWith(userData);
});
it('Should call updateCurrentUser if user is currentUser logged in', async () => {
await act(async () => {
render(<UserPage />, { wrapper: MemoryRouter });
});
await act(async () => {
fireEvent.click(screen.getByText('UserComponentSaveButton'));
});
expect(mockUpdateCurrentUser).toHaveBeenCalled();
});
it('Should not call updateCurrentUser if user is not currentUser logged in', async () => {
(useApplicationStore as unknown as jest.Mock).mockImplementation(() => ({
currentUser: { ...USER_DATA, id: '123' },
updateCurrentUser: mockUpdateCurrentUser,
}));
await act(async () => {
render(<UserPage />, { wrapper: MemoryRouter });
});
await act(async () => {
fireEvent.click(screen.getByText('UserComponentSaveButton'));
});
expect(mockUpdateCurrentUser).not.toHaveBeenCalled();
});
});