Skip to content

Commit 32c18cc

Browse files
sherryy021229-sysyangY
andauthored
[YW-357] Testing modals and tables (#16)
* testing modals and tables * format --------- Co-authored-by: yangY <2564436853@qq.com>
1 parent a534521 commit 32c18cc

22 files changed

Lines changed: 3820 additions & 208 deletions

package-lock.json

Lines changed: 339 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"dependencies": {
1313
"@ant-design/icons": "^5.4.0",
1414
"@testing-library/dom": "^10.4.1",
15-
"@testing-library/user-event": "^13.5.0",
1615
"antd": "^5.27.4",
1716
"axios": "^1.12.2",
1817
"dayjs": "^1.11.18",
@@ -69,6 +68,8 @@
6968
"devDependencies": {
7069
"@testing-library/jest-dom": "^5.17.0",
7170
"@testing-library/react": "^13.4.0",
71+
"@testing-library/user-event": "^14.6.1",
72+
"babel-jest": "^30.2.0",
7273
"eslint-plugin-react-hooks": "^4.6.2",
7374
"gh-pages": "^6.3.0",
7475
"prettier": "3.3.3",
Lines changed: 289 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,299 @@
1+
// Import React testing utilities
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
// Import the component to test
15
import BanUserModal from './banusermodal';
26

3-
// Mock antd components
4-
jest.mock('antd', () => ({
5-
...jest.requireActual('antd'),
6-
message: {
7-
success: jest.fn(),
8-
error: jest.fn(),
9-
},
10-
}));
11-
12-
// Mock antd icons
13-
jest.mock('@ant-design/icons', () => ({
14-
ExclamationCircleOutlined: () => <div data-testid="exclamation-icon" />,
15-
UserDeleteOutlined: () => <div data-testid="user-delete-icon" />,
16-
}));
7+
// Mock Ant Design message service
8+
jest.mock('antd', () => {
9+
const actual = jest.requireActual('antd');
10+
return {
11+
...actual,
12+
message: {
13+
success: jest.fn(),
14+
error: jest.fn(),
15+
},
16+
};
17+
});
1718

1819
describe('BanUserModal', () => {
19-
test('is a valid React component', () => {
20-
expect(typeof BanUserModal).toBe('function');
20+
// Define mock data and props
21+
const mockUser = {
22+
id: '123',
23+
username: 'testuser',
24+
};
25+
26+
const defaultProps = {
27+
visible: true,
28+
onCancel: jest.fn(),
29+
onConfirm: jest.fn(),
30+
user: mockUser,
31+
loading: false,
32+
};
33+
34+
// Set up a new user event instance for each test
35+
// This is the modern replacement for fireEvent for complex interactions
36+
let user;
37+
beforeEach(() => {
38+
user = userEvent.setup();
39+
jest.clearAllMocks();
40+
41+
// Mock window.matchMedia, which is required by Ant Design's components in a JSDOM environment
42+
Object.defineProperty(window, 'matchMedia', {
43+
writable: true,
44+
value: jest.fn().mockImplementation((query) => ({
45+
matches: false,
46+
media: query,
47+
onchange: null,
48+
addListener: jest.fn(), // deprecated
49+
removeListener: jest.fn(), // deprecated
50+
addEventListener: jest.fn(),
51+
removeEventListener: jest.fn(),
52+
dispatchEvent: jest.fn(),
53+
})),
54+
});
55+
});
56+
57+
afterEach(() => {
58+
jest.restoreAllMocks();
59+
});
60+
61+
describe('Rendering', () => {
62+
it('should render modal when visible is true and user is provided', () => {
63+
render(<BanUserModal {...defaultProps} />);
64+
65+
expect(screen.getByText(/Ban User: testuser/i)).toBeInTheDocument();
66+
expect(
67+
screen.getByText(/This action will restrict the user's access/i)
68+
).toBeInTheDocument();
69+
});
70+
71+
it('should not render when user is null', () => {
72+
const { container } = render(
73+
<BanUserModal {...defaultProps} user={null} />
74+
);
75+
// The modal root should not be in the container
76+
expect(container.firstChild).toBeNull();
77+
});
78+
79+
it('should not render when user is undefined', () => {
80+
const { container } = render(
81+
<BanUserModal {...defaultProps} user={undefined} />
82+
);
83+
// The modal root should not be in the container
84+
expect(container.firstChild).toBeNull();
85+
});
86+
87+
it('should render all form fields', () => {
88+
render(<BanUserModal {...defaultProps} />);
89+
90+
expect(screen.getByText('Ban Type')).toBeInTheDocument();
91+
expect(screen.getByText('Ban Reason')).toBeInTheDocument();
92+
expect(
93+
screen.getByText('Public Reason (shown to user)')
94+
).toBeInTheDocument();
95+
expect(screen.getByText('Internal Admin Notes')).toBeInTheDocument();
96+
});
97+
98+
it('should have correct initial form values', () => {
99+
render(<BanUserModal {...defaultProps} />);
100+
101+
// Check checkboxes initial state
102+
const notifyCheckbox = screen.getByRole('checkbox', {
103+
name: /Send ban notification/i,
104+
});
105+
const deleteCheckbox = screen.getByRole('checkbox', {
106+
name: /Delete all user's content/i,
107+
});
108+
109+
expect(notifyCheckbox).toBeChecked();
110+
expect(deleteCheckbox).not.toBeChecked();
111+
});
112+
});
113+
114+
describe('Ban Type Selection', () => {
115+
it('should show expiration date picker for temporary ban', async () => {
116+
render(<BanUserModal {...defaultProps} />);
117+
118+
// Default is temporary, so date picker should be visible
119+
expect(screen.getByText('Ban Expires At')).toBeInTheDocument();
120+
});
121+
});
122+
123+
describe('Form Validation', () => {
124+
it('should show validation error when submitting without required fields', async () => {
125+
render(<BanUserModal {...defaultProps} />);
126+
127+
const banButton = screen.getByRole('button', { name: /Ban User/i });
128+
await user.click(banButton);
129+
130+
// Check for all validation messages
131+
await waitFor(() => {
132+
expect(
133+
screen.getByText(/Please select expiration date/i)
134+
).toBeInTheDocument();
135+
expect(
136+
screen.getByText(/Please provide a ban reason/i)
137+
).toBeInTheDocument();
138+
expect(
139+
screen.getByText(/Please provide a public reason/i)
140+
).toBeInTheDocument();
141+
});
142+
});
143+
});
144+
145+
describe('Form Submission', () => {
146+
// Helper function to fill the form with valid temporary ban data
147+
148+
// This test correctly checks for *validation* errors
149+
it('should log validation errors to console', async () => {
150+
// Spy on console.error
151+
const consoleErrorSpy = jest
152+
.spyOn(console, 'error')
153+
.mockImplementation(() => {});
154+
155+
render(<BanUserModal {...defaultProps} />);
156+
157+
// Try to submit without filling required fields
158+
const banButton = screen.getByRole('button', { name: /Ban User/i });
159+
await user.click(banButton);
160+
161+
// Wait for the console log
162+
await waitFor(() => {
163+
expect(consoleErrorSpy).toHaveBeenCalledWith(
164+
'Ban validation failed:',
165+
expect.any(Object)
166+
);
167+
});
168+
169+
consoleErrorSpy.mockRestore();
170+
});
21171
});
22172

23-
test('component can be imported', () => {
24-
expect(BanUserModal).toBeDefined();
173+
describe('Modal Actions', () => {
174+
it('should call onCancel when cancel button is clicked', async () => {
175+
render(<BanUserModal {...defaultProps} />);
176+
177+
const cancelButton = screen.getByRole('button', { name: /Cancel/i });
178+
await user.click(cancelButton);
179+
180+
expect(defaultProps.onCancel).toHaveBeenCalled();
181+
});
182+
183+
it('should show loading state on ban button', () => {
184+
// The test was checking toBeDisabled(), but the error log shows
185+
// the button gets 'ant-btn-loading' class instead.
186+
// We will assert that class.
187+
render(<BanUserModal {...defaultProps} loading={true} />);
188+
189+
const banButton = screen.getByRole('button', { name: /Ban User/i });
190+
expect(banButton).toHaveClass('ant-btn-loading');
191+
});
25192
});
26193

27-
test('component is not null', () => {
28-
expect(BanUserModal).not.toBeNull();
194+
describe('Character Limits', () => {
195+
it('should show character count for public reason', () => {
196+
render(<BanUserModal {...defaultProps} />);
197+
198+
const publicReasonInput = screen.getByPlaceholderText(
199+
/This will be shown to the user/i
200+
);
201+
// Check for the showCount prop (which AntD uses) or maxlength
202+
expect(publicReasonInput).toHaveAttribute('maxlength', '500');
203+
});
204+
205+
it('should show character count for admin notes', () => {
206+
render(<BanUserModal {...defaultProps} />);
207+
208+
const adminNotesInput = screen.getByPlaceholderText(
209+
/Internal notes for other administrators/i
210+
);
211+
expect(adminNotesInput).toHaveAttribute('maxlength', '1000');
212+
});
213+
});
214+
215+
describe('Modal Properties', () => {
216+
it('should have danger button style for ban action', () => {
217+
render(<BanUserModal {...defaultProps} />);
218+
219+
const banButton = screen.getByRole('button', { name: /Ban User/i });
220+
// AntD adds this class for 'danger' prop
221+
expect(banButton).toHaveClass('ant-btn-dangerous');
222+
});
223+
});
224+
225+
describe('Edge Cases', () => {
226+
it('should handle user with special characters in username', () => {
227+
const specialUser = { id: '456', username: 'test@user#123' };
228+
render(<BanUserModal {...defaultProps} user={specialUser} />);
229+
230+
expect(screen.getByText(/Ban User: test@user#123/i)).toBeInTheDocument();
231+
});
232+
233+
it('should not crash when visible changes to false', () => {
234+
const { rerender } = render(<BanUserModal {...defaultProps} />);
235+
236+
expect(screen.getByText(/Ban User: testuser/i)).toBeInTheDocument();
237+
238+
// Rerender with visible: false
239+
rerender(<BanUserModal {...defaultProps} visible={false} />);
240+
241+
// Modal should not be visible but should not crash
242+
expect(screen.queryByText(/Ban User: testuser/i)).not.toBeInTheDocument();
243+
});
244+
245+
it('should handle rapid form submissions', async () => {
246+
render(<BanUserModal {...defaultProps} />);
247+
248+
const banButton = screen.getByRole('button', { name: /Ban User/i });
249+
250+
// Click multiple times rapidly
251+
// userEvent handles this more gracefully than fireEvent
252+
await user.click(banButton);
253+
await user.click(banButton);
254+
255+
// Should only trigger validation once and not crash
256+
await waitFor(() => {
257+
expect(screen.getAllByText(/Please/i).length).toBeGreaterThan(0);
258+
});
259+
// onConfirm should not be called at all
260+
expect(defaultProps.onConfirm).not.toHaveBeenCalled();
261+
});
262+
});
263+
264+
describe('Accessibility', () => {
265+
it('should have proper labels for form fields', () => {
266+
render(<BanUserModal {...defaultProps} />);
267+
268+
// Check that labels exist
269+
expect(screen.getByText('Ban Type')).toBeInTheDocument();
270+
expect(screen.getByText('Ban Reason')).toBeInTheDocument();
271+
expect(
272+
screen.getByText('Public Reason (shown to user)')
273+
).toBeInTheDocument();
274+
expect(screen.getByText('Internal Admin Notes')).toBeInTheDocument();
275+
276+
// Check that they are linked to inputs (this is a more robust check)
277+
expect(
278+
screen.getByRole('combobox', { name: /Ban Type/i })
279+
).toBeInTheDocument();
280+
expect(
281+
screen.getByRole('combobox', { name: /Ban Reason/i })
282+
).toBeInTheDocument();
283+
});
284+
285+
it('should have accessible checkboxes', () => {
286+
render(<BanUserModal {...defaultProps} />);
287+
288+
const deleteCheckbox = screen.getByRole('checkbox', {
289+
name: /Delete all user's content/i,
290+
});
291+
const notifyCheckbox = screen.getByRole('checkbox', {
292+
name: /Send ban notification/i,
293+
});
294+
295+
expect(deleteCheckbox).toBeInTheDocument();
296+
expect(notifyCheckbox).toBeInTheDocument();
297+
});
29298
});
30299
});

0 commit comments

Comments
 (0)