Skip to content

Commit 57e9e9e

Browse files
authored
Merge pull request #413 from AvaCodeSolutions/copilot/create-personalised-pages
feat: tests and fixes for personalised frontend pages (Quiz, Assignment, Certificate)
2 parents c5831ec + 0f93006 commit 57e9e9e

10 files changed

Lines changed: 526 additions & 1 deletion

File tree

django_email_learning/personalised/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-unty
335335
"django_email_learning:api_personalised:submit_certificate_form"
336336
),
337337
"localeMessages": {
338+
"form_title": _("Certificate of Completion"),
338339
"form_intro": _(
339340
"Congratulations on completing the course! To issue your certificate, please enter the name you would like displayed on it."
340341
),

frontend/personalised/assignment_public/Assignment.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,6 @@ const Assignment = () => {
211211
};
212212

213213

214+
export { Assignment };
215+
214216
render({ children: <Assignment /> });

frontend/personalised/certificate/Certificate.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,6 @@ const CertificateContent = () => {
167167
}
168168

169169

170+
export { Certificate };
171+
170172
render({children: <Certificate />});

frontend/personalised/certificate_form/CertificateForm.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,6 @@ const CertificateForm = () => {
105105
</Box>);
106106
};
107107

108+
export { CertificateForm };
109+
108110
render({children: <CertificateForm />});

frontend/personalised/quiz_public/Quiz.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,5 @@ const Quiz = () => {
196196
</Layout>
197197
}
198198

199+
export { Quiz };
199200
render({children: <Quiz />});
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { screen, fireEvent, waitFor } from '@testing-library/react';
3+
import { renderWithProviders } from './test-utils';
4+
import { Assignment } from '../../personalised/assignment_public/Assignment.jsx';
5+
6+
vi.mock('../render.jsx');
7+
8+
const sampleAssignment = {
9+
id: 1,
10+
title: 'Write a Report',
11+
description: 'Write a short report about React.',
12+
requires_text_submission: true,
13+
requires_file_submission: false,
14+
};
15+
16+
const sampleLocaleMessages = {
17+
text_submission_label: 'Your Answer',
18+
file_submission_label: 'Upload Your File',
19+
submission_success: 'Your assignment has been submitted successfully!',
20+
submission_error: 'An error occurred while submitting your assignment.',
21+
submit: 'Submit',
22+
close_window_message: 'You can now close this window!',
23+
text_submission_required: 'Text submission is required.',
24+
file_submission_required: 'File submission is required.',
25+
error: 'Error',
26+
};
27+
28+
const defaultAppContext = {
29+
assignment: sampleAssignment,
30+
token: 'test-token',
31+
csrfToken: 'csrf-token',
32+
apiEndpoint: '/api/assignment/submit/',
33+
fileUploadApiEndpoint: '/api/file/upload/',
34+
localeMessages: sampleLocaleMessages,
35+
direction: 'ltr',
36+
};
37+
38+
describe('Assignment', () => {
39+
beforeEach(() => {
40+
global.fetch.mockResolvedValue({
41+
ok: true,
42+
json: () => Promise.resolve({ message: 'Submitted!' }),
43+
});
44+
});
45+
46+
it('renders the assignment title', () => {
47+
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
48+
expect(screen.getByText('Write a Report')).toBeInTheDocument();
49+
});
50+
51+
it('renders the assignment description', () => {
52+
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
53+
expect(screen.getByText('Write a short report about React.')).toBeInTheDocument();
54+
});
55+
56+
it('renders the text submission field when requires_text_submission is true', () => {
57+
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
58+
expect(screen.getByLabelText('Your Answer')).toBeInTheDocument();
59+
});
60+
61+
it('does not render the text submission field when requires_text_submission is false', () => {
62+
const ctx = {
63+
...defaultAppContext,
64+
assignment: { ...sampleAssignment, requires_text_submission: false },
65+
};
66+
renderWithProviders(<Assignment />, { appContext: ctx });
67+
expect(screen.queryByLabelText('Your Answer')).not.toBeInTheDocument();
68+
});
69+
70+
it('renders the file upload section when requires_file_submission is true', () => {
71+
const ctx = {
72+
...defaultAppContext,
73+
assignment: {
74+
...sampleAssignment,
75+
requires_text_submission: false,
76+
requires_file_submission: true,
77+
},
78+
};
79+
renderWithProviders(<Assignment />, { appContext: ctx });
80+
const fileLabels = screen.getAllByText('Upload Your File');
81+
expect(fileLabels.length).toBeGreaterThan(0);
82+
});
83+
84+
it('renders the submit button', () => {
85+
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
86+
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
87+
});
88+
89+
it('shows a validation error when text is required but empty', async () => {
90+
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
91+
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
92+
await waitFor(() =>
93+
expect(screen.getByText('Text submission is required.')).toBeInTheDocument()
94+
);
95+
expect(global.fetch).not.toHaveBeenCalled();
96+
});
97+
98+
it('shows success message after successful submission', async () => {
99+
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
100+
fireEvent.change(screen.getByLabelText('Your Answer'), {
101+
target: { value: 'My answer text' },
102+
});
103+
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
104+
await waitFor(() =>
105+
expect(screen.getByText('Submitted!')).toBeInTheDocument()
106+
);
107+
expect(screen.getByText('You can now close this window!')).toBeInTheDocument();
108+
});
109+
110+
it('posts the text submission to the api endpoint', async () => {
111+
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
112+
fireEvent.change(screen.getByLabelText('Your Answer'), {
113+
target: { value: 'My answer' },
114+
});
115+
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
116+
await waitFor(() => expect(global.fetch).toHaveBeenCalledOnce());
117+
const [url, options] = global.fetch.mock.calls[0];
118+
expect(url).toBe('/api/assignment/submit/');
119+
expect(options.method).toBe('POST');
120+
expect(options.headers['X-CSRFToken']).toBe('csrf-token');
121+
const body = JSON.parse(options.body);
122+
expect(body.text_submission).toBe('My answer');
123+
expect(body.token).toBe('test-token');
124+
});
125+
126+
it('shows an error alert when submission fails', async () => {
127+
global.fetch.mockResolvedValue({
128+
ok: false,
129+
json: () => Promise.resolve({ error: 'Server error' }),
130+
});
131+
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
132+
fireEvent.change(screen.getByLabelText('Your Answer'), {
133+
target: { value: 'My answer' },
134+
});
135+
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
136+
await waitFor(() => expect(screen.getByText('Server error')).toBeInTheDocument());
137+
});
138+
139+
it('shows error alert when errorMessage is present', () => {
140+
renderWithProviders(<Assignment />, {
141+
appContext: { ...defaultAppContext, errorMessage: 'Link expired', ref: 'ref-xyz' },
142+
});
143+
expect(screen.getByText(/Link expired/)).toBeInTheDocument();
144+
expect(screen.getByText(/ref-xyz/)).toBeInTheDocument();
145+
});
146+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { screen } from '@testing-library/react';
3+
import { renderWithProviders } from './test-utils';
4+
import { Certificate } from '../../personalised/certificate/Certificate.jsx';
5+
6+
vi.mock('../render.jsx');
7+
8+
const defaultAppContext = {
9+
name: 'Jane Doe',
10+
issueDate: 'January 01, 2025',
11+
certificateNumber: 'ORG-COURSE-42-abc123',
12+
qrcodeUrl: 'https://example.com/qr.png',
13+
logoUrl: 'https://example.com/logo.png',
14+
localeMessages: {
15+
title: 'Certificate of Completion',
16+
description: 'This certifies that Jane Doe has successfully completed the React Fundamentals course',
17+
issue_date: 'Issued on',
18+
certificate_number: 'Certificate Number',
19+
organization_team: 'Acme Team',
20+
},
21+
};
22+
23+
describe('Certificate', () => {
24+
it('renders the certificate title', () => {
25+
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
26+
expect(screen.getByText('Certificate of Completion')).toBeInTheDocument();
27+
});
28+
29+
it('renders the recipient name in the description', () => {
30+
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
31+
expect(screen.getByText(/Jane Doe/)).toBeInTheDocument();
32+
});
33+
34+
it('renders the issue date', () => {
35+
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
36+
expect(screen.getByText(/Issued on/)).toBeInTheDocument();
37+
expect(screen.getByText(/January 01, 2025/)).toBeInTheDocument();
38+
});
39+
40+
it('renders the certificate number', () => {
41+
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
42+
expect(screen.getByText(/Certificate Number/)).toBeInTheDocument();
43+
expect(screen.getByText(/ORG-COURSE-42-abc123/)).toBeInTheDocument();
44+
});
45+
46+
it('renders the organization team name', () => {
47+
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
48+
expect(screen.getByText('Acme Team')).toBeInTheDocument();
49+
});
50+
51+
it('renders the QR code image', () => {
52+
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
53+
const qrImg = screen.getByAltText('QR Code');
54+
expect(qrImg).toBeInTheDocument();
55+
expect(qrImg).toHaveAttribute('src', 'https://example.com/qr.png');
56+
});
57+
58+
it('renders the organization logo when provided', () => {
59+
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
60+
const logoImg = screen.getByAltText('Organization Logo');
61+
expect(logoImg).toBeInTheDocument();
62+
expect(logoImg).toHaveAttribute('src', 'https://example.com/logo.png');
63+
});
64+
65+
it('does not render the organization logo when logoUrl is empty', () => {
66+
renderWithProviders(<Certificate />, {
67+
appContext: { ...defaultAppContext, logoUrl: '' },
68+
});
69+
expect(screen.queryByAltText('Organization Logo')).not.toBeInTheDocument();
70+
});
71+
72+
it('shows an error alert when errorMessage is present', () => {
73+
renderWithProviders(<Certificate />, {
74+
appContext: { ...defaultAppContext, errorMessage: 'Certificate not found' },
75+
});
76+
expect(screen.getByText('Certificate not found')).toBeInTheDocument();
77+
expect(screen.queryByText('Certificate of Completion')).not.toBeInTheDocument();
78+
});
79+
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { screen, fireEvent, waitFor } from '@testing-library/react';
3+
import { renderWithProviders } from './test-utils';
4+
import { CertificateForm } from '../../personalised/certificate_form/CertificateForm.jsx';
5+
6+
vi.mock('../render.jsx');
7+
8+
const sampleLocaleMessages = {
9+
form_title: 'Certificate of Completion',
10+
form_intro: 'Congratulations! Enter the name you would like on your certificate.',
11+
full_name: 'Full Name',
12+
full_name_required: 'Full Name is required',
13+
error_sending_data: 'An error occurred while sending data. Please try again later.',
14+
form_submission_success: 'Your certificate name has been submitted successfully!',
15+
submit: 'Submit',
16+
view_certificate: 'View Certificate',
17+
};
18+
19+
const defaultAppContext = {
20+
localeMessages: sampleLocaleMessages,
21+
apiEndpoint: '/api/certificate/submit/',
22+
token: 'test-token',
23+
csrfToken: 'csrf-token',
24+
};
25+
26+
describe('CertificateForm', () => {
27+
beforeEach(() => {
28+
global.fetch.mockResolvedValue({
29+
ok: true,
30+
json: () => Promise.resolve({ certificate_url: '/certificates/ORG-COURSE-42-abc123/' }),
31+
});
32+
});
33+
34+
it('renders the form title', () => {
35+
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
36+
expect(screen.getByText('Certificate of Completion')).toBeInTheDocument();
37+
});
38+
39+
it('renders the intro text', () => {
40+
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
41+
expect(
42+
screen.getByText('Congratulations! Enter the name you would like on your certificate.')
43+
).toBeInTheDocument();
44+
});
45+
46+
it('renders the full name input field', () => {
47+
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
48+
expect(screen.getByLabelText(/Full Name/)).toBeInTheDocument();
49+
});
50+
51+
it('renders the submit button', () => {
52+
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
53+
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
54+
});
55+
56+
it('shows a validation error when name is empty', async () => {
57+
const { container } = renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
58+
fireEvent.submit(container.querySelector('form'));
59+
await waitFor(() =>
60+
expect(screen.getByText('Full Name is required')).toBeInTheDocument()
61+
);
62+
expect(global.fetch).not.toHaveBeenCalled();
63+
});
64+
65+
it('submits the form with the entered name', async () => {
66+
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
67+
fireEvent.change(screen.getByLabelText(/Full Name/), {
68+
target: { value: 'Jane Doe' },
69+
});
70+
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
71+
await waitFor(() => expect(global.fetch).toHaveBeenCalledOnce());
72+
const [, options] = global.fetch.mock.calls[0];
73+
const body = JSON.parse(options.body);
74+
expect(body.name).toBe('Jane Doe');
75+
expect(body.token).toBe('test-token');
76+
expect(options.headers['X-CSRFToken']).toBe('csrf-token');
77+
});
78+
79+
it('shows success alert after successful submission', async () => {
80+
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
81+
fireEvent.change(screen.getByLabelText(/Full Name/), {
82+
target: { value: 'Jane Doe' },
83+
});
84+
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
85+
await waitFor(() =>
86+
expect(
87+
screen.getByText('Your certificate name has been submitted successfully!')
88+
).toBeInTheDocument()
89+
);
90+
});
91+
92+
it('shows the View Certificate button after successful submission', async () => {
93+
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
94+
fireEvent.change(screen.getByLabelText(/Full Name/), {
95+
target: { value: 'Jane Doe' },
96+
});
97+
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
98+
await waitFor(() =>
99+
expect(screen.getByRole('link', { name: 'View Certificate' })).toBeInTheDocument()
100+
);
101+
expect(screen.getByRole('link', { name: 'View Certificate' })).toHaveAttribute(
102+
'href',
103+
'/certificates/ORG-COURSE-42-abc123/'
104+
);
105+
});
106+
107+
it('shows an error alert when the API call fails', async () => {
108+
global.fetch.mockResolvedValue({
109+
ok: false,
110+
json: () => Promise.resolve({}),
111+
});
112+
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
113+
fireEvent.change(screen.getByLabelText(/Full Name/), {
114+
target: { value: 'Jane Doe' },
115+
});
116+
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
117+
await waitFor(() =>
118+
expect(
119+
screen.getByText('An error occurred while sending data. Please try again later.')
120+
).toBeInTheDocument()
121+
);
122+
});
123+
});

0 commit comments

Comments
 (0)