Merge remote-tracking branch 'test-1/main' into restore-reverted-sso-pr

This commit is contained in:
Eddi3_As 2025-02-28 13:13:03 -05:00
commit 3a2baaaa1c
12 changed files with 201 additions and 160 deletions

View file

@ -3,66 +3,87 @@ import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview'; import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
const validQuestions = [
'::TFTitle::[markdown]Troo statement {TRUE}',
'::SATitle::[markdown]What is the answer? {=ShortAnswerOne =ShortAnswerTwo}',
'::MCQTitle::[markdown]MultiChoice question? {=MQAnswerOne ~MQAnswerTwo#feedback####Gen feedback}',
];
const unsupportedQuestions = [
'::Title::[markdown]Essay {}',
'::Title::[markdown]Matching {}',
'::Title::[markdown]Description',
'$CATEGORY a/b/c'
];
describe('GIFTTemplatePreview Component', () => { describe('GIFTTemplatePreview Component', () => {
test('renders error message when questions contain invalid syntax', () => { it('renders error message when questions contain invalid syntax', () => {
render(<GIFTTemplatePreview questions={[':: title']} />); render(<GIFTTemplatePreview questions={['T{']} hideAnswers={false} />);
const errorMessage = screen.getByText(/Title ::, Category, Description, or Question formatted stem but ":" found./i); const previewContainer = screen.getByTestId('preview-container');
expect(previewContainer).toBeInTheDocument();
const errorMessage = previewContainer.querySelector('div[label="error-message"]');
expect(errorMessage).toBeInTheDocument(); expect(errorMessage).toBeInTheDocument();
}); });
test('renders preview when valid questions are provided', () => { it('renders preview when valid questions are provided, including answers, has no errors', () => {
const questions = [ render(<GIFTTemplatePreview questions={validQuestions} hideAnswers={false} />);
'Stem1 {=ans1 ~ans2 ~ans3}',
];
render(<GIFTTemplatePreview questions={questions} />);
const previewContainer = screen.getByTestId('preview-container'); const previewContainer = screen.getByTestId('preview-container');
expect(previewContainer).toBeInTheDocument(); expect(previewContainer).toBeInTheDocument();
// const question1 = screen.getByText('Stem1'); // Check that all question titles are rendered inside the previewContainer
const mcQuestion1 = screen.getByText('Stem1'); validQuestions.forEach((question) => {
const ans1 = screen.getByText('ans1'); const title = question.split('::')[1].split('::')[0];
const ans2 = screen.getByText('ans2'); expect(previewContainer).toHaveTextContent(title);
const ans3 = screen.getByText('ans3'); });
expect(mcQuestion1).toBeInTheDocument(); // There should be no errors
expect(ans1).toBeInTheDocument(); const errorMessage = previewContainer.querySelector('div[label="error-message"]');
expect(ans2).toBeInTheDocument(); expect(errorMessage).not.toBeInTheDocument();
expect(ans3).toBeInTheDocument(); // Check that some stems and answers are rendered inside the previewContainer
expect(previewContainer).toHaveTextContent('Troo statement');
expect(previewContainer).toHaveTextContent('What is the answer?');
expect(previewContainer).toHaveTextContent('MultiChoice question?');
expect(previewContainer).toHaveTextContent('Vrai');
// short answers are stored in a textbox
const answerInputElements = screen.getAllByRole('textbox');
const giftInputElements = answerInputElements.filter(element => element.classList.contains('gift-input'));
// each answer should have a radio button before it expect(giftInputElements).toHaveLength(1);
const radioButtons = screen.getAllByRole('radio'); expect(giftInputElements[0]).toHaveAttribute('placeholder', 'ShortAnswerOne, ShortAnswerTwo');
expect(radioButtons).toHaveLength(3);
// ans1 should be the <label> for the first radio button
expect(radioButtons[0].nextElementSibling).toBe(ans1);
// ans2 should be the <label> for the second radio button
expect(radioButtons[1].nextElementSibling).toBe(ans2);
// ans3 should be the <label> for the third radio button
expect(radioButtons[2].nextElementSibling).toBe(ans3);
// after the <label> for correct answer (ans1) there should be an svg with aria-hidden="true" // Check for correct answer icon just after MQAnswerOne
expect(ans1.nextElementSibling).toHaveAttribute('aria-hidden', 'true'); const mqAnswerOneElement = screen.getByText('MQAnswerOne');
// after the <label> for incorrect answer (ans2) there should be an svg with aria-hidden="true" const correctAnswerIcon = mqAnswerOneElement.parentElement?.querySelector('[data-testid="correct-icon"]');
expect(ans2.nextElementSibling).toHaveAttribute('aria-hidden', 'true'); expect(correctAnswerIcon).toBeInTheDocument();
// after the <label> for incorrect answer (ans3) there should be an svg with aria-hidden="true"
expect(ans3.nextElementSibling).toHaveAttribute('aria-hidden', 'true');
}); // Check for incorrect answer icon just after MQAnswerTwo
test('hides correct/incorrect answers when hideAnswers prop is true', () => { const mqAnswerTwoElement = screen.getByText('MQAnswerTwo');
const questions = [ const incorrectAnswerIcon = mqAnswerTwoElement.parentElement?.querySelector('[data-testid="incorrect-icon"]');
'Stem1 {=ans1 ~ans2 ~ans3}', expect(incorrectAnswerIcon).toBeInTheDocument();
]; });
render(<GIFTTemplatePreview questions={questions} hideAnswers />);
it('hides answers when hideAnswers prop is true', () => {
render(<GIFTTemplatePreview questions={validQuestions} hideAnswers={true} />);
const previewContainer = screen.getByTestId('preview-container'); const previewContainer = screen.getByTestId('preview-container');
expect(previewContainer).toBeInTheDocument(); expect(previewContainer).toBeInTheDocument();
const ans1 = screen.queryByText('ans1'); expect(previewContainer).toHaveTextContent('Troo statement');
const ans2 = screen.queryByText('ans2'); expect(previewContainer).toHaveTextContent('What is the answer?');
const ans3 = screen.queryByText('ans3'); expect(previewContainer).toHaveTextContent('MultiChoice question?');
expect(previewContainer).toHaveTextContent('Vrai');
const radioButtons = screen.getAllByRole('radio'); expect(previewContainer).not.toHaveTextContent('ShortAnswerOne');
expect(radioButtons).toHaveLength(3); expect(previewContainer).not.toHaveTextContent('ShortAnswerTwo');
expect(radioButtons[0].nextElementSibling).toBe(ans1); // shouldn't have correct/incorrect icons
expect(ans1?.nextElementSibling).toBeNull(); const correctAnswerIcon = screen.queryByTestId('correct-icon');
expect(radioButtons[1].nextElementSibling).toBe(ans2); expect(correctAnswerIcon).not.toBeInTheDocument();
expect(ans2?.nextElementSibling).toBeNull(); const incorrectAnswerIcon = screen.queryByTestId('incorrect-icon');
expect(radioButtons[2].nextElementSibling).toBe(ans3); expect(incorrectAnswerIcon).not.toBeInTheDocument();
expect(ans3?.nextElementSibling).toBeNull();
}); });
it('should indicate in the preview that unsupported GIFT questions are not supported', () => {
render(<GIFTTemplatePreview questions={unsupportedQuestions} hideAnswers={false} />);
const previewContainer = screen.getByTestId('preview-container');
expect(previewContainer).toBeInTheDocument();
// find all unsupported errors (should be 4)
const unsupportedMessages = previewContainer.querySelectorAll('div[label="error-message"]');
expect(unsupportedMessages).toHaveLength(4);
});
}); });

View file

@ -64,7 +64,7 @@ exports[`MultipleChoice snapshot test 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Choice 1 Choice 1
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -88,7 +88,7 @@ exports[`MultipleChoice snapshot test 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Choice 2 Choice 2
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -180,7 +180,7 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
Choice 1 Choice 1
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -205,7 +205,7 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
Choice 2 Choice 2
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -229,7 +229,7 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
&lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt; &lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt;
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -321,7 +321,7 @@ exports[`MultipleChoice snapshot test with Moodle text format 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Choice 1 Choice 1
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -345,7 +345,7 @@ exports[`MultipleChoice snapshot test with Moodle text format 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Choice 2 Choice 2
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -436,7 +436,7 @@ exports[`MultipleChoice snapshot test with image 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Choice 1 Choice 1
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -460,7 +460,7 @@ exports[`MultipleChoice snapshot test with image 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Choice 2 Choice 2
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -484,7 +484,7 @@ exports[`MultipleChoice snapshot test with image 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
&lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt; &lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt;
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -577,7 +577,7 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
Choice 1 Choice 1
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -602,7 +602,7 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
Choice 2 Choice 2
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -626,7 +626,7 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
&lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt; &lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt;
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -718,7 +718,7 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Choice 1 Choice 1
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -742,7 +742,7 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Choice 2 Choice 2
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -833,7 +833,7 @@ exports[`MultipleChoice snapshot test with katex, using html text format 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Choice 1 Choice 1
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -857,7 +857,7 @@ exports[`MultipleChoice snapshot test with katex, using html text format 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Choice 2 Choice 2
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;

View file

@ -150,7 +150,7 @@ exports[`TrueFalse snapshot test with katex 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Vrai Vrai
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -174,7 +174,7 @@ exports[`TrueFalse snapshot test with katex 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Faux Faux
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -265,7 +265,7 @@ exports[`TrueFalse snapshot test with moodle 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Vrai Vrai
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -289,7 +289,7 @@ exports[`TrueFalse snapshot test with moodle 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Faux Faux
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -380,7 +380,7 @@ exports[`TrueFalse snapshot test with plain text 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Vrai Vrai
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="correct-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
@ -404,7 +404,7 @@ exports[`TrueFalse snapshot test with plain text 1`] = `
" for="idmocked-id"&gt; " for="idmocked-id"&gt;
Faux Faux
&lt;/label&gt; &lt;/label&gt;
&lt;svg style=" &lt;svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;

View file

@ -1,6 +1,6 @@
// GIFTTemplatePreview.tsx // GIFTTemplatePreview.tsx
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Template, { ErrorTemplate } from './templates'; import Template, { ErrorTemplate, UnsupportedQuestionTypeError } from './templates';
import { parse } from 'gift-pegjs'; import { parse } from 'gift-pegjs';
import './styles.css'; import './styles.css';
import { FormattedTextTemplate } from './templates/TextTypeTemplate'; import { FormattedTextTemplate } from './templates/TextTypeTemplate';
@ -22,19 +22,6 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
try { try {
let previewHTML = ''; let previewHTML = '';
questions.forEach((giftQuestion) => { questions.forEach((giftQuestion) => {
// TODO : afficher un message que les images spécifiées par <img> sont dépréciées et qu'il faut utiliser [markdown] et la syntaxe ![alt](url)
// const isImage = item.includes('<img');
// if (isImage) {
// const imageUrlMatch = item.match(/<img[^>]+>/i);
// if (imageUrlMatch) {
// let imageUrl = imageUrlMatch[0];
// imageUrl = imageUrl.replace('img', 'img style="width:10vw;" src=');
// item = item.replace(imageUrlMatch[0], '');
// previewHTML += `${imageUrl}`;
// }
// }
try { try {
const question = parse(giftQuestion); const question = parse(giftQuestion);
previewHTML += Template(question[0], { previewHTML += Template(question[0], {
@ -42,11 +29,15 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
theme: 'light' theme: 'light'
}); });
} catch (error) { } catch (error) {
if (error instanceof Error) { let errorMsg: string;
previewHTML += ErrorTemplate(giftQuestion + '\n' + error.message); if (error instanceof UnsupportedQuestionTypeError) {
errorMsg = ErrorTemplate(giftQuestion, `Erreur: ${error.message}`);
} else if (error instanceof Error) {
errorMsg = ErrorTemplate(giftQuestion, `Erreur GIFT: ${error.message}`);
} else { } else {
previewHTML += ErrorTemplate(giftQuestion + '\n' + 'Erreur inconnue'); errorMsg = ErrorTemplate(giftQuestion, 'Erreur inconnue');
} }
previewHTML += `<div label="error-message">${errorMsg}</div>`;
} }
}); });

View file

@ -18,11 +18,11 @@ What is the capital of Canada? {=Canada -> Ottawa =Italy -> Rome =Japan -> Tokyo
const items = multiple.map((item) => Template(item, { theme: 'dark' })).join(''); const items = multiple.map((item) => Template(item, { theme: 'dark' })).join('');
const errorItemDark = ErrorTemplate('Hello'); const errorItemDark = ErrorTemplate('Hello', 'Error');
const lightItems = multiple.map((item) => Template(item, { theme: 'light' })).join(''); const lightItems = multiple.map((item) => Template(item, { theme: 'light' })).join('');
const errorItem = ErrorTemplate('Hello'); const errorItem = ErrorTemplate('Hello', 'Error');
const app = document.getElementById('app'); const app = document.getElementById('app');
if (app) app.innerHTML = items + errorItemDark + lightItems + errorItem; if (app) app.innerHTML = items + errorItemDark + lightItems + errorItem;

View file

@ -25,11 +25,11 @@ export default function AnswerIcon({ correct }: AnswerIconOptions): string {
`; `;
const CorrectIcon = (): string => { const CorrectIcon = (): string => {
return `<svg style="${Icon} ${Correct}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>`; return `<svg data-testid="correct-icon" style="${Icon} ${Correct}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>`;
}; };
const IncorrectIcon = (): string => { const IncorrectIcon = (): string => {
return `<svg style="${Icon} ${Incorrect}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>`; return `<svg data-testid="incorrect-icon" style="${Icon} ${Incorrect}" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>`;
}; };
return correct ? CorrectIcon() : IncorrectIcon(); return correct ? CorrectIcon() : IncorrectIcon();

View file

@ -1,7 +1,7 @@
import { theme, ParagraphStyle } from '../constants'; import { theme, ParagraphStyle } from '../constants';
import { state } from '.'; import { state } from '.';
export default function (text: string): string { export default function (questionText: string, errorText: string): string {
const Container = ` const Container = `
flex-wrap: wrap; flex-wrap: wrap;
position: relative; position: relative;
@ -13,47 +13,49 @@ export default function (text: string): string {
box-shadow: 0px 1px 3px ${theme(state.theme, 'gray400', 'black900')}; box-shadow: 0px 1px 3px ${theme(state.theme, 'gray400', 'black900')};
`; `;
const document = removeBackslash(lineRegex(documentRegex(text))).split(/\r?\n/); // const document = removeBackslash(lineRegex(documentRegex(text))).split(/\r?\n/);
return document[0] !== `` // return document[0] !== ``
? `<section style="${Container}">${document // ? `<section style="${Container}">${document
.map((i) => `<p style="${ParagraphStyle(state.theme)}">${i}</p>`) // .map((i) => `<p style="${ParagraphStyle(state.theme)}">${i}</p>`)
.join('')}</section>` // .join('')}</section>`
: ``; // : ``;
return `<section style="${Container}"><p style="${ParagraphStyle(state.theme)}">${questionText}<br><em>${errorText}</em></p></section>`;
} }
function documentRegex(text: string): string { // function documentRegex(text: string): string {
const newText = text // const newText = text
.split(/\r?\n/) // .split(/\r?\n/)
.map((comment) => comment.replace(/(^[ \\t]+)?(^)((\/\/))(.*)/gm, '')) // .map((comment) => comment.replace(/(^[ \\t]+)?(^)((\/\/))(.*)/gm, ''))
.join(''); // .join('');
const newLineAnswer = /([^\\]|[^\S\r\n][^=])(=|~)/g; // const newLineAnswer = /([^\\]|[^\S\r\n][^=])(=|~)/g;
const correctAnswer = /([^\\]|^{)(([^\\]|^|\\s*)=(.*)(?=[=~}]|\\n))/g; // const correctAnswer = /([^\\]|^{)(([^\\]|^|\\s*)=(.*)(?=[=~}]|\\n))/g;
const incorrectAnswer = /([^\\]|^{)(([^\\]|^|\\s*)~(.*)(?=[=~}]|\\n))/g; // const incorrectAnswer = /([^\\]|^{)(([^\\]|^|\\s*)~(.*)(?=[=~}]|\\n))/g;
return newText // return newText
.replace(newLineAnswer, `\n$2`) // .replace(newLineAnswer, `\n$2`)
.replace(correctAnswer, `$1<li>$4</li>`) // .replace(correctAnswer, `$1<li>$4</li>`)
.replace(incorrectAnswer, `$1<li>$4</li>`); // .replace(incorrectAnswer, `$1<li>$4</li>`);
} // }
function lineRegex(text: string): string { // function lineRegex(text: string): string {
return text // return text
.split(/\r?\n/) // // CPF: disabled the following regex because it's not clear what it's supposed to do
.map((category) => // // .split(/\r?\n/)
category.replace(/(^[ \\t]+)?(((^|\n)\s*[$]CATEGORY:))(.+)/g, `<br><b>$5</b><br>`) // // .map((category) =>
) // // category.replace(/(^[ \\t]+)?(((^|\n)\s*[$]CATEGORY:))(.+)/g, `<br><b>$5</b><br>`)
.map((title) => title.replace(/\s*(::)\s*(.*?)(::)/g, `<br><b>$2</b><br>`)) // // )
.map((openBracket) => openBracket.replace(/([^\\]|^){([#])?/g, `$1<br>`)) // // .map((title) => title.replace(/\s*(::)\s*(.*?)(::)/g, `<br><b>$2</b><br>`))
.map((closeBracket) => closeBracket.replace(/([^\\]|^)}/g, `$1<br>`)) // // .map((openBracket) => openBracket.replace(/([^\\]|^){([#])?/g, `$1<br>`))
.join(''); // // .map((closeBracket) => closeBracket.replace(/([^\\]|^)}/g, `$1<br>`))
} // // .join('');
// }
function removeBackslash(text: string): string { // function removeBackslash(text: string): string {
return text // return text
.split(/\r?\n/) // .split(/\r?\n/)
.map((colon) => colon.replace(/[\\]:/g, ':')) // .map((colon) => colon.replace(/[\\]:/g, ':'))
.map((openBracket) => openBracket.replace(/[\\]{/g, '{')) // .map((openBracket) => openBracket.replace(/[\\]{/g, '{'))
.map((closeBracket) => closeBracket.replace(/[\\]}/g, '}')) // .map((closeBracket) => closeBracket.replace(/[\\]}/g, '}'))
.join(''); // .join('');
} // }

View file

@ -6,20 +6,32 @@ import {
MultipleChoiceQuestion as MultipleChoiceType, MultipleChoiceQuestion as MultipleChoiceType,
NumericalQuestion as NumericalType, NumericalQuestion as NumericalType,
ShortAnswerQuestion as ShortAnswerType, ShortAnswerQuestion as ShortAnswerType,
// Essay as EssayType, // EssayQuestion as EssayType,
TrueFalseQuestion as TrueFalseType, TrueFalseQuestion as TrueFalseType,
// MatchingQuestion as MatchingType, // MatchingQuestion as MatchingType,
} from 'gift-pegjs'; } from 'gift-pegjs';
import { DisplayOptions } from './types'; import { DisplayOptions } from './types';
import DescriptionTemplate from './DescriptionTemplate'; // import DescriptionTemplate from './DescriptionTemplate';
import EssayTemplate from './EssayTemplate'; // import EssayTemplate from './EssayTemplate';
import MatchingTemplate from './MatchingTemplate'; // import MatchingTemplate from './MatchingTemplate';
import MultipleChoiceTemplate from './MultipleChoiceTemplate'; import MultipleChoiceTemplate from './MultipleChoiceTemplate';
import NumericalTemplate from './NumericalTemplate'; import NumericalTemplate from './NumericalTemplate';
import ShortAnswerTemplate from './ShortAnswerTemplate'; import ShortAnswerTemplate from './ShortAnswerTemplate';
import TrueFalseTemplate from './TrueFalseTemplate'; import TrueFalseTemplate from './TrueFalseTemplate';
import Error from './ErrorTemplate'; import Error from './ErrorTemplate';
import CategoryTemplate from './CategoryTemplate'; // import CategoryTemplate from './CategoryTemplate';
export class UnsupportedQuestionTypeError extends globalThis.Error {
constructor(type: string) {
const userFriendlyType = (type === 'Essay') ? 'Réponse longue (Essay)'
: (type === 'Matching') ? 'Association (Matching)'
: (type === 'Category') ? 'Catégorie (Category)'
: type;
super(`Les questions du type ${userFriendlyType} ne sont pas supportées.`);
this.name = 'UnsupportedQuestionTypeError';
}
}
export const state: DisplayOptions = { preview: true, theme: 'light' }; export const state: DisplayOptions = { preview: true, theme: 'light' };
@ -54,23 +66,21 @@ export default function Template(
// case 'Matching': // case 'Matching':
// return Matching({ ...(keys as MatchingType) }); // return Matching({ ...(keys as MatchingType) });
default: default:
// TODO: throw error for unsupported question types? // convert type to human-readable string
// throw new Error(`Unsupported question type: ${type}`); throw new UnsupportedQuestionTypeError(type); }
return ``;
}
} }
export function ErrorTemplate(text: string, options?: Partial<DisplayOptions>): string { export function ErrorTemplate(questionText: string, errorText: string, options?: Partial<DisplayOptions>): string {
Object.assign(state, options); Object.assign(state, options);
return Error(text); return Error(questionText, errorText);
} }
export { export {
CategoryTemplate, // CategoryTemplate,
DescriptionTemplate as Description, // DescriptionTemplate as Description,
EssayTemplate as Essay, // EssayTemplate as Essay,
MatchingTemplate as Matching, // MatchingTemplate as Matching,
MultipleChoiceTemplate as MultipleChoice, MultipleChoiceTemplate as MultipleChoice,
NumericalTemplate as Numerical, NumericalTemplate as Numerical,
ShortAnswerTemplate as ShortAnswer, ShortAnswerTemplate as ShortAnswer,

View file

@ -1,12 +1,12 @@
import { TableCell, TableHead, TableRow } from "@mui/material";
import React from "react"; import React from "react";
import { TableCell, TableHead, TableRow } from "@mui/material";
interface LiveResultsFooterProps { interface LiveResultsHeaderProps {
maxQuestions: number; maxQuestions: number;
showSelectedQuestion: (index: number) => void; showSelectedQuestion: (index: number) => void;
} }
const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({ const LiveResultsTableHeader: React.FC<LiveResultsHeaderProps> = ({
maxQuestions, maxQuestions,
showSelectedQuestion, showSelectedQuestion,
}) => { }) => {
@ -47,4 +47,4 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
</TableHead> </TableHead>
); );
}; };
export default LiveResultsTableFooter; export default LiveResultsTableHeader;

View file

@ -113,6 +113,12 @@ const JoinRoom: React.FC = () => {
webSocketService.submitAnswer(answerData); webSocketService.submitAnswer(answerData);
}; };
const handleReturnKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && username && roomName) {
handleSocket();
}
};
if (isWaitingForTeacher) { if (isWaitingForTeacher) {
return ( return (
<div className='room'> <div className='room'>
@ -169,7 +175,8 @@ const JoinRoom: React.FC = () => {
onChange={(e) => setRoomName(e.target.value)} onChange={(e) => setRoomName(e.target.value)}
placeholder="Numéro de la salle" placeholder="Numéro de la salle"
sx={{ marginBottom: '1rem' }} sx={{ marginBottom: '1rem' }}
fullWidth fullWidth={true}
onKeyDown={handleReturnKey}
/> />
<TextField <TextField
@ -179,7 +186,8 @@ const JoinRoom: React.FC = () => {
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
placeholder="Nom d'utilisateur" placeholder="Nom d'utilisateur"
sx={{ marginBottom: '1rem' }} sx={{ marginBottom: '1rem' }}
fullWidth fullWidth={true}
onKeyDown={handleReturnKey}
/> />
<LoadingButton <LoadingButton

View file

@ -118,7 +118,11 @@ const QuizForm: React.FC = () => {
setValue(value); setValue(value);
} }
const linesArray = value.split(/(?<=^|[^\\]}.*)[\n]+/); // split value when there is at least one blank line
const linesArray = value.split(/\n{2,}/);
// if the first item in linesArray is blank, remove it
if (linesArray[0] === '') linesArray.shift();
if (linesArray[linesArray.length - 1] === '') linesArray.pop(); if (linesArray[linesArray.length - 1] === '') linesArray.pop();

View file

@ -1,6 +1,4 @@
import { useNavigate, Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
// JoinRoom.tsx
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import './Login.css'; import './Login.css';
@ -38,6 +36,11 @@ const Login: React.FC = () => {
}; };
const handleReturnKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && email && password) {
login();
}
};
return ( return (
<LoginContainer <LoginContainer
@ -51,7 +54,8 @@ const Login: React.FC = () => {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="Adresse courriel" placeholder="Adresse courriel"
sx={{ marginBottom: '1rem' }} sx={{ marginBottom: '1rem' }}
fullWidth fullWidth={true}
onKeyDown={handleReturnKey} // Add this line as well
/> />
<TextField <TextField
@ -62,7 +66,8 @@ const Login: React.FC = () => {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="Mot de passe" placeholder="Mot de passe"
sx={{ marginBottom: '1rem' }} sx={{ marginBottom: '1rem' }}
fullWidth fullWidth={true}
onKeyDown={handleReturnKey} // Add this line as well
/> />
<LoadingButton <LoadingButton