Skip to content

Commit 1be5db1

Browse files
fusharclaude
andauthored
Standardize judgels-client test patterns (#882)
- Replace document.querySelector with testing-library selectors (getByRole, getByLabelText) for form inputs, selects, checkboxes - Add aria-label to FormTable components for accessibility and testability - Add role="article" to announcement/clarification cards, use role="link" for problem cards - Replace document.querySelectorAll CSS class selectors with getAllByRole - Improve table assertions to check cell content matrix instead of just getByText/row count - Add missing nock.isDone() checks in ContestEditGeneralTab, ContestEditConfigsTab, and ContestEditDescriptionTab tests Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f4e7bcb commit 1be5db1

File tree

19 files changed

+110
-71
lines changed

19 files changed

+110
-71
lines changed

judgels-client/src/components/forms/FormTableCheckbox/FormTableCheckbox.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function FormTableCheckbox(props) {
1616
<Checkbox
1717
defaultChecked={!!value}
1818
onChange={newOnChange}
19+
aria-label={props.label}
1920
{...inputProps}
2021
className={classNames(getIntentClassName(meta))}
2122
/>

judgels-client/src/components/forms/FormTableDateInput/FormTableDateInput.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function FormTableDateInput(props) {
2626
maxDate={new Date(4102444800000)}
2727
timePickerProps={{ showArrowButtons: true }}
2828
onChange={onChange}
29-
inputProps={{ name: inputProps.name }}
29+
inputProps={{ name: inputProps.name, 'aria-label': props.label }}
3030
{...inputProps}
3131
/>
3232
<FormInputValidation meta={meta} />

judgels-client/src/components/forms/FormTableSelect/FormTableSelect.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function FormTableSelect(props) {
1010
const { input, meta, children } = props;
1111
return (
1212
<FormTableInput {...props}>
13-
<HTMLSelect {...input} className={classNames(getIntentClassName(meta))}>
13+
<HTMLSelect {...input} aria-label={props.label} className={classNames(getIntentClassName(meta))}>
1414
{children}
1515
</HTMLSelect>
1616
</FormTableInput>

judgels-client/src/components/forms/FormTableTextInput/FormTableTextInput.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function FormTableTextInput(props) {
1212
{...input}
1313
type={type || 'text'}
1414
disabled={disabled}
15+
aria-label={props.label}
1516
className={classNames(Classes.INPUT, getIntentClassName(meta))}
1617
/>
1718
</FormTableInput>

judgels-client/src/routes/admin/archives/ArchiveCreateDialog/ArchiveCreateDialog.test.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ describe('ArchiveCreateDialog', () => {
2929
const button = screen.getByRole('button');
3030
await user.click(button);
3131

32-
const slug = document.querySelector('input[name="slug"]');
32+
const slug = screen.getByRole('textbox', { name: /slug/i });
3333
await user.type(slug, 'new-archive');
3434

35-
const name = document.querySelector('input[name="name"]');
35+
const name = screen.getByRole('textbox', { name: /^name$/i });
3636
await user.type(name, 'New archive');
3737

38-
const category = document.querySelector('input[name="category"]');
38+
const category = screen.getByRole('textbox', { name: /category/i });
3939
await user.type(category, 'New category');
4040

41-
const description = document.querySelector('textarea[name="description"]');
41+
const description = screen.getByRole('textbox', { name: /description/i });
4242
await user.type(description, 'New description');
4343

4444
nockJerahmeel()

judgels-client/src/routes/admin/users/UserViewPage/UserViewPage.test.jsx

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { act, render, screen, waitFor } from '@testing-library/react';
1+
import { act, render, screen, waitFor, within } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
33
import nock from 'nock';
44

@@ -40,18 +40,34 @@ describe('UserViewPage', () => {
4040
test('renders user details', async () => {
4141
await renderComponent();
4242

43-
expect(await screen.findAllByText(/andi/));
44-
expect(screen.getByText('JIDUSER123')).toBeInTheDocument();
45-
expect(screen.getByText('andi@example.com')).toBeInTheDocument();
46-
});
47-
48-
test('renders user info', async () => {
49-
await renderComponent();
50-
51-
expect(await screen.findByText(/Andi Smith/)).toBeInTheDocument();
52-
expect(screen.getByText('Andi Smith')).toBeInTheDocument();
53-
expect(screen.getByText('MALE')).toBeInTheDocument();
54-
expect(screen.getByText('ID')).toBeInTheDocument();
43+
await screen.findAllByText(/andi/);
44+
const tables = screen.getAllByRole('table');
45+
expect(
46+
within(tables[0])
47+
.getAllByRole('row')
48+
.map(row =>
49+
within(row)
50+
.getAllByRole('cell')
51+
.map(cell => cell.textContent)
52+
)
53+
).toEqual([
54+
['JID', 'JIDUSER123'],
55+
['Email', 'andi@example.com'],
56+
]);
57+
58+
expect(
59+
within(tables[1])
60+
.getAllByRole('row')
61+
.map(row =>
62+
within(row)
63+
.getAllByRole('cell')
64+
.map(cell => cell.textContent)
65+
)
66+
).toEqual([
67+
['Name', 'Andi Smith'],
68+
['Gender', 'MALE'],
69+
['Country', 'ID'],
70+
]);
5571
});
5672

5773
test('user info form', async () => {
@@ -62,16 +78,16 @@ describe('UserViewPage', () => {
6278
const button = await screen.findByRole('button', { name: /edit/i });
6379
await user.click(button);
6480

65-
const name = document.querySelector('input[name="name"]');
81+
const name = screen.getByRole('textbox', { name: /name/i });
6682
expect(name).toHaveValue('Andi Smith');
6783
await user.clear(name);
6884
await user.type(name, 'Caca');
6985

70-
const gender = document.querySelector('select[name="gender"]');
86+
const gender = screen.getByRole('combobox', { name: /gender/i });
7187
expect(gender).toHaveValue('MALE');
7288
await user.selectOptions(gender, 'FEMALE');
7389

74-
const country = document.querySelector('select[name="country"]');
90+
const country = screen.getByRole('combobox', { name: /country/i });
7591
expect(country).toHaveValue('ID');
7692
await user.selectOptions(country, 'US');
7793

judgels-client/src/routes/contests/contests/single/announcements/ContestAnnouncementCard/ContestAnnouncementCard.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function ContestAnnouncementCard({
4141
};
4242

4343
return (
44-
<Callout className="contest-announcement-card" intent={intent} icon={null}>
44+
<Callout role="article" className="contest-announcement-card" intent={intent} icon={null}>
4545
<Flex justifyContent="space-between">
4646
<h5>{announcement.title}</h5>
4747
<p>

judgels-client/src/routes/contests/contests/single/announcements/ContestAnnouncementsPage/ContestAnnouncementsPage.test.jsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,15 @@ describe('ContestAnnouncementsPage', () => {
8383
test('renders placeholder when there are no announcements', async () => {
8484
await renderComponent({ announcements: [] });
8585
expect(await screen.findByText('No announcements.')).toBeInTheDocument();
86-
expect(document.querySelectorAll('div.contest-announcement-card')).toHaveLength(0);
86+
expect(screen.queryAllByRole('article')).toHaveLength(0);
8787
});
8888

8989
test('renders announcements when not canSupervise', async () => {
9090
await renderComponent({ canSupervise: false });
9191
await waitFor(() => {
92-
expect(document.querySelectorAll('div.contest-announcement-card').length).toBeGreaterThan(0);
92+
expect(screen.getAllByRole('article').length).toBeGreaterThan(0);
9393
});
94-
const announcements = document.querySelectorAll('div.contest-announcement-card');
94+
const announcements = screen.getAllByRole('article');
9595
expect(announcements).toHaveLength(2);
9696

9797
expect(within(announcements[0]).getByRole('heading')).toHaveTextContent('Title 1');
@@ -106,9 +106,9 @@ describe('ContestAnnouncementsPage', () => {
106106
test('renders announcements when canSupervise', async () => {
107107
await renderComponent({ canSupervise: true });
108108
await waitFor(() => {
109-
expect(document.querySelectorAll('div.contest-announcement-card').length).toBeGreaterThan(0);
109+
expect(screen.getAllByRole('article').length).toBeGreaterThan(0);
110110
});
111-
const announcements = document.querySelectorAll('div.contest-announcement-card');
111+
const announcements = screen.getAllByRole('article');
112112
expect(announcements).toHaveLength(2);
113113

114114
expect(within(announcements[0]).getByRole('heading')).toHaveTextContent('Title 1');
@@ -123,9 +123,9 @@ describe('ContestAnnouncementsPage', () => {
123123
test('renders announcements when canManage', async () => {
124124
await renderComponent({ canSupervise: true, canManage: true });
125125
await waitFor(() => {
126-
expect(document.querySelectorAll('div.contest-announcement-card').length).toBeGreaterThan(0);
126+
expect(screen.getAllByRole('article').length).toBeGreaterThan(0);
127127
});
128-
const announcements = document.querySelectorAll('div.contest-announcement-card');
128+
const announcements = screen.getAllByRole('article');
129129
expect(announcements).toHaveLength(2);
130130

131131
expect(within(announcements[0]).getByRole('heading')).toHaveTextContent('Title 1');

judgels-client/src/routes/contests/contests/single/clarifications/ContestClarificationCard/ContestClarificationCard.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export function ContestClarificationCard({
7979
};
8080

8181
return (
82-
<Callout className="contest-clarification-card" intent={questionIntent} icon={null}>
82+
<Callout role="article" className="contest-clarification-card" intent={questionIntent} icon={null}>
8383
<Flex justifyContent="space-between">
8484
<h5>
8585
{clarification.title} &nbsp; <Tag>{topic}</Tag>
@@ -93,6 +93,7 @@ export function ContestClarificationCard({
9393
<hr />
9494
<div className="multiline-text">{clarification.question}</div>
9595
<Callout
96+
role="article"
9697
className="contest-clarification-card contest-clarification-card__answer"
9798
intent={answerIntent}
9899
icon={null}

judgels-client/src/routes/contests/contests/single/clarifications/ContestClarificationsPage/ContestClarificationsPage.test.jsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,15 @@ describe('ContestClarificationsPage', () => {
9696
test('renders placeholder when there are no clarifications', async () => {
9797
await renderComponent({ clarifications: [] });
9898
expect(await screen.findByText('No clarifications.')).toBeInTheDocument();
99-
expect(document.querySelectorAll('div.contest-clarification-card')).toHaveLength(0);
99+
expect(screen.queryAllByRole('article')).toHaveLength(0);
100100
});
101101

102102
test('renders clarifications when not canSupervise', async () => {
103103
await renderComponent({ canSupervise: false });
104104
await waitFor(() => {
105-
expect(document.querySelectorAll('div.contest-clarification-card').length).toBeGreaterThan(0);
105+
expect(screen.getAllByRole('article').length).toBeGreaterThan(0);
106106
});
107-
const clarifications = document.querySelectorAll('div.contest-clarification-card');
107+
const clarifications = screen.getAllByRole('article');
108108
expect(clarifications).toHaveLength(4);
109109

110110
expect(within(clarifications[0]).getAllByRole('heading')[0]).toHaveTextContent('Title 1 General');
@@ -125,9 +125,9 @@ describe('ContestClarificationsPage', () => {
125125
test('renders clarifications when canSupervise', async () => {
126126
await renderComponent({ canSupervise: true });
127127
await waitFor(() => {
128-
expect(document.querySelectorAll('div.contest-clarification-card').length).toBeGreaterThan(0);
128+
expect(screen.getAllByRole('article').length).toBeGreaterThan(0);
129129
});
130-
const clarifications = document.querySelectorAll('div.contest-clarification-card');
130+
const clarifications = screen.getAllByRole('article');
131131
expect(clarifications).toHaveLength(4);
132132

133133
expect(within(clarifications[0]).getAllByRole('heading')[0]).toHaveTextContent('Title 1 General');

0 commit comments

Comments
 (0)