Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions django_email_learning/personalised/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-unty
"django_email_learning:api_personalised:submit_certificate_form"
),
"localeMessages": {
"form_title": _("Certificate of Completion"),
"form_intro": _(
"Congratulations on completing the course! To issue your certificate, please enter the name you would like displayed on it."
),
Expand Down
2 changes: 2 additions & 0 deletions frontend/personalised/assignment_public/Assignment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,6 @@ const Assignment = () => {
};


export { Assignment };

render({ children: <Assignment /> });
2 changes: 2 additions & 0 deletions frontend/personalised/certificate/Certificate.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,6 @@ const CertificateContent = () => {
}


export { Certificate };

render({children: <Certificate />});
2 changes: 2 additions & 0 deletions frontend/personalised/certificate_form/CertificateForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,6 @@ const CertificateForm = () => {
</Box>);
};

export { CertificateForm };

render({children: <CertificateForm />});
1 change: 1 addition & 0 deletions frontend/personalised/quiz_public/Quiz.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,5 @@ const Quiz = () => {
</Layout>
}

export { Quiz };
render({children: <Quiz />});
146 changes: 146 additions & 0 deletions frontend/src/test/Assignment.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import { renderWithProviders } from './test-utils';
import { Assignment } from '../../personalised/assignment_public/Assignment.jsx';

vi.mock('../render.jsx');

const sampleAssignment = {
id: 1,
title: 'Write a Report',
description: 'Write a short report about React.',
requires_text_submission: true,
requires_file_submission: false,
};

const sampleLocaleMessages = {
text_submission_label: 'Your Answer',
file_submission_label: 'Upload Your File',
submission_success: 'Your assignment has been submitted successfully!',
submission_error: 'An error occurred while submitting your assignment.',
submit: 'Submit',
close_window_message: 'You can now close this window!',
text_submission_required: 'Text submission is required.',
file_submission_required: 'File submission is required.',
error: 'Error',
};

const defaultAppContext = {
assignment: sampleAssignment,
token: 'test-token',
csrfToken: 'csrf-token',
apiEndpoint: '/api/assignment/submit/',
fileUploadApiEndpoint: '/api/file/upload/',
localeMessages: sampleLocaleMessages,
direction: 'ltr',
};

describe('Assignment', () => {
beforeEach(() => {
global.fetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ message: 'Submitted!' }),
});
});

it('renders the assignment title', () => {
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
expect(screen.getByText('Write a Report')).toBeInTheDocument();
});

it('renders the assignment description', () => {
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
expect(screen.getByText('Write a short report about React.')).toBeInTheDocument();
});

it('renders the text submission field when requires_text_submission is true', () => {
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
expect(screen.getByLabelText('Your Answer')).toBeInTheDocument();
});

it('does not render the text submission field when requires_text_submission is false', () => {
const ctx = {
...defaultAppContext,
assignment: { ...sampleAssignment, requires_text_submission: false },
};
renderWithProviders(<Assignment />, { appContext: ctx });
expect(screen.queryByLabelText('Your Answer')).not.toBeInTheDocument();
});

it('renders the file upload section when requires_file_submission is true', () => {
const ctx = {
...defaultAppContext,
assignment: {
...sampleAssignment,
requires_text_submission: false,
requires_file_submission: true,
},
};
renderWithProviders(<Assignment />, { appContext: ctx });
const fileLabels = screen.getAllByText('Upload Your File');
expect(fileLabels.length).toBeGreaterThan(0);
});

it('renders the submit button', () => {
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
});

it('shows a validation error when text is required but empty', async () => {
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() =>
expect(screen.getByText('Text submission is required.')).toBeInTheDocument()
);
expect(global.fetch).not.toHaveBeenCalled();
});

it('shows success message after successful submission', async () => {
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
fireEvent.change(screen.getByLabelText('Your Answer'), {
target: { value: 'My answer text' },
});
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() =>
expect(screen.getByText('Submitted!')).toBeInTheDocument()
);
expect(screen.getByText('You can now close this window!')).toBeInTheDocument();
});

it('posts the text submission to the api endpoint', async () => {
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
fireEvent.change(screen.getByLabelText('Your Answer'), {
target: { value: 'My answer' },
});
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() => expect(global.fetch).toHaveBeenCalledOnce());
const [url, options] = global.fetch.mock.calls[0];
expect(url).toBe('/api/assignment/submit/');
expect(options.method).toBe('POST');
expect(options.headers['X-CSRFToken']).toBe('csrf-token');
const body = JSON.parse(options.body);
expect(body.text_submission).toBe('My answer');
expect(body.token).toBe('test-token');
});

it('shows an error alert when submission fails', async () => {
global.fetch.mockResolvedValue({
ok: false,
json: () => Promise.resolve({ error: 'Server error' }),
});
renderWithProviders(<Assignment />, { appContext: defaultAppContext });
fireEvent.change(screen.getByLabelText('Your Answer'), {
target: { value: 'My answer' },
});
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() => expect(screen.getByText('Server error')).toBeInTheDocument());
});

it('shows error alert when errorMessage is present', () => {
renderWithProviders(<Assignment />, {
appContext: { ...defaultAppContext, errorMessage: 'Link expired', ref: 'ref-xyz' },
});
expect(screen.getByText(/Link expired/)).toBeInTheDocument();
expect(screen.getByText(/ref-xyz/)).toBeInTheDocument();
});
});
79 changes: 79 additions & 0 deletions frontend/src/test/Certificate.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it, expect, vi } from 'vitest';
import { screen } from '@testing-library/react';
import { renderWithProviders } from './test-utils';
import { Certificate } from '../../personalised/certificate/Certificate.jsx';

vi.mock('../render.jsx');

const defaultAppContext = {
name: 'Jane Doe',
issueDate: 'January 01, 2025',
certificateNumber: 'ORG-COURSE-42-abc123',
qrcodeUrl: 'https://example.com/qr.png',
logoUrl: 'https://example.com/logo.png',
localeMessages: {
title: 'Certificate of Completion',
description: 'This certifies that Jane Doe has successfully completed the React Fundamentals course',
issue_date: 'Issued on',
certificate_number: 'Certificate Number',
organization_team: 'Acme Team',
},
};

describe('Certificate', () => {
it('renders the certificate title', () => {
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
expect(screen.getByText('Certificate of Completion')).toBeInTheDocument();
});

it('renders the recipient name in the description', () => {
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
expect(screen.getByText(/Jane Doe/)).toBeInTheDocument();
});

it('renders the issue date', () => {
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
expect(screen.getByText(/Issued on/)).toBeInTheDocument();
expect(screen.getByText(/January 01, 2025/)).toBeInTheDocument();
});

it('renders the certificate number', () => {
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
expect(screen.getByText(/Certificate Number/)).toBeInTheDocument();
expect(screen.getByText(/ORG-COURSE-42-abc123/)).toBeInTheDocument();
});

it('renders the organization team name', () => {
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
expect(screen.getByText('Acme Team')).toBeInTheDocument();
});

it('renders the QR code image', () => {
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
const qrImg = screen.getByAltText('QR Code');
expect(qrImg).toBeInTheDocument();
expect(qrImg).toHaveAttribute('src', 'https://example.com/qr.png');
});

it('renders the organization logo when provided', () => {
renderWithProviders(<Certificate />, { appContext: defaultAppContext });
const logoImg = screen.getByAltText('Organization Logo');
expect(logoImg).toBeInTheDocument();
expect(logoImg).toHaveAttribute('src', 'https://example.com/logo.png');
});

it('does not render the organization logo when logoUrl is empty', () => {
renderWithProviders(<Certificate />, {
appContext: { ...defaultAppContext, logoUrl: '' },
});
expect(screen.queryByAltText('Organization Logo')).not.toBeInTheDocument();
});

it('shows an error alert when errorMessage is present', () => {
renderWithProviders(<Certificate />, {
appContext: { ...defaultAppContext, errorMessage: 'Certificate not found' },
});
expect(screen.getByText('Certificate not found')).toBeInTheDocument();
expect(screen.queryByText('Certificate of Completion')).not.toBeInTheDocument();
});
});
123 changes: 123 additions & 0 deletions frontend/src/test/CertificateForm.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import { renderWithProviders } from './test-utils';
import { CertificateForm } from '../../personalised/certificate_form/CertificateForm.jsx';

vi.mock('../render.jsx');

const sampleLocaleMessages = {
form_title: 'Certificate of Completion',
form_intro: 'Congratulations! Enter the name you would like on your certificate.',
full_name: 'Full Name',
full_name_required: 'Full Name is required',
error_sending_data: 'An error occurred while sending data. Please try again later.',
form_submission_success: 'Your certificate name has been submitted successfully!',
submit: 'Submit',
view_certificate: 'View Certificate',
};

const defaultAppContext = {
localeMessages: sampleLocaleMessages,
apiEndpoint: '/api/certificate/submit/',
token: 'test-token',
csrfToken: 'csrf-token',
};

describe('CertificateForm', () => {
beforeEach(() => {
global.fetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ certificate_url: '/certificates/ORG-COURSE-42-abc123/' }),
});
});

it('renders the form title', () => {
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
expect(screen.getByText('Certificate of Completion')).toBeInTheDocument();
});

it('renders the intro text', () => {
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
expect(
screen.getByText('Congratulations! Enter the name you would like on your certificate.')
).toBeInTheDocument();
});

it('renders the full name input field', () => {
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
expect(screen.getByLabelText(/Full Name/)).toBeInTheDocument();
});

it('renders the submit button', () => {
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
});

it('shows a validation error when name is empty', async () => {
const { container } = renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
fireEvent.submit(container.querySelector('form'));
await waitFor(() =>
expect(screen.getByText('Full Name is required')).toBeInTheDocument()
);
expect(global.fetch).not.toHaveBeenCalled();
});

it('submits the form with the entered name', async () => {
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
fireEvent.change(screen.getByLabelText(/Full Name/), {
target: { value: 'Jane Doe' },
});
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() => expect(global.fetch).toHaveBeenCalledOnce());
const [, options] = global.fetch.mock.calls[0];
const body = JSON.parse(options.body);
expect(body.name).toBe('Jane Doe');
expect(body.token).toBe('test-token');
expect(options.headers['X-CSRFToken']).toBe('csrf-token');
});

it('shows success alert after successful submission', async () => {
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
fireEvent.change(screen.getByLabelText(/Full Name/), {
target: { value: 'Jane Doe' },
});
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() =>
expect(
screen.getByText('Your certificate name has been submitted successfully!')
).toBeInTheDocument()
);
});

it('shows the View Certificate button after successful submission', async () => {
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
fireEvent.change(screen.getByLabelText(/Full Name/), {
target: { value: 'Jane Doe' },
});
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() =>
expect(screen.getByRole('link', { name: 'View Certificate' })).toBeInTheDocument()
);
expect(screen.getByRole('link', { name: 'View Certificate' })).toHaveAttribute(
'href',
'/certificates/ORG-COURSE-42-abc123/'
);
});

it('shows an error alert when the API call fails', async () => {
global.fetch.mockResolvedValue({
ok: false,
json: () => Promise.resolve({}),
});
renderWithProviders(<CertificateForm />, { appContext: defaultAppContext });
fireEvent.change(screen.getByLabelText(/Full Name/), {
target: { value: 'Jane Doe' },
});
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() =>
expect(
screen.getByText('An error occurred while sending data. Please try again later.')
).toBeInTheDocument()
);
});
});
Loading
Loading