Skip to content

Commit 8f24c89

Browse files
committed
test: improve test coverage
KK-1420 - OccurrenceTimeRangeField - OccurrenceAttendedField
1 parent 1b24a3e commit 8f24c89

4 files changed

Lines changed: 384 additions & 2 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React from 'react';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import {
4+
useTranslate,
5+
useGetOne,
6+
Loading,
7+
useNotify,
8+
useLocaleState,
9+
} from 'react-admin';
10+
import { ThemeProvider } from '@mui/material/styles';
11+
import { createTheme } from '@mui/material';
12+
import * as Sentry from '@sentry/browser';
13+
14+
import Dashboard from '../Dashboard';
15+
import projectService from '../../projects/projectService';
16+
17+
// Mock necessary react-admin hooks
18+
vi.mock('react-admin', async (importOriginal) => ({
19+
...(await importOriginal()),
20+
useTranslate: vi.fn(),
21+
useGetOne: vi.fn(),
22+
useNotify: vi.fn(),
23+
useLocaleState: vi.fn(),
24+
}));
25+
26+
// Mock Sentry
27+
vi.mock('@sentry/browser', async (importActual) => ({
28+
...(await importActual()),
29+
captureException: vi.fn(),
30+
}));
31+
32+
// Mock projectService
33+
vi.mock('../../projects/projectService', async (importOriginal) => ({
34+
...(await importOriginal()),
35+
projectId: 'test-project-id',
36+
}));
37+
38+
const theme = createTheme();
39+
40+
describe('Dashboard', () => {
41+
const mockTranslate = vi.fn((key) => key);
42+
const mockNotify = vi.fn();
43+
const mockLocale = 'fi';
44+
const mockCaptureException = vi.fn();
45+
const mockProjectData = {
46+
id: 'test-project-id',
47+
translations: { FI: { name: 'name_fi' }, EN: { name: 'name_en' } },
48+
year: 2023,
49+
};
50+
beforeEach(() => {
51+
vi.clearAllMocks();
52+
useTranslate.mockReturnValue(mockTranslate);
53+
useNotify.mockReturnValue(mockNotify);
54+
useLocaleState.mockReturnValue([mockLocale, vi.fn()]);
55+
Sentry.captureException.mockImplementation(mockCaptureException);
56+
});
57+
58+
it('should render dashboard title', async () => {
59+
useGetOne.mockReturnValue({
60+
data: mockProjectData,
61+
isLoading: false,
62+
});
63+
64+
render(
65+
<ThemeProvider theme={theme}>
66+
<Dashboard />
67+
</ThemeProvider>
68+
);
69+
70+
await waitFor(() =>
71+
expect(mockTranslate).toHaveBeenCalledWith('dashboard.title')
72+
);
73+
expect(screen.getByText('dashboard.title')).toBeInTheDocument();
74+
});
75+
76+
it('should render project data when data is loaded', async () => {
77+
useGetOne.mockReturnValue({
78+
data: mockProjectData,
79+
isLoading: false,
80+
});
81+
82+
render(
83+
<ThemeProvider theme={theme}>
84+
<Dashboard />
85+
</ThemeProvider>
86+
);
87+
88+
await screen.findByText('2023 name_fi');
89+
expect(screen.getByText('2023 name_fi')).toBeInTheDocument();
90+
});
91+
92+
it('should call Sentry and notify on error', async () => {
93+
const mockError = new Error('Test error');
94+
useGetOne.mockImplementation((resource, params, { onError }) => {
95+
onError(mockError);
96+
return {
97+
data: undefined,
98+
isLoading: false,
99+
error: mockError,
100+
};
101+
});
102+
103+
render(
104+
<ThemeProvider theme={theme}>
105+
<Dashboard />
106+
</ThemeProvider>
107+
);
108+
109+
await waitFor(() => {
110+
expect(mockCaptureException).toHaveBeenCalledWith(mockError);
111+
});
112+
expect(mockNotify).toHaveBeenCalledWith('ra.message.error', {
113+
type: 'warning',
114+
});
115+
});
116+
});

src/domain/occurrences/fields/OccurrenceAttendedField.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ const OccurrenceAttendedField = () => {
1616
);
1717
const dataProvider = useDataProvider();
1818
const translate = useTranslate();
19-
const handleChange = (event: SelectChangeEvent) => {
19+
const handleChange = async (event: SelectChangeEvent) => {
2020
const value = event.target.value;
21-
dataProvider.setEnrolmentAttendance(enrolment.id, JSON.parse(value));
21+
try {
22+
await dataProvider.setEnrolmentAttendance(
23+
enrolment.id,
24+
JSON.parse(value)
25+
);
26+
} catch (error) {
27+
Promise.reject(error);
28+
}
2229
setAttended(value);
2330
};
2431
return (
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3+
import {
4+
DataProviderContext,
5+
RecordContext,
6+
TranslatableContext,
7+
} from 'react-admin';
8+
import { MemoryRouter } from 'react-router-dom';
9+
import { describe, it, expect, vi } from 'vitest';
10+
import { QueryClient, QueryClientProvider } from 'react-query';
11+
12+
import OccurrenceAttendedField from '../OccurrenceAttendedField';
13+
14+
vi.mock('react-admin', async () => {
15+
const actual = await import('react-admin');
16+
return {
17+
...actual,
18+
TranslatableContext: {
19+
Provider: actual.TranslatableContext.Provider,
20+
Consumer: actual.TranslatableContext.Consumer,
21+
},
22+
};
23+
});
24+
25+
// Mock data provider
26+
const mockDataProvider = {
27+
setEnrolmentAttendance: vi.fn(),
28+
};
29+
30+
// Mock translate function
31+
const mockTranslate = (key) => key;
32+
33+
describe('OccurrenceAttendedField', () => {
34+
const enrolment = {
35+
node: {
36+
id: '1',
37+
attended: null,
38+
},
39+
cursor: '1',
40+
};
41+
const attendedEnrolment = {
42+
node: {
43+
id: '2',
44+
attended: true,
45+
},
46+
cursor: '2',
47+
};
48+
const notAttendedEnrolment = {
49+
node: {
50+
id: '3',
51+
attended: false,
52+
},
53+
cursor: '3',
54+
};
55+
56+
// eslint-disable-next-line react/prop-types
57+
const ProviderWrapper = ({ children }) => {
58+
const queryClient = new QueryClient();
59+
return (
60+
<MemoryRouter>
61+
<QueryClientProvider client={queryClient}>
62+
<TranslatableContext.Provider value={{ translate: mockTranslate }}>
63+
<DataProviderContext.Provider value={mockDataProvider}>
64+
<RecordContext.Provider value={enrolment}>
65+
{children}
66+
</RecordContext.Provider>
67+
</DataProviderContext.Provider>
68+
</TranslatableContext.Provider>
69+
</QueryClientProvider>
70+
</MemoryRouter>
71+
);
72+
};
73+
74+
beforeEach(() => {
75+
vi.clearAllMocks();
76+
});
77+
78+
it('displays the correct default value', () => {
79+
render(<OccurrenceAttendedField />, { wrapper: ProviderWrapper });
80+
expect(
81+
screen.getByText(
82+
'occurrences.fields.enrolments.fields.attended.choices.null'
83+
)
84+
).toBeInTheDocument();
85+
});
86+
87+
it.each([
88+
{
89+
translationKey:
90+
'occurrences.fields.enrolments.fields.attended.choices.true',
91+
value: true,
92+
},
93+
{
94+
translationKey:
95+
'occurrences.fields.enrolments.fields.attended.choices.false',
96+
value: false,
97+
},
98+
])(
99+
'calls dataProvider.setEnrolmentAttendance when the selection changes to $value',
100+
async ({ translationKey, value }) => {
101+
mockDataProvider.setEnrolmentAttendance.mockResolvedValue(
102+
'setEnrolmentAttendance'
103+
);
104+
const { container } = render(<OccurrenceAttendedField />, {
105+
wrapper: ProviderWrapper,
106+
});
107+
const menuTrigger = screen.getByText(
108+
'occurrences.fields.enrolments.fields.attended.choices.null'
109+
);
110+
fireEvent.mouseDown(menuTrigger);
111+
const menuItem = await screen.findByText(translationKey);
112+
fireEvent.click(menuItem);
113+
await waitFor(() => {
114+
expect(mockDataProvider.setEnrolmentAttendance).toHaveBeenCalledWith(
115+
'1',
116+
value
117+
);
118+
});
119+
}
120+
);
121+
122+
it.each([
123+
{
124+
record: attendedEnrolment,
125+
textContent: 'occurrences.fields.enrolments.fields.attended.choices.true',
126+
value: true,
127+
},
128+
{
129+
record: notAttendedEnrolment,
130+
textContent:
131+
'occurrences.fields.enrolments.fields.attended.choices.false',
132+
value: false,
133+
},
134+
])(
135+
'displays the correct value when attended is $value',
136+
({ record, textContent }) => {
137+
const queryClient = new QueryClient();
138+
render(
139+
<MemoryRouter>
140+
<QueryClientProvider client={queryClient}>
141+
<TranslatableContext.Provider value={{ translate: mockTranslate }}>
142+
<DataProviderContext.Provider value={mockDataProvider}>
143+
<RecordContext.Provider value={record}>
144+
<OccurrenceAttendedField />
145+
</RecordContext.Provider>
146+
</DataProviderContext.Provider>
147+
</TranslatableContext.Provider>
148+
</QueryClientProvider>
149+
</MemoryRouter>
150+
);
151+
expect(screen.getByText(textContent)).toBeInTheDocument();
152+
}
153+
);
154+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import { RecordContext } from 'react-admin';
4+
import { MemoryRouter } from 'react-router-dom';
5+
import { describe, it, expect, vi } from 'vitest';
6+
7+
import OccurrenceTimeRangeField from '../OccurrenceTimeRangeField';
8+
9+
vi.mock('react-admin', async (importOriginal) => {
10+
const actual = await importOriginal();
11+
return {
12+
...actual,
13+
};
14+
});
15+
16+
vi.mock(
17+
'../../../../common/translation/I18nProvider',
18+
async (importOriginal) => {
19+
const actual = await importOriginal();
20+
return {
21+
...actual,
22+
getLocale: () => 'fi',
23+
};
24+
}
25+
);
26+
27+
// Mock data provider
28+
const mockDataProvider = {
29+
setEnrolmentAttendance: vi.fn(),
30+
};
31+
32+
// Mock I18nProvider
33+
const mockI18nProvider = {
34+
getLocale: () => 'fi',
35+
};
36+
37+
describe('OccurrenceTimeRangeField', () => {
38+
const occurrence = {
39+
id: '1',
40+
time: '2023-10-27T06:00:00Z',
41+
event: {
42+
name: 'Event 1',
43+
duration: 3 * 60,
44+
eventGroup: null,
45+
},
46+
};
47+
const occurrenceWithoutDuration = {
48+
id: '2',
49+
time: '2023-10-28T07:00:00Z',
50+
event: {
51+
nameme: 'Event 2',
52+
duration: null,
53+
eventGroup: null,
54+
},
55+
};
56+
57+
// eslint-disable-next-line react/prop-types
58+
const ProviderWrapper = ({ children }) => {
59+
return <MemoryRouter>{children}</MemoryRouter>;
60+
};
61+
62+
it('displays the duration when it exists', () => {
63+
render(
64+
<RecordContext.Provider value={occurrence}>
65+
<OccurrenceTimeRangeField />
66+
</RecordContext.Provider>,
67+
{ wrapper: ProviderWrapper }
68+
);
69+
expect(screen.getByText('09.00 - 12.00')).toBeInTheDocument();
70+
});
71+
72+
it('displays the startTime when duration does not exist', () => {
73+
render(
74+
<RecordContext.Provider value={occurrenceWithoutDuration}>
75+
<OccurrenceTimeRangeField />
76+
</RecordContext.Provider>
77+
),
78+
{ wrapper: ProviderWrapper };
79+
expect(screen.getByText('10.00 -')).toBeInTheDocument();
80+
});
81+
82+
it('renders null when record is not provided', () => {
83+
const { container } = render(
84+
<RecordContext.Provider value={null}>
85+
<OccurrenceTimeRangeField />
86+
</RecordContext.Provider>,
87+
{ wrapper: ProviderWrapper }
88+
);
89+
expect(container.firstChild).toBeNull();
90+
});
91+
92+
it('displays correct duration when using occurrenceSource', () => {
93+
const recordWithOccurrence = {
94+
otherData: 'some data',
95+
occurrence: occurrence,
96+
};
97+
render(
98+
<RecordContext.Provider value={recordWithOccurrence}>
99+
<OccurrenceTimeRangeField occurrenceSource="occurrence" />
100+
</RecordContext.Provider>,
101+
{ wrapper: ProviderWrapper }
102+
);
103+
expect(screen.getByText('09.00 - 12.00')).toBeInTheDocument();
104+
});
105+
});

0 commit comments

Comments
 (0)