Skip to content

Commit 9ffcf30

Browse files
Copilotpayamnj
andauthored
feat: add frontend tests for platform forms and pages
Agent-Logs-Url: https://github.com/AvaCodeSolutions/django-email-learning/sessions/a24da982-8e72-4f1b-b728-72ef31917591 Co-authored-by: payamnj <11951509+payamnj@users.noreply.github.com>
1 parent c5831ec commit 9ffcf30

21 files changed

Lines changed: 1616 additions & 0 deletions

frontend/platform/courses/Courses.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,6 @@ function Courses() {
199199
)
200200
}
201201

202+
export default Courses;
203+
202204
render({children: <Courses />});

frontend/platform/organizations/Organizations.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,6 @@ function Organizations() {
176176
</Base>)
177177
}
178178

179+
export default Organizations;
180+
179181
render({children: <Organizations />});

frontend/platform/settings_api_keys/ApiKeys.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,6 @@ const ApiKeys = () => {
198198
</Base>)
199199
}
200200

201+
export default ApiKeys;
202+
201203
render({children: <ApiKeys />});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { screen, waitFor } from '@testing-library/react';
3+
import { renderWithProviders } from '../test-utils';
4+
import AddImapConnectionForm from '../../../platform/courses/components/AddImapConnectionForm';
5+
6+
vi.mock('../../render.jsx');
7+
8+
const localeMessages = {
9+
imap_connection: 'IMAP Connection',
10+
new_imap_connection: 'New IMAP Connection',
11+
email: 'Email',
12+
password: 'Password',
13+
server: 'Server',
14+
port: 'Port',
15+
add: 'Add',
16+
add_folder_helper_text: 'Enter folder name.',
17+
email_required_helper_text: 'Email is required.',
18+
invalid_email_helper_text: 'Invalid email address.',
19+
password_required_helper_text: 'Password is required.',
20+
server_required_helper_text: 'Server is required.',
21+
port_required_helper_text: 'Port is required.',
22+
invalid_port_helper_text: 'Invalid port number.',
23+
};
24+
25+
describe('AddImapConnectionForm', () => {
26+
beforeEach(() => {
27+
global.fetch.mockResolvedValue({
28+
ok: true,
29+
json: () => Promise.resolve({ imap_connections: [] }),
30+
});
31+
});
32+
33+
it('shows the "New IMAP Connection" accordion when no connections exist', async () => {
34+
renderWithProviders(
35+
<AddImapConnectionForm onChangeCallback={vi.fn()} activeOrganizationId="1" />,
36+
{ appContext: { localeMessages } }
37+
);
38+
await waitFor(() =>
39+
expect(screen.getByText('New IMAP Connection')).toBeInTheDocument()
40+
);
41+
});
42+
43+
it('shows existing connections as a labeled select when they exist', async () => {
44+
global.fetch.mockResolvedValue({
45+
ok: true,
46+
json: () =>
47+
Promise.resolve({
48+
imap_connections: [{ id: '5', email: 'mail@server.com' }],
49+
}),
50+
});
51+
renderWithProviders(
52+
<AddImapConnectionForm onChangeCallback={vi.fn()} activeOrganizationId="1" />,
53+
{ appContext: { localeMessages } }
54+
);
55+
await waitFor(() =>
56+
expect(screen.getByLabelText('IMAP Connection')).toBeInTheDocument()
57+
);
58+
});
59+
60+
it('calls onChangeCallback when a new connection is created', async () => {
61+
const onChangeCallback = vi.fn();
62+
global.fetch.mockImplementation((url) => {
63+
if (url.includes('imap-connections') && !url.includes('POST')) {
64+
return Promise.resolve({
65+
ok: true,
66+
json: () => Promise.resolve({ imap_connections: [] }),
67+
});
68+
}
69+
return Promise.resolve({
70+
ok: true,
71+
json: () => Promise.resolve({ id: '10', email: 'new@server.com' }),
72+
});
73+
});
74+
renderWithProviders(
75+
<AddImapConnectionForm onChangeCallback={onChangeCallback} activeOrganizationId="1" />,
76+
{ appContext: { localeMessages } }
77+
);
78+
// Verify the accordion is expanded and form is available
79+
await waitFor(() =>
80+
expect(screen.getByText('New IMAP Connection')).toBeInTheDocument()
81+
);
82+
});
83+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { screen, waitFor } from '@testing-library/react';
3+
import { renderWithProviders } from '../test-utils';
4+
import AddInstructorsSection from '../../../platform/courses/components/AddInstructorsSection';
5+
6+
vi.mock('../../render.jsx');
7+
8+
const localeMessages = {
9+
select_instructors: 'Select Instructors',
10+
new_instructor: 'New Instructor',
11+
instructor_email: 'Instructor Email',
12+
instructor_display_name: 'Display Name',
13+
instructor_photo: 'Photo',
14+
add_instructor: 'Add Instructor',
15+
email_required_helper_text: 'Email is required.',
16+
invalid_email_helper_text: 'Invalid email.',
17+
instructor_display_name_required: 'Display name is required.',
18+
instructor_add_failed: 'Failed to add instructor.',
19+
upload_button_label: 'Upload',
20+
uploaded_image_alt: 'Uploaded image',
21+
remove_image: 'Remove',
22+
};
23+
24+
describe('AddInstructorsSection', () => {
25+
beforeEach(() => {
26+
global.fetch.mockResolvedValue({
27+
ok: true,
28+
json: () => Promise.resolve({ organization_users: [] }),
29+
});
30+
});
31+
32+
it('shows the "New Instructor" accordion when no instructors exist', async () => {
33+
renderWithProviders(
34+
<AddInstructorsSection onChangeCallback={vi.fn()} activeOrganizationId="1" />,
35+
{ appContext: { localeMessages } }
36+
);
37+
await waitFor(() =>
38+
expect(screen.getByText('New Instructor')).toBeInTheDocument()
39+
);
40+
});
41+
42+
it('shows instructor select dropdown when instructors exist', async () => {
43+
global.fetch.mockResolvedValue({
44+
ok: true,
45+
json: () =>
46+
Promise.resolve({
47+
organization_users: [
48+
{ id: '3', email: 'prof@example.com', display_name: 'Prof. Jones', can_act_as_instructor: true },
49+
],
50+
}),
51+
});
52+
renderWithProviders(
53+
<AddInstructorsSection onChangeCallback={vi.fn()} activeOrganizationId="1" />,
54+
{ appContext: { localeMessages } }
55+
);
56+
await waitFor(() =>
57+
expect(screen.getByLabelText('Select Instructors')).toBeInTheDocument()
58+
);
59+
});
60+
61+
it('pre-selects initial instructor IDs', async () => {
62+
global.fetch.mockResolvedValue({
63+
ok: true,
64+
json: () =>
65+
Promise.resolve({
66+
organization_users: [
67+
{ id: '3', email: 'prof@example.com', display_name: 'Prof. Jones', can_act_as_instructor: true },
68+
],
69+
}),
70+
});
71+
renderWithProviders(
72+
<AddInstructorsSection
73+
onChangeCallback={vi.fn()}
74+
activeOrganizationId="1"
75+
initialInstructorIds={['3']}
76+
/>,
77+
{ appContext: { localeMessages } }
78+
);
79+
await waitFor(() =>
80+
expect(screen.getAllByText('Prof. Jones').length).toBeGreaterThan(0)
81+
);
82+
});
83+
});
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { screen, waitFor } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { renderWithProviders } from '../test-utils';
5+
import ApiKeys from '../../../platform/settings_api_keys/ApiKeys';
6+
7+
vi.mock('../../render.jsx');
8+
9+
vi.mock('@mui/material', async () => ({
10+
...(await vi.importActual('@mui/material')),
11+
useMediaQuery: vi.fn(() => true),
12+
}));
13+
14+
function setupFetch(apiKeys = []) {
15+
global.fetch.mockImplementation((url) => {
16+
if (url.includes('/api_keys/')) {
17+
return Promise.resolve({
18+
ok: true,
19+
json: () => Promise.resolve({ api_keys: apiKeys }),
20+
});
21+
}
22+
if (url.includes('/status/jobs/')) {
23+
return Promise.resolve({
24+
ok: true,
25+
json: () => Promise.resolve({ jobs: { deliver_contents: null } }),
26+
});
27+
}
28+
if (url.includes('/organizations/')) {
29+
return Promise.resolve({
30+
ok: true,
31+
json: () => Promise.resolve({ organizations: [] }),
32+
});
33+
}
34+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
35+
});
36+
}
37+
38+
const localeMessages = {
39+
api_keys: 'API Keys',
40+
api_key_intro: 'Use API keys to access the API.',
41+
add_api_key: 'Add API Key',
42+
key: 'Key',
43+
created_by: 'Created By',
44+
created_at: 'Created At',
45+
actions: 'Actions',
46+
copy: 'Copy',
47+
confirm_deletion: 'Confirm Deletion',
48+
are_you_sure_delete_key: 'Are you sure you want to delete this API key?',
49+
cancel: 'Cancel',
50+
delete: 'Delete',
51+
organizations: 'Organizations',
52+
course_management: 'Courses',
53+
learners: 'Learners',
54+
content_delivery_tooltip: 'Content delivery',
55+
content_delivery_job: 'Content delivery',
56+
last_run: 'Last run:',
57+
never_run: 'Never run',
58+
upload_button_label: 'Upload',
59+
uploaded_image_alt: 'Uploaded image',
60+
remove_image: 'Remove',
61+
};
62+
63+
describe('ApiKeys', () => {
64+
beforeEach(() => {
65+
setupFetch();
66+
});
67+
68+
it('renders the intro text', async () => {
69+
renderWithProviders(<ApiKeys />, {
70+
appContext: { localeMessages, isPlatformAdmin: true },
71+
});
72+
await waitFor(() =>
73+
expect(screen.getByText('Use API keys to access the API.')).toBeInTheDocument()
74+
);
75+
});
76+
77+
it('renders the Add API Key button', async () => {
78+
renderWithProviders(<ApiKeys />, {
79+
appContext: { localeMessages, isPlatformAdmin: true },
80+
});
81+
await waitFor(() =>
82+
expect(screen.getByRole('button', { name: /Add API Key/ })).toBeInTheDocument()
83+
);
84+
});
85+
86+
it('shows the key table after loading keys', async () => {
87+
setupFetch([
88+
{ id: '1', key: 'abc123', created_by: 'admin', created_at: '2024-01-01', visible: false },
89+
]);
90+
renderWithProviders(<ApiKeys />, {
91+
appContext: { localeMessages, isPlatformAdmin: true },
92+
});
93+
await waitFor(() => expect(screen.getByText('admin')).toBeInTheDocument());
94+
expect(screen.getByText('2024-01-01')).toBeInTheDocument();
95+
});
96+
97+
it('adds a new API key when Add API Key is clicked', async () => {
98+
const user = userEvent.setup();
99+
global.fetch.mockImplementation((url, options) => {
100+
if (url.includes('/api_keys/') && options?.method === 'POST') {
101+
return Promise.resolve({
102+
ok: true,
103+
json: () => Promise.resolve({
104+
id: '2', key: 'new-key-xyz', created_by: 'admin', created_at: '2024-06-01', visible: false,
105+
}),
106+
});
107+
}
108+
if (url.includes('/api_keys/')) {
109+
return Promise.resolve({
110+
ok: true,
111+
json: () => Promise.resolve({ api_keys: [] }),
112+
});
113+
}
114+
if (url.includes('/status/jobs/')) {
115+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ jobs: { deliver_contents: null } }) });
116+
}
117+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ organizations: [] }) });
118+
});
119+
renderWithProviders(<ApiKeys />, {
120+
appContext: { localeMessages, isPlatformAdmin: true },
121+
});
122+
await waitFor(() =>
123+
expect(screen.getByRole('button', { name: /Add API Key/ })).toBeInTheDocument()
124+
);
125+
await user.click(screen.getByRole('button', { name: /Add API Key/ }));
126+
await waitFor(() => expect(screen.getByText('2024-06-01')).toBeInTheDocument());
127+
});
128+
129+
it('shows delete confirmation dialog when delete icon is clicked', async () => {
130+
setupFetch([
131+
{ id: '1', key: 'abc123', created_by: 'admin', created_at: '2024-01-01', visible: false },
132+
]);
133+
const user = userEvent.setup();
134+
renderWithProviders(<ApiKeys />, {
135+
appContext: { localeMessages, isPlatformAdmin: true },
136+
});
137+
await waitFor(() => expect(screen.getByText('admin')).toBeInTheDocument());
138+
// Find the button containing the DeleteIcon svg
139+
const deleteButton = screen.getAllByRole('button').find(
140+
(btn) => btn.querySelector('[data-testid="DeleteIcon"]')
141+
);
142+
await user.click(deleteButton);
143+
await waitFor(() =>
144+
expect(screen.getByText('Confirm Deletion')).toBeInTheDocument()
145+
);
146+
});
147+
});

0 commit comments

Comments
 (0)