Skip to content

Commit 045db70

Browse files
test: adding unit test for audit user page components
1 parent f6f8c73 commit 045db70

10 files changed

Lines changed: 1270 additions & 9 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
4+
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
5+
import { IntlProvider } from '@edx/frontend-platform/i18n';
6+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
7+
import AuditUserPage from './index';
8+
9+
jest.mock('@edx/frontend-platform/auth', () => ({
10+
getAuthenticatedHttpClient: jest.fn(),
11+
configure: jest.fn(), // Add this line
12+
}));
13+
14+
const mockUser = {
15+
username: 'johndoe',
16+
17+
profile_image: { has_image: false },
18+
};
19+
const mockAssignments = {
20+
count: 1,
21+
results: [
22+
{
23+
id: '1',
24+
role: 'library_admin',
25+
org: 'Test Org',
26+
scope: 'lib:test',
27+
permissionCount: 5,
28+
},
29+
],
30+
next: null,
31+
previous: null,
32+
};
33+
34+
const renderWithRouter = (route = '/audit/johndoe') => {
35+
const queryClient = new QueryClient({
36+
defaultOptions: {
37+
queries: {
38+
retry: false,
39+
},
40+
},
41+
});
42+
43+
return render(
44+
<QueryClientProvider client={queryClient}>
45+
<IntlProvider locale="en">
46+
<MemoryRouter initialEntries={[route]}>
47+
<Routes>
48+
<Route path="/audit/:username" element={<AuditUserPage />} />
49+
<Route path="/authz" element={<div>Home Page</div>} />
50+
</Routes>
51+
</MemoryRouter>
52+
</IntlProvider>
53+
</QueryClientProvider>,
54+
);
55+
};
56+
57+
describe('AuditUserPage', () => {
58+
beforeEach(() => {
59+
jest.clearAllMocks();
60+
});
61+
62+
it('renders user info and table when data is loaded', async () => {
63+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
64+
get: jest
65+
.fn()
66+
.mockResolvedValueOnce({ data: mockUser })
67+
.mockResolvedValueOnce({ data: mockAssignments }),
68+
});
69+
70+
renderWithRouter();
71+
72+
await waitFor(() => {
73+
expect(screen.getByRole('heading', { name: 'johndoe' })).toBeInTheDocument();
74+
expect(screen.getByText('[email protected]')).toBeInTheDocument();
75+
expect(screen.getByRole('button', { name: /assign role/i })).toBeInTheDocument();
76+
expect(screen.getByText('Library Admin')).toBeInTheDocument();
77+
expect(screen.getByText('Test Org')).toBeInTheDocument();
78+
expect(screen.getByText('lib:test')).toBeInTheDocument();
79+
expect(screen.getByText('5 permissions available')).toBeInTheDocument();
80+
});
81+
});
82+
83+
it('navigates to home if user is not found', async () => {
84+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
85+
get: jest
86+
.fn()
87+
.mockResolvedValueOnce({ data: null })
88+
.mockResolvedValueOnce({ data: mockAssignments }),
89+
});
90+
91+
renderWithRouter();
92+
93+
await waitFor(() => {
94+
expect(screen.getByText('Home Page')).toBeInTheDocument();
95+
});
96+
});
97+
98+
it('allows user to interact with Assign Role button', async () => {
99+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
100+
get: jest
101+
.fn()
102+
.mockResolvedValueOnce({ data: mockUser })
103+
.mockResolvedValueOnce({ data: mockAssignments }),
104+
});
105+
106+
renderWithRouter();
107+
108+
await waitFor(() => {
109+
expect(screen.getByRole('button', { name: /assign role/i })).toBeInTheDocument();
110+
});
111+
112+
const user = userEvent.setup();
113+
const button = screen.getByRole('button', { name: /assign role/i });
114+
await user.click(button);
115+
expect(button).not.toBeInTheDocument();
116+
});
117+
118+
it('renders empty state when user has no assignments', async () => {
119+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
120+
get: jest
121+
.fn()
122+
.mockResolvedValueOnce({ data: mockUser })
123+
.mockResolvedValueOnce({
124+
data: {
125+
count: 0, results: [], next: null, previous: null,
126+
},
127+
}),
128+
});
129+
130+
renderWithRouter();
131+
132+
await waitFor(() => {
133+
expect(screen.getByRole('heading', { name: 'johndoe' })).toBeInTheDocument();
134+
expect(screen.queryByText('5 permissions available')).not.toBeInTheDocument();
135+
expect(screen.getByRole('table')).toBeInTheDocument();
136+
});
137+
});
138+
139+
it('renders correct table headers', async () => {
140+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
141+
get: jest
142+
.fn()
143+
.mockResolvedValueOnce({ data: mockUser })
144+
.mockResolvedValueOnce({ data: mockAssignments }),
145+
});
146+
147+
renderWithRouter();
148+
149+
await waitFor(() => {
150+
expect(screen.getByText('Role')).toBeInTheDocument();
151+
expect(screen.getByText('Organization')).toBeInTheDocument();
152+
expect(screen.getByText('Scope')).toBeInTheDocument();
153+
expect(screen.getByText('Permissions')).toBeInTheDocument();
154+
expect(screen.getByText('Actions')).toBeInTheDocument();
155+
});
156+
});
157+
158+
it('renders the pagination controls when assignments are present', async () => {
159+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
160+
get: jest
161+
.fn()
162+
.mockResolvedValueOnce({ data: mockUser })
163+
.mockResolvedValueOnce({ data: mockAssignments }),
164+
});
165+
166+
renderWithRouter();
167+
168+
await waitFor(() => {
169+
expect(screen.getByText('Showing 1 of 1.')).toBeInTheDocument();
170+
});
171+
});
172+
173+
it('renders the breadcrumb navigation with home link', async () => {
174+
(getAuthenticatedHttpClient as jest.Mock).mockReturnValue({
175+
get: jest
176+
.fn()
177+
.mockResolvedValueOnce({ data: mockUser })
178+
.mockResolvedValueOnce({ data: mockAssignments }),
179+
});
180+
181+
renderWithRouter();
182+
183+
await waitFor(() => {
184+
expect(screen.getByRole('link', { name: /roles and permissions management/i })).toBeInTheDocument();
185+
expect(screen.getByText(mockUser.username, { selector: 'li[aria-current="page"]' })).toBeInTheDocument();
186+
});
187+
});
188+
});

src/authz-module/audit-user/index.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
Container, DataTable,
66
} from '@openedx/paragon';
77
import TableFooter from '@src/authz-module/components/TableFooter/TableFooter';
8-
import { TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants';
8+
import { AUTHZ_HOME_PATH, TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants';
99
import AuthZLayout from '@src/authz-module/components/AuthZLayout';
1010
import { useNavigate, useParams } from 'react-router-dom';
1111
import { useUserAccount } from '@src/data/hooks';
@@ -24,16 +24,15 @@ const AuditUserPage = () => {
2424
const navigate = useNavigate();
2525
const { isLoading: isLoadingUser, data: user } = useUserAccount(username ?? '');
2626
const { querySettings, handleTableFetch } = useQuerySettings();
27-
// TODO: use actual assigned roles data when API is ready, currently using dummy data for development purpose
2827
const { data: { results: userAssignments } = { results: [] } } = useUserAssignedRoles(username ?? '', querySettings);
29-
const authzHomePath = '/authz';
28+
3029
if (!user && !isLoadingUser) {
31-
navigate(authzHomePath);
30+
navigate(AUTHZ_HOME_PATH);
3231
}
3332
const navLinks = [
3433
{
3534
label: formatMessage(baseMessages['authz.management.home.nav.link']),
36-
to: authzHomePath,
35+
to: AUTHZ_HOME_PATH,
3736
},
3837
];
3938
const additionalColumns = [
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { useNavigate } from 'react-router-dom';
4+
import { initializeMockApp } from '@edx/frontend-platform/testing';
5+
import { renderWrapper } from '@src/setupTest';
6+
import AddRoleButton from './AddRoleButton';
7+
8+
// Mock react-router-dom navigation
9+
jest.mock('react-router-dom', () => ({
10+
...jest.requireActual('react-router-dom'),
11+
useNavigate: jest.fn(),
12+
}));
13+
14+
describe('AddRoleButton', () => {
15+
const mockNavigate = jest.fn();
16+
17+
beforeAll(() => {
18+
initializeMockApp({
19+
authenticatedUser: {
20+
userId: 1,
21+
username: 'testuser',
22+
23+
},
24+
});
25+
});
26+
27+
beforeEach(() => {
28+
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
29+
});
30+
31+
afterEach(() => {
32+
jest.clearAllMocks();
33+
});
34+
35+
describe('rendering', () => {
36+
it('renders the assign role button with correct text', () => {
37+
renderWrapper(<AddRoleButton />);
38+
39+
const button = screen.getByRole('button', { name: /assign role/i });
40+
expect(button).toBeInTheDocument();
41+
});
42+
43+
it('displays the plus icon', () => {
44+
renderWrapper(<AddRoleButton />);
45+
46+
const button = screen.getByRole('button', { name: /assign role/i });
47+
expect(button.querySelector('svg')).toBeInTheDocument();
48+
});
49+
50+
it('renders correctly when presetUsername is provided', () => {
51+
renderWrapper(<AddRoleButton presetUsername="testuser123" />);
52+
53+
const button = screen.getByRole('button', { name: /assign role/i });
54+
expect(button).toBeInTheDocument();
55+
});
56+
});
57+
58+
describe('navigation behavior', () => {
59+
it('navigates to assign role page without username when clicked', async () => {
60+
const user = userEvent.setup();
61+
renderWrapper(<AddRoleButton />);
62+
63+
const button = screen.getByRole('button', { name: /assign role/i });
64+
await user.click(button);
65+
66+
expect(mockNavigate).toHaveBeenCalledTimes(1);
67+
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role');
68+
});
69+
70+
it('navigates to assign role page with username query parameter when presetUsername is provided', async () => {
71+
const user = userEvent.setup();
72+
const presetUsername = 'john.doe';
73+
renderWrapper(<AddRoleButton presetUsername={presetUsername} />);
74+
75+
const button = screen.getByRole('button', { name: /assign role/i });
76+
await user.click(button);
77+
78+
expect(mockNavigate).toHaveBeenCalledTimes(1);
79+
expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?username=${presetUsername}`);
80+
});
81+
82+
it('handles special characters in presetUsername correctly', async () => {
83+
const user = userEvent.setup();
84+
const presetUsername = '[email protected]';
85+
renderWrapper(<AddRoleButton presetUsername={presetUsername} />);
86+
87+
const button = screen.getByRole('button', { name: /assign role/i });
88+
await user.click(button);
89+
90+
expect(mockNavigate).toHaveBeenCalledTimes(1);
91+
expect(mockNavigate).toHaveBeenCalledWith(`/authz/assign-role?username=${presetUsername}`);
92+
});
93+
});
94+
95+
describe('user interactions', () => {
96+
it('responds to keyboard navigation', async () => {
97+
const user = userEvent.setup();
98+
renderWrapper(<AddRoleButton />);
99+
100+
const button = screen.getByRole('button', { name: /assign role/i });
101+
102+
await user.tab();
103+
expect(button).toHaveFocus();
104+
105+
await user.keyboard('{Enter}');
106+
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role');
107+
});
108+
109+
it('responds to spacebar activation', async () => {
110+
const user = userEvent.setup();
111+
renderWrapper(<AddRoleButton />);
112+
113+
const button = screen.getByRole('button', { name: /assign role/i });
114+
button.focus();
115+
116+
await user.keyboard(' ');
117+
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role');
118+
});
119+
120+
it('handles multiple clicks gracefully', async () => {
121+
const user = userEvent.setup();
122+
renderWrapper(<AddRoleButton presetUsername="testuser" />);
123+
124+
const button = screen.getByRole('button', { name: /assign role/i });
125+
126+
await user.click(button);
127+
await user.click(button);
128+
await user.click(button);
129+
130+
expect(mockNavigate).toHaveBeenCalledTimes(3);
131+
expect(mockNavigate).toHaveBeenCalledWith('/authz/assign-role?username=testuser');
132+
});
133+
});
134+
});

0 commit comments

Comments
 (0)