Merge pull request #211 from ets-cfuhrman-pfe/gift-pegjs-2.0
Some checks are pending
CI/CD Pipeline for Backend / build_and_push_backend (push) Waiting to run
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Waiting to run
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Waiting to run
Tests / tests (client) (push) Waiting to run
Tests / tests (server) (push) Waiting to run

Gift pegjs 2.0
This commit is contained in:
Christopher (Cris) Fuhrman 2025-01-28 15:29:54 -05:00 committed by GitHub
commit 763f606712
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 1693 additions and 1810 deletions

View file

@ -19,7 +19,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
node-version: '20'
- name: Install Dependencies, lint and Run Tests
run: |

View file

@ -21,7 +21,7 @@
"axios": "^1.6.7",
"dompurify": "^3.2.3",
"esbuild": "^0.23.1",
"gift-pegjs": "^1.0.2",
"gift-pegjs": "^2.0.0-beta.1",
"jest-environment-jsdom": "^29.7.0",
"katex": "^0.16.11",
"marked": "^14.1.2",
@ -66,6 +66,9 @@
"vite-plugin-environment": "^1.1.3"
}
},
"../GIFT-grammar-PEG.js": {
"extraneous": true
},
"node_modules/@adobe/css-tools": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz",
@ -7410,10 +7413,9 @@
}
},
"node_modules/gift-pegjs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/gift-pegjs/-/gift-pegjs-1.0.2.tgz",
"integrity": "sha512-S/A2wBDdia2QWKpB5FtASx1gguep1wg5If5glDWJgUMiABICJT7ogArGfsdgozevhBdbdOiHhrykJP86hbgvRw==",
"license": "MIT",
"version": "2.0.0-beta.1",
"resolved": "https://registry.npmjs.org/gift-pegjs/-/gift-pegjs-2.0.0-beta.1.tgz",
"integrity": "sha512-NFWSu3KjpjKrfnbIu/eQOyQqjCgOd/ONDe3+bKhtTQCrTgQPVoybme9cm8tqBmJz1YynloocrPlv9f2syQl/LQ==",
"dependencies": {
"pegjs": "^0.10.x"
}
@ -10689,7 +10691,6 @@
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz",
"integrity": "sha512-qI5+oFNEGi3L5HAxDwN2LA4Gg7irF70Zs25edhjld9QemOgp0CbvMtbFcMvFtEo1OityPrcCzkQFB8JP/hxgow==",
"license": "MIT",
"bin": {
"pegjs": "bin/pegjs"
},

View file

@ -25,7 +25,7 @@
"axios": "^1.6.7",
"dompurify": "^3.2.3",
"esbuild": "^0.23.1",
"gift-pegjs": "^1.0.2",
"gift-pegjs": "^2.0.0-beta.1",
"jest-environment-jsdom": "^29.7.0",
"katex": "^0.16.11",
"marked": "^14.1.2",

View file

@ -1,5 +1,5 @@
import { GIFTQuestion } from 'gift-pegjs';
import { BaseQuestion } from "gift-pegjs";
export interface QuestionType {
question: GIFTQuestion;
question: BaseQuestion;
}

View file

@ -1,4 +1,4 @@
console.log('constantsMock.tsx is loaded');
// console.log('constantsMock.tsx is loaded');
// constants.tsx
const ENV_VARIABLES = {
@ -7,7 +7,7 @@ const ENV_VARIABLES = {
VITE_BACKEND_SOCKET_URL: process.env.VITE_BACKEND_SOCKET_URL || "",
};
console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`);
console.log(`ENV_VARIABLES.VITE_BACKEND_SOCKET_URL=${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
// console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`);
// console.log(`ENV_VARIABLES.VITE_BACKEND_SOCKET_URL=${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
export { ENV_VARIABLES };

View file

@ -1,6 +1,6 @@
//QuestionType.test.tsx
import { GIFTQuestion } from 'gift-pegjs';
import { QuestionType } from '../../Types/QuestionType';
// Superfluous test now that gift-pegjs has TypeScript types (and its own tests)
import { Question } from 'gift-pegjs';
const sampleStem = 'Sample question stem';
const options = ['Option A', 'Option B'];
@ -8,30 +8,28 @@ const sampleFormat = 'plain';
const sampleType = 'MC';
const sampleTitle = 'Sample Question';
const mockQuestion: GIFTQuestion = {
const mockQuestion: Question = {
id: '1',
type: sampleType,
stem: { format: sampleFormat, text: sampleStem },
formattedStem: { format: sampleFormat, text: sampleStem },
title: sampleTitle,
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: [
{ text: { format: sampleFormat, text: options[0] }, isCorrect: true, weight: 1, feedback: null },
{ text: { format: sampleFormat, text: options[1] }, isCorrect: false, weight: 0, feedback: null },
{ formattedText: { format: sampleFormat, text: options[0] }, isCorrect: true, weight: 1 },
{ formattedText: { format: sampleFormat, text: options[1] }, isCorrect: false, weight: 0 },
],
};
const mockQuestionType: QuestionType = {
question: mockQuestion,
};
const mockQuestionType = mockQuestion;
describe('QuestionType', () => {
// test seems useless (it's broken) now that gift-pegjs has TypeScript types (and its own tests)
describe.skip('QuestionType', () => {
test('has the expected structure', () => {
expect(mockQuestionType).toEqual(expect.objectContaining({
question: expect.any(Object),
}));
expect(mockQuestionType.question).toEqual(expect.objectContaining({
expect(mockQuestionType).toEqual(expect.objectContaining({
id: expect.any(String),
type: expect.any(String),
stem: expect.objectContaining({

View file

@ -1,29 +1,30 @@
// TextType.test.ts
import { FormattedTextTemplate } from "src/components/GiftTemplate/templates/TextTypeTemplate";
import { TextFormat } from "gift-pegjs";
import textType from "src/components/GiftTemplate/templates/TextType";
describe('TextType', () => {
it('should format text with basic characters correctly', () => {
const input: TextFormat = {
// Text here would already be past the GIFT parsing stage, so we don't need to escape GIFT special characters
text: 'Hello, world! 5 > 3, right?',
format: 'plain'
format: 'moodle'
};
const expectedOutput = 'Hello, world! 5 > 3, right?';
expect(textType({ text: input })).toBe(expectedOutput);
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
it('should format text with newlines correctly', () => {
it('should format text with embedded newlines correctly', () => {
const input: TextFormat = {
// Text here would already be past the GIFT parsing stage, so we don't need to escape GIFT special characters
text: 'Hello,\nworld!\n5 > 3, right?',
format: 'plain'
format: 'moodle'
};
const expectedOutput = 'Hello,<br>world!<br>5 > 3, right?';
expect(textType({ text: input })).toBe(expectedOutput);
const expectedOutput = 'Hello,<br>world!<br>5 &gt; 3, right?';
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
it('should format text with LaTeX correctly', () => {
it('should format text with display-mode LaTeX correctly, with $$ delimiters', () => {
const input: TextFormat = {
// Text here would already be past the GIFT parsing stage, so we don't need to escape GIFT special characters
text: '$$E=mc^2$$',
format: 'plain'
};
@ -32,28 +33,42 @@ describe('TextType', () => {
// Hint -- if the output changes because of a change in the code or library, you can update
// by running the test and copying the "Received string:" in jest output
// when it fails (assuming the output is correct)
const expectedOutput = '<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(textType({ text: input })).toContain(expectedOutput);
const expectedOutput = '<span class="katex-display"><span class="katex"><span class="katex-mathml"><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow>E=mc^2</math></span><span aria-hidden="true" class="katex-html"><span class="base"><span style="height:0.6833em;" class="strut"></span><span style="margin-right:0.05764em;" class="mord mathnormal">E</span><span style="margin-right:0.2778em;" class="mspace"></span><span class="mrel">=</span><span style="margin-right:0.2778em;" class="mspace"></span></span><span class="base"><span style="height:0.8641em;" class="strut"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span style="height:0.8641em;" class="vlist"><span style="top:-3.113em;margin-right:0.05em;"><span style="height:2.7em;" class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(FormattedTextTemplate(input)).toContain(expectedOutput);
});
it('should format text with two equations (inline and separate) correctly', () => {
const input: TextFormat = {
// Text here would already be past the GIFT parsing stage, so we don't need to escape GIFT special characters
text: '$a + b = c$ ? $$E=mc^2$$',
format: 'plain'
format: 'moodle'
};
// hint: katex-display is the class that indicates a separate equation
const expectedOutput = '<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding="application/x-tex">a + b = c</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">c</span></span></span></span> ? <span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(textType({ text: input })).toContain(expectedOutput);
const expectedOutput = '<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow>a + b = c</math></span><span aria-hidden="true" class="katex-html"><span class="base"><span style="height:0.6667em;vertical-align:-0.0833em;" class="strut"></span><span class="mord mathnormal">a</span><span style="margin-right:0.2222em;" class="mspace"></span><span class="mbin">+</span><span style="margin-right:0.2222em;" class="mspace"></span></span><span class="base"><span style="height:0.6944em;" class="strut"></span><span class="mord mathnormal">b</span><span style="margin-right:0.2778em;" class="mspace"></span><span class="mrel">=</span><span style="margin-right:0.2778em;" class="mspace"></span></span><span class="base"><span style="height:0.4306em;" class="strut"></span><span class="mord mathnormal">c</span></span></span></span> ? <span class="katex-display"><span class="katex"><span class="katex-mathml"><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow>E=mc^2</math></span><span aria-hidden="true" class="katex-html"><span class="base"><span style="height:0.6833em;" class="strut"></span><span style="margin-right:0.05764em;" class="mord mathnormal">E</span><span style="margin-right:0.2778em;" class="mspace"></span><span class="mrel">=</span><span style="margin-right:0.2778em;" class="mspace"></span></span><span class="base"><span style="height:0.8641em;" class="strut"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span style="height:0.8641em;" class="vlist"><span style="top:-3.113em;margin-right:0.05em;"><span style="height:2.7em;" class="pstrut"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(FormattedTextTemplate(input)).toContain(expectedOutput);
});
it('should format text with a katex matrix correctly', () => {
it('should format text with an inline katex matrix correctly', () => {
const input: TextFormat = {
// eslint-disable-next-line no-useless-escape
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}\n a&b \\\\\n c&d\n\\end\{pmatrix\}`,
format: 'plain'
// Text here would already be past the GIFT parsing stage, so we don't need to escape GIFT special characters
text: `Inline matrix: \\( \\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\)`,
format: ''
};
const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow></mrow><annotation encoding="application/x-tex"></annotation></semantics></math></span><span class="katex-html" aria-hidden="true"></span></span>\\begin{pmatrix}<br> a&b \\\\<br> c&d<br>\\end{pmatrix}';
expect(textType({ text: input })).toContain(expectedOutput);
// eslint-disable-next-line no-irregular-whitespace
// warning: there are zero-width spaces "" in the expected output -- you must enable seeing them with an extension such as Gremlins tracker in VSCode
// eslint-disable-next-line no-irregular-whitespace
const expectedOutput = `Inline matrix: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mo fence="true">(</mo><mtable rowspacing="0.16em"><mtr><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>a</mi></mstyle></mtd><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>b</mi></mstyle></mtd></mtr><mtr><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>c</mi></mstyle></mtd><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>d</mi></mstyle></mtd></mtr></mtable><mo fence="true">)</mo></mrow> \\begin{pmatrix} a &amp; b \\\\ c &amp; d \\end{pmatrix} </math></span><span aria-hidden="true" class="katex-html"><span class="base"><span style="height:2.4em;vertical-align:-0.95em;" class="strut"></span><span class="minner"><span style="top:0em;" class="mopen delimcenter"><span class="delimsizing size3">(</span></span><span class="mord"><span class="mtable"><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span style="height:1.45em;" class="vlist"><span style="top:-3.61em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">a</span></span></span><span style="top:-2.41em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">c</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span style="height:0.95em;" class="vlist"><span></span></span></span></span></span><span style="width:0.5em;" class="arraycolsep"></span><span style="width:0.5em;" class="arraycolsep"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span style="height:1.45em;" class="vlist"><span style="top:-3.61em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">b</span></span></span><span style="top:-2.41em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">d</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span style="height:0.95em;" class="vlist"><span></span></span></span></span></span></span></span><span style="top:0em;" class="mclose delimcenter"><span class="delimsizing size3">)</span></span></span></span></span></span>`;
expect(FormattedTextTemplate(input)).toContain(expectedOutput);
});
it('should format text with an inline katex matrix correctly, with \\( and \\) as inline delimiters.', () => {
const input: TextFormat = {
text: `Donnez le déterminant de la matrice suivante.\\( \\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\)`,
format: ''
};
const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mo fence="true">(</mo><mtable rowspacing="0.16em"><mtr><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>a</mi></mstyle></mtd><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>b</mi></mstyle></mtd></mtr><mtr><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>c</mi></mstyle></mtd><mtd><mstyle displaystyle="false" scriptlevel="0"><mi>d</mi></mstyle></mtd></mtr></mtable><mo fence="true">)</mo></mrow> \\begin{pmatrix} a &amp; b \\\\ c &amp; d \\end{pmatrix} </math></span><span aria-hidden="true" class="katex-html"><span class="base"><span style="height:2.4em;vertical-align:-0.95em;" class="strut"></span><span class="minner"><span style="top:0em;" class="mopen delimcenter"><span class="delimsizing size3">(</span></span><span class="mord"><span class="mtable"><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span style="height:1.45em;" class="vlist"><span style="top:-3.61em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">a</span></span></span><span style="top:-2.41em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">c</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span style="height:0.95em;" class="vlist"><span></span></span></span></span></span><span style="width:0.5em;" class="arraycolsep"></span><span style="width:0.5em;" class="arraycolsep"></span><span class="col-align-c"><span class="vlist-t vlist-t2"><span class="vlist-r"><span style="height:1.45em;" class="vlist"><span style="top:-3.61em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">b</span></span></span><span style="top:-2.41em;"><span style="height:3em;" class="pstrut"></span><span class="mord"><span class="mord mathnormal">d</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span style="height:0.95em;" class="vlist"><span></span></span></span></span></span></span></span><span style="top:0em;" class="mclose delimcenter"><span class="delimsizing size3">)</span></span></span></span></span></span>';
expect(FormattedTextTemplate(input)).toContain(expectedOutput);
});
it('should format text with Markdown correctly', () => {
@ -63,7 +78,7 @@ describe('TextType', () => {
};
// TODO: investigate why the output has an extra newline
const expectedOutput = '<strong>Bold</strong>\n';
expect(textType({ text: input })).toBe(expectedOutput);
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
it('should format text with HTML correctly', () => {
@ -72,7 +87,7 @@ describe('TextType', () => {
format: 'html'
};
const expectedOutput = '<em>yes</em>';
expect(textType({ text: input })).toBe(expectedOutput);
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
it('should format plain text correctly', () => {
@ -81,8 +96,16 @@ describe('TextType', () => {
format: 'plain'
};
const expectedOutput = 'Just plain text';
expect(textType({ text: input })).toBe(expectedOutput);
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
// Add more tests for other formats if needed
it('should format a resized image correctly', () => {
const input: TextFormat = {
text: '![](https\\://www.etsmtl.ca/assets/img/ets.svg "\\=50px")',
format: 'markdown'
};
const expectedOutput = '<img width="50p" alt="" src="https://www.etsmtl.ca/assets/img/ets.svg">\n';
expect(FormattedTextTemplate(input)).toBe(expectedOutput);
});
});

View file

@ -1,7 +1,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import AnswerIcon from 'src/components/GiftTemplate/templates/AnswerIcon';
import AnswerIcon from 'src/components/GiftTemplate/templates/AnswerIconTemplate';
import DOMPurify from 'dompurify';
describe('AnswerIcon', () => {

View file

@ -2,99 +2,100 @@ import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MultipleChoice } from 'src/components/GiftTemplate/templates';
import { TemplateOptions, MultipleChoice as MultipleChoiceType } from 'src/components/GiftTemplate/templates/types';
import { TemplateOptions } from 'src/components/GiftTemplate/templates/types';
import { MultipleChoiceQuestion } from 'gift-pegjs';
// Mock the nanoid function
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id')
}));
const mockProps: TemplateOptions & MultipleChoiceType = {
const mockProps: TemplateOptions & MultipleChoiceQuestion = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title',
stem: { format: 'plain' , text: 'Sample Stem'},
formattedStem: { format: 'plain' , text: 'Sample Stem'},
choices: [
{ text: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
{ formattedText: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ formattedText: { format: 'plain', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
formattedGlobalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const katekMock: TemplateOptions & MultipleChoiceType = {
const katekMock: TemplateOptions & MultipleChoiceQuestion = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title',
stem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'},
formattedStem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'},
choices: [
{ text: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
{ formattedText: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ formattedText: { format: 'plain', text: 'Choice 2' }, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
formattedGlobalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const imageMock: TemplateOptions & MultipleChoiceType = {
const imageMock: TemplateOptions & MultipleChoiceQuestion = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title with Image',
stem: { format: 'plain', text: 'Sample Stem with Image' },
formattedStem: { format: 'plain', text: 'Sample Stem with Image' },
choices: [
{ text: { format: 'plain', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
{ text: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, feedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
{ formattedText: { format: 'plain', text: 'Choice 1' }, isCorrect: true, formattedFeedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ formattedText: { format: 'plain', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
{ formattedText: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, formattedFeedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback with Image' }
formattedGlobalFeedback: { format: 'plain', text: 'Sample Global Feedback with Image' }
};
const mockMoodle: TemplateOptions & MultipleChoiceType = {
const mockMoodle: TemplateOptions & MultipleChoiceQuestion = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title',
stem: { format: 'moodle' , text: 'Sample Stem'},
formattedStem: { format: 'moodle' , text: 'Sample Stem'},
choices: [
{ text: { format: 'moodle' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'plain', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
{ formattedText: { format: 'moodle' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ formattedText: { format: 'plain', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
formattedGlobalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const mockHTML: TemplateOptions & MultipleChoiceType = {
const mockHTML: TemplateOptions & MultipleChoiceQuestion = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title',
stem: { format: 'html' , text: '$$\\frac{zzz}{yyy}$$'},
formattedStem: { format: 'html' , text: '$$\\frac{zzz}{yyy}$$'},
choices: [
{ text: { format: 'html' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'html', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
{ formattedText: { format: 'html' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ formattedText: { format: 'html', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { format: 'plain' , text: 'InCorrect!'}, weight: 1 }
],
globalFeedback: { format: 'html', text: 'Sample Global Feedback' }
formattedGlobalFeedback: { format: 'html', text: 'Sample Global Feedback' }
};
const mockMarkdown: TemplateOptions & MultipleChoiceType = {
const mockMarkdown: TemplateOptions & MultipleChoiceQuestion = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title with Image',
stem: { format: 'markdown', text: 'Sample Stem with Image' },
formattedStem: { format: 'markdown', text: 'Sample Stem with Image' },
choices: [
{ text: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
{ text: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, feedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
{ formattedText: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, formattedFeedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ formattedText: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
{ formattedText: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, formattedFeedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
],
globalFeedback: { format: 'markdown', text: 'Sample Global Feedback with Image' }
formattedGlobalFeedback: { format: 'markdown', text: 'Sample Global Feedback with Image' }
};
const mockMarkdownTwoImages: TemplateOptions & MultipleChoiceType = {
const mockMarkdownTwoImages: TemplateOptions & MultipleChoiceQuestion = {
type: 'MC',
hasEmbeddedAnswers: false,
title: 'Sample Title with Image',
stem: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt = "Sample Image"/>' },
formattedStem: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt = "Sample Image"/>' },
choices: [
{ text: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
{ text: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, feedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
{ formattedText: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, formattedFeedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ formattedText: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 },
{ formattedText: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: false, formattedFeedback: { format: 'plain', text: 'Image Feedback' }, weight: 1 }
],
globalFeedback: { format: 'markdown', text: 'Sample Global Feedback with Image' }
formattedGlobalFeedback: { format: 'markdown', text: 'Sample Global Feedback with Image' }
};
test('MultipleChoice snapshot test', () => {

View file

@ -1,62 +1,40 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import Numerical from 'src/components/GiftTemplate/templates/Numerical';
import { TemplateOptions, Numerical as NumericalType } from 'src/components/GiftTemplate/templates/types';
import Numerical from 'src/components/GiftTemplate/templates/NumericalTemplate';
import { TemplateOptions } from 'src/components/GiftTemplate/templates/types';
import { parse, NumericalQuestion } from 'gift-pegjs';
// Mock the nanoid function
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id')
}));
const plainTextMock: TemplateOptions & NumericalType = {
type: 'Numerical',
hasEmbeddedAnswers: false,
title: 'Sample Numerical Title',
stem: { format: 'plain', text: 'Sample Stem' },
choices: [
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'plain', text: 'Correct!' } },
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'plain', text: 'Incorrect!' } }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const plainTextMock: TemplateOptions & NumericalQuestion =
parse(`
::Sample Numerical Title:: Sample Stem {#=42#Correct!=43#Incorrect!####Sample Global Feedback}
`)[0] as NumericalQuestion;
const htmlMock: TemplateOptions & NumericalType = {
type: 'Numerical',
hasEmbeddedAnswers: false,
title: 'Sample Numerical Title',
stem: { format: 'html', text: '$$\\frac{zzz}{yyy}$$' },
choices: [
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'html', text: 'Correct!' } },
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'html', text: 'Incorrect!' } }
],
globalFeedback: { format: 'html', text: 'Sample Global Feedback' }
};
const htmlMock: TemplateOptions & NumericalQuestion =
parse(`
::Sample Numerical Title::
[html]$$\\frac\\{zzz\\}\\{yyy\\}$$ {#
=42#Correct
=43#Incorrect!
####Sample Global Feedback
}
`)[0] as NumericalQuestion;
const moodleMock: TemplateOptions & NumericalType = {
type: 'Numerical',
hasEmbeddedAnswers: false,
title: 'Sample Numerical Title',
stem: { format: 'moodle', text: 'Sample Stem' },
choices: [
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'moodle', text: 'Correct!' } },
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'moodle', text: 'Incorrect!' } }
],
globalFeedback: { format: 'moodle', text: 'Sample Global Feedback' }
};
const moodleMock: TemplateOptions & NumericalQuestion =
parse(`
::Sample Numerical Title::[moodle]Sample Stem {#=42#Correct!=43#Incorrect!####Sample Global Feedback}
`)[0] as NumericalQuestion;
const imageMock: TemplateOptions & NumericalQuestion =
parse(`
::Sample Numerical Title with Image::[markdown]Sample Stem with Image ![](https\\://example.com/cat.jpg){#=42#Correct!=43#Incorrect!=44#Also Incorrect! ![](https\\://example.com/cat.jpg)####Sample Global Feedback with Image}
`)[0] as NumericalQuestion;
const imageMock: TemplateOptions & NumericalType = {
type: 'Numerical',
hasEmbeddedAnswers: false,
title: 'Sample Numerical Title with Image',
stem: { format: 'plain', text: 'Sample Stem with Image' },
choices: [
{ isCorrect: true, weight: 1, text: { type: 'simple', number: 42}, feedback: { format: 'plain', text: 'Correct!' } },
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 43}, feedback: { format: 'plain', text: 'Incorrect!' } },
{ isCorrect: false, weight: 1, text: { type: 'simple', number: 44}, feedback: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' } }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback with Image' }
};
test('Numerical snapshot test with plain text', () => {
const { asFragment } = render(<Numerical {...plainTextMock} />);

View file

@ -1,63 +1,35 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import ShortAnswer from 'src/components/GiftTemplate/templates/ShortAnswer';
import { TemplateOptions, ShortAnswer as ShortAnswerType } from 'src/components/GiftTemplate/templates/types';
import ShortAnswer from 'src/components/GiftTemplate/templates/ShortAnswerTemplate';
import { TemplateOptions } from 'src/components/GiftTemplate/templates/types';
import { parse, ShortAnswerQuestion } from 'gift-pegjs';
// Mock the nanoid function
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id')
}));
const plainTextMock: TemplateOptions & ShortAnswerType = {
type: 'Short',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title',
stem: { format: 'plain', text: 'Sample Stem' },
choices: [
{ text: { format: 'plain' , text: 'Answer 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'plain' , text: 'Answer 2'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const plainTextMock: TemplateOptions & ShortAnswerQuestion =
parse(`
::Sample Short Answer Title:: Sample Stem {=%1%Answer 1#Correct! =%1%Answer 2#Correct!####Sample Global Feedback}
`)[0] as ShortAnswerQuestion;
const katexMock: TemplateOptions & ShortAnswerType = {
type: 'Short',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title',
stem: { format: 'html', text: '$$\\frac{zzz}{yyy}$$' },
choices: [
{ text: { format: 'html' , text: 'Answer 1'}, isCorrect: true, feedback: { format: 'html' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'html' , text: 'Answer 2'}, isCorrect: true, feedback: { format: 'moodle' , text: 'Correct!'}, weight: 1 }
],
globalFeedback: { format: 'html', text: 'Sample Global Feedback' }
};
const katexMock: TemplateOptions & ShortAnswerQuestion =
parse(`
::Sample Short Answer Title:: $$\\frac\\{zzz\\}\\{yyy\\}$$ {=%1%Answer 1#Correct! =%1%Answer 2#Correct!####[html]Sample Global Feedback}
`)[0] as ShortAnswerQuestion;
const moodleMock: TemplateOptions & ShortAnswerType = {
type: 'Short',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title',
stem: { format: 'moodle', text: 'Sample Stem' },
choices: [
{ text: { format: 'moodle' , text: 'Answer 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ text: { format: 'moodle' , text: 'Answer 2'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }
],
globalFeedback: { format: 'moodle', text: 'Sample Global Feedback' }
};
const imageMock: TemplateOptions & ShortAnswerType = {
type: 'Short',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title with Image',
stem: { format: 'markdown', text: 'Sample Stem with Image' },
choices: [
{ text: { format: 'markdown', text: 'Answer 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'markdown', text: 'Answer 2' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'markdown', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 }
],
globalFeedback: { format: 'plain', text: 'Sample Global Feedback with Image' }
};
const moodleMock: TemplateOptions & ShortAnswerQuestion =
parse(`
::Sample Short Answer Title:: Sample Stem {=%1%Answer 1#Correct! =%1%Answer 2#Correct!####[moodle]Sample Global Feedback}
`)[0] as ShortAnswerQuestion;
const imageMock: TemplateOptions & ShortAnswerQuestion =
parse(`
::Sample Short Answer Title with Image::[markdown]Sample Stem with Image ![](https\\://example.com/cat.jpg) {=%1%Answer 1#Correct! =%1%Answer 2#Correct!####Sample Global Feedback with Image}
`)[0] as ShortAnswerQuestion;
test('ShortAnswer snapshot test with plain text', () => {
const { asFragment } = render(<ShortAnswer {...plainTextMock} />);

View file

@ -2,56 +2,27 @@ import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import TrueFalse from 'src/components/GiftTemplate/templates';
import { TemplateOptions, TrueFalse as TrueFalseType } from 'src/components/GiftTemplate/templates/types';
import { TemplateOptions } from 'src/components/GiftTemplate/templates/types';
import { parse, ShortAnswerQuestion, TrueFalseQuestion } from 'gift-pegjs';
// Mock the nanoid function
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id')
}));
}));
const plainTextMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample True/False Title',
stem: { format: 'plain', text: 'Sample Stem' },
isTrue: true,
trueFeedback: { format: 'plain', text: 'Correct!' },
falseFeedback: { format: 'plain', text: 'Incorrect!' },
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const plainTextMock: TemplateOptions & TrueFalseQuestion =
parse(`::Sample True/False Title::Sample Stem {T#Correct!#Incorrect!####Sample Global Feedback}`)[0] as TrueFalseQuestion;
const katexMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample True/False Title',
stem: { format: 'html', text: '$$\\frac{zzz}{yyy}$$' },
isTrue: true,
trueFeedback: { format: 'moodle', text: 'Correct!' },
falseFeedback: { format: 'html', text: 'Incorrect!' },
globalFeedback: { format: 'markdown', text: 'Sample Global Feedback' }
};
const katexMock: TemplateOptions & TrueFalseQuestion =
parse(`::Sample True/False Title::$$\\frac\\{zzz\\}\\{yyy\\}$$ {T#Correct!#Incorrect!####Sample Global Feedback}`)[0] as TrueFalseQuestion;
const moodleMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample True/False Title',
stem: { format: 'moodle', text: 'Sample Stem' },
isTrue: true,
trueFeedback: { format: 'moodle', text: 'Correct!' },
falseFeedback: { format: 'moodle', text: 'Incorrect!' },
globalFeedback: { format: 'moodle', text: 'Sample Global Feedback' }
};
const moodleMock: TemplateOptions & TrueFalseQuestion =
parse(`::Sample True/False Title::[moodle]Sample Stem{TRUE#Correct!#Incorrect!####Sample Global Feedback}`)[0] as TrueFalseQuestion;
const imageMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title with Image',
stem: { format: 'plain', text: 'Sample Stem with Image' },
trueFeedback: { format: 'moodle', text: 'Correct!' },
isTrue: true,
falseFeedback: { format: 'moodle', text: 'Incorrect!' },
globalFeedback: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }
};
const imageMock: TemplateOptions & ShortAnswerQuestion =
parse(`::Sample Short Answer Title with Image::
[markdown]Sample Stem with Image ![](https\\://example.com/cat.gif)
{=A =B =C####[html]<img src\\="https\\://via.placeholder.com/150" alt\\="Sample Image" />}`)[0] as ShortAnswerQuestion;
test('TrueFalse snapshot test with plain text', () => {
const { asFragment } = render(<TrueFalse {...plainTextMock} />);

View file

@ -38,24 +38,24 @@ exports[`MultipleChoice snapshot test 1`] = `
"&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;&lt;span style="
" class="present-question-stem"&gt;
Sample Stem
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(134, 31%, 32%);
background-color: hsl(134, 68%, 95%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -73,25 +73,13 @@ exports[`MultipleChoice snapshot test 1`] = `
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(35, 51%, 33%);
background-color: hsl(36, 84%, 93%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -109,9 +97,7 @@ exports[`MultipleChoice snapshot test 1`] = `
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;InCorrect!&lt;/span&gt;
&lt;span class="feedback-container"&gt;InCorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
@ -167,24 +153,24 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
"&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;img src="https://via.placeholder.com/150" alt = "Sample Image"/&gt;&lt;/p&gt;&lt;span style="
" class="present-question-stem"&gt;
&lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt;
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(134, 31%, 32%);
background-color: hsl(134, 68%, 95%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -203,25 +189,13 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(35, 51%, 33%);
background-color: hsl(36, 84%, 93%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -240,32 +214,20 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(35, 51%, 33%);
background-color: hsl(36, 84%, 93%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
&lt;img src="https://via.placeholder.com/150" alt="Sample Image" /&gt;
&lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt;
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
@ -276,9 +238,7 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Image Feedback&lt;/span&gt;
&lt;span class="feedback-container"&gt;Image Feedback&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
@ -335,24 +295,24 @@ exports[`MultipleChoice snapshot test with Moodle text format 1`] = `
"&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;&lt;span style="
" class="present-question-stem"&gt;
Sample Stem
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(134, 31%, 32%);
background-color: hsl(134, 68%, 95%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -370,25 +330,13 @@ exports[`MultipleChoice snapshot test with Moodle text format 1`] = `
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(35, 51%, 33%);
background-color: hsl(36, 84%, 93%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -406,9 +354,7 @@ exports[`MultipleChoice snapshot test with Moodle text format 1`] = `
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;InCorrect!&lt;/span&gt;
&lt;span class="feedback-container"&gt;InCorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
@ -464,24 +410,24 @@ exports[`MultipleChoice snapshot test with image 1`] = `
"&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image&lt;/p&gt;&lt;span style="
" class="present-question-stem"&gt;
Sample Stem with Image
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(134, 31%, 32%);
background-color: hsl(134, 68%, 95%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -499,25 +445,13 @@ exports[`MultipleChoice snapshot test with image 1`] = `
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(35, 51%, 33%);
background-color: hsl(36, 84%, 93%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -535,32 +469,20 @@ exports[`MultipleChoice snapshot test with image 1`] = `
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(35, 51%, 33%);
background-color: hsl(36, 84%, 93%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
&lt;img src="https://via.placeholder.com/150" alt="Sample Image" /&gt;
&lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt;
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
@ -571,9 +493,7 @@ exports[`MultipleChoice snapshot test with image 1`] = `
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Image Feedback&lt;/span&gt;
&lt;span class="feedback-container"&gt;Image Feedback&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
@ -629,25 +549,25 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
"&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image
&lt;/p&gt;&lt;span style="
" class="present-question-stem"&gt;
Sample Stem with Image
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(134, 31%, 32%);
background-color: hsl(134, 68%, 95%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -666,25 +586,13 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(35, 51%, 33%);
background-color: hsl(36, 84%, 93%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -703,32 +611,20 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(35, 51%, 33%);
background-color: hsl(36, 84%, 93%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
&lt;img src="https://via.placeholder.com/150" alt="Sample Image" /&gt;
&lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt;
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
@ -739,9 +635,7 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Image Feedback&lt;/span&gt;
&lt;span class="feedback-container"&gt;Image Feedback&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
@ -798,24 +692,24 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
"&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;span style="
" class="present-question-stem"&gt;
&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math display="block" xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;\\frac{zzz}{yyy}&lt;/math&gt;&lt;/span&gt;&lt;span aria-hidden="true" class="katex-html"&gt;&lt;span class="base"&gt;&lt;span style="height:1.988em;vertical-align:-0.8804em;" class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span style="height:1.1076em;" class="vlist"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span style="margin-right:0.03588em;" class="mord mathnormal"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span style="border-bottom-width:0.04em;" class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span style="margin-right:0.04398em;" class="mord mathnormal"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span style="height:0.8804em;" class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(134, 31%, 32%);
background-color: hsl(134, 68%, 95%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -833,25 +727,13 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(134, 31%, 32%);
background-color: hsl(134, 68%, 95%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -869,9 +751,7 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
@ -927,24 +807,24 @@ exports[`MultipleChoice snapshot test with katex, using html text format 1`] = `
"&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;span style="
" class="present-question-stem"&gt;
&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math display="block" xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;\\frac{zzz}{yyy}&lt;/math&gt;&lt;/span&gt;&lt;span aria-hidden="true" class="katex-html"&gt;&lt;span class="base"&gt;&lt;span style="height:1.988em;vertical-align:-0.8804em;" class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span style="height:1.1076em;" class="vlist"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span style="margin-right:0.03588em;" class="mord mathnormal"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span style="border-bottom-width:0.04em;" class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span style="margin-right:0.04398em;" class="mord mathnormal"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span style="height:0.8804em;" class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(134, 31%, 32%);
background-color: hsl(134, 68%, 95%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -962,25 +842,13 @@ exports[`MultipleChoice snapshot test with katex, using html text format 1`] = `
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style="
box-shadow: 0px 1px 1px hsl(0, 0%, 74%);
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
color: hsl(35, 51%, 33%);
background-color: hsl(36, 84%, 93%);
"&gt;1%&lt;/span&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
@ -998,9 +866,7 @@ exports[`MultipleChoice snapshot test with katex, using html text format 1`] = `
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;InCorrect!&lt;/span&gt;
&lt;span class="feedback-container"&gt;InCorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="

View file

@ -38,13 +38,21 @@ exports[`Numerical snapshot test with html 1`] = `
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;span style="
" class="present-question-stem"&gt;
&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math display="block" xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;\\frac{zzz}{yyy}&lt;/math&gt;&lt;/span&gt;&lt;span aria-hidden="true" class="katex-html"&gt;&lt;span class="base"&gt;&lt;span style="height:1.988em;vertical-align:-0.8804em;" class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span style="height:1.1076em;" class="vlist"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span style="margin-right:0.03588em;" class="mord mathnormal"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span style="border-bottom-width:0.04em;" class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span style="margin-right:0.04398em;" class="mord mathnormal"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span style="height:0.8804em;" class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
"&gt;Réponse: &lt;/p&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
@ -58,9 +66,23 @@ exports[`Numerical snapshot test with html 1`] = `
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="42, 43"&gt;
&lt;/div&gt;
&lt;div style="
;width: 100%" placeholder="42"&gt;&lt;div class="feedback-container"&gt;Correct&lt;/div&gt;&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/p&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
;width: 100%" placeholder="43"&gt;&lt;div class="feedback-container"&gt;Incorrect!&lt;/div&gt;&lt;/div&gt;&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
@ -113,13 +135,22 @@ exports[`Numerical snapshot test with image 1`] = `
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image&lt;/p&gt;
&lt;div&gt;
&lt;span style="
" class="present-question-stem"&gt;
Sample Stem with Image &lt;img alt="" src="https://example.com/cat.jpg"&gt;
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
"&gt;Réponse: &lt;/p&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
@ -133,9 +164,42 @@ exports[`Numerical snapshot test with image 1`] = `
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="42, 43, 44"&gt;
&lt;/div&gt;
&lt;div style="
;width: 100%" placeholder="42"&gt;&lt;div class="feedback-container"&gt;Correct!
&lt;/div&gt;&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/p&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
;width: 100%" placeholder="43"&gt;&lt;div class="feedback-container"&gt;Incorrect!
&lt;/div&gt;&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/p&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
;width: 100%" placeholder="44"&gt;&lt;div class="feedback-container"&gt;Also Incorrect! &lt;img alt="" src="https://example.com/cat.jpg"&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
@ -145,7 +209,8 @@ exports[`Numerical snapshot test with image 1`] = `
border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
"&gt;
&lt;p&gt;Sample Global Feedback with Image&lt;/p&gt;
&lt;p&gt;Sample Global Feedback with Image
&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
@ -188,13 +253,21 @@ exports[`Numerical snapshot test with moodle 1`] = `
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
&lt;div&gt;
&lt;span style="
" class="present-question-stem"&gt;
Sample Stem
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
"&gt;Réponse: &lt;/p&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
@ -208,9 +281,23 @@ exports[`Numerical snapshot test with moodle 1`] = `
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="42, 43"&gt;
&lt;/div&gt;
&lt;div style="
;width: 100%" placeholder="42"&gt;&lt;div class="feedback-container"&gt;Correct!&lt;/div&gt;&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/p&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
;width: 100%" placeholder="43"&gt;&lt;div class="feedback-container"&gt;Incorrect!&lt;/div&gt;&lt;/div&gt;&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
@ -263,13 +350,21 @@ exports[`Numerical snapshot test with plain text 1`] = `
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
&lt;div&gt;
&lt;span style="
" class="present-question-stem"&gt;
Sample Stem
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
"&gt;Réponse: &lt;/p&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
@ -283,9 +378,23 @@ exports[`Numerical snapshot test with plain text 1`] = `
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="42, 43"&gt;
&lt;/div&gt;
&lt;div style="
;width: 100%" placeholder="42"&gt;&lt;div class="feedback-container"&gt;Correct!&lt;/div&gt;&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/p&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
;width: 100%" placeholder="43"&gt;&lt;div class="feedback-container"&gt;Incorrect!&lt;/div&gt;&lt;/div&gt;&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;

View file

@ -38,10 +38,20 @@ exports[`ShortAnswer snapshot test with image 1`] = `
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image
&lt;/p&gt;
" class="present-question-stem"&gt;
Sample Stem with Image &lt;img alt="" src="https://example.com/cat.jpg"&gt;
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
@ -59,9 +69,7 @@ exports[`ShortAnswer snapshot test with image 1`] = `
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="Answer 1
, Answer 2
, &lt;img src="https://via.placeholder.com/150" alt="Sample Image" /&gt;"&gt;
" placeholder="Answer 1, Answer 2"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
@ -73,7 +81,8 @@ exports[`ShortAnswer snapshot test with image 1`] = `
border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
"&gt;
&lt;p&gt;Sample Global Feedback with Image&lt;/p&gt;
&lt;p&gt;Sample Global Feedback with Image
&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
@ -116,9 +125,19 @@ exports[`ShortAnswer snapshot test with katex 1`] = `
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
" class="present-question-stem"&gt;
&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math display="block" xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;\\frac{zzz}{yyy}&lt;/math&gt;&lt;/span&gt;&lt;span aria-hidden="true" class="katex-html"&gt;&lt;span class="base"&gt;&lt;span style="height:1.988em;vertical-align:-0.8804em;" class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span style="height:1.1076em;" class="vlist"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span style="margin-right:0.03588em;" class="mord mathnormal"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span style="border-bottom-width:0.04em;" class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span style="margin-right:0.04398em;" class="mord mathnormal"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span style="height:0.8804em;" class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
@ -191,9 +210,19 @@ exports[`ShortAnswer snapshot test with moodle 1`] = `
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
" class="present-question-stem"&gt;
Sample Stem
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
@ -266,9 +295,19 @@ exports[`ShortAnswer snapshot test with plain text 1`] = `
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
" class="present-question-stem"&gt;
Sample Stem
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);

View file

@ -35,66 +35,43 @@ exports[`TrueFalse snapshot test with image 1`] = `
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Vrai/Faux&lt;/span&gt;
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image&lt;/p&gt;&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Vrai
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
" class="present-question-stem"&gt;
Sample Stem with Image &lt;img alt="" src="https://example.com/cat.gif"&gt;
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
padding: 0.375rem 0.75rem;
line-height: 1.5;
color: hsl(0, 0%, 16%);
background-color: hsl(0, 0%, 100%);
border: 1px solid hsl(0, 0%, 81%);
border-radius: 0.25rem;
box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 90%;
" placeholder="A, B, C"&gt;
&lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Faux
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
@ -104,7 +81,7 @@ exports[`TrueFalse snapshot test with image 1`] = `
border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
"&gt;
&lt;p&gt;&lt;img src="https://via.placeholder.com/150" alt="Sample Image" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Sample Image" src="https://via.placeholder.com/150"&gt;&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
@ -147,9 +124,19 @@ exports[`TrueFalse snapshot test with katex 1`] = `
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;span style="
" class="present-question-stem"&gt;
&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math display="block" xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;\\frac{zzz}{yyy}&lt;/math&gt;&lt;/span&gt;&lt;span aria-hidden="true" class="katex-html"&gt;&lt;span class="base"&gt;&lt;span style="height:1.988em;vertical-align:-0.8804em;" class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span style="height:1.1076em;" class="vlist"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span style="margin-right:0.03588em;" class="mord mathnormal"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span style="border-bottom-width:0.04em;" class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span style="height:3em;" class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span style="margin-right:0.04398em;" class="mord mathnormal"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span style="height:0.8804em;" class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
@ -172,9 +159,7 @@ exports[`TrueFalse snapshot test with katex 1`] = `
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
@ -198,9 +183,7 @@ exports[`TrueFalse snapshot test with katex 1`] = `
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
@ -213,8 +196,7 @@ exports[`TrueFalse snapshot test with katex 1`] = `
border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
"&gt;
&lt;p&gt;Sample Global Feedback
&lt;/p&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
@ -257,9 +239,19 @@ exports[`TrueFalse snapshot test with moodle 1`] = `
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;&lt;span style="
" class="present-question-stem"&gt;
Sample Stem
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
@ -282,9 +274,7 @@ exports[`TrueFalse snapshot test with moodle 1`] = `
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
@ -308,9 +298,7 @@ exports[`TrueFalse snapshot test with moodle 1`] = `
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
@ -366,9 +354,19 @@ exports[`TrueFalse snapshot test with plain text 1`] = `
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;&lt;span style="
" class="present-question-stem"&gt;
Sample Stem
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
@ -391,9 +389,7 @@ exports[`TrueFalse snapshot test with plain text 1`] = `
width: 1em;
color: hsl(120, 39%, 54%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
@ -417,9 +413,7 @@ exports[`TrueFalse snapshot test with plain text 1`] = `
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="

View file

@ -1,35 +1,43 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import MultipleChoiceQuestion from 'src/components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
import { act } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { MultipleChoiceQuestion, parse } from 'gift-pegjs';
import MultipleChoiceQuestionDisplay from 'src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay';
const questionStem = 'Question stem';
const sampleFeedback = 'Feedback';
const questions = parse(
`::Sample Question 1:: Question stem
{
=Choice 1
~Choice 2
}`) as MultipleChoiceQuestion[];
describe('MultipleChoiceQuestion', () => {
const question = questions[0];
describe('MultipleChoiceQuestionDisplay', () => {
const mockHandleOnSubmitAnswer = jest.fn();
const choices = [
{ feedback: null, isCorrect: true, text: { format: 'plain', text: 'Choice 1' } },
{ feedback: null, isCorrect: false, text: { format: 'plain', text: 'Choice 2' } }
];
const sampleProps = {
question: question,
handleOnSubmitAnswer: mockHandleOnSubmitAnswer,
showAnswer: false
};
const choices = question.choices;
beforeEach(() => {
render(
<MemoryRouter>
<MultipleChoiceQuestion
globalFeedback={sampleFeedback}
choices={choices}
handleOnSubmitAnswer={mockHandleOnSubmitAnswer}
questionStem={{ text: questionStem, format: 'plain' }} />
<MultipleChoiceQuestionDisplay
{...sampleProps}
/>
</MemoryRouter>);
});
test('renders the question and choices', () => {
expect(screen.getByText(questionStem)).toBeInTheDocument();
expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument();
choices.forEach((choice) => {
expect(screen.getByText(choice.text.text)).toBeInTheDocument();
expect(screen.getByText(choice.formattedText.text)).toBeInTheDocument();
});
});

View file

@ -1,30 +1,48 @@
// NumericalQuestion.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import NumericalQuestion from 'src/components/Questions/NumericalQuestion/NumericalQuestion';
import { NumericalQuestion, parse, ParsedGIFTQuestion } from 'gift-pegjs';
import { MemoryRouter } from 'react-router-dom';
import NumericalQuestionDisplay from 'src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay';
const questions = parse(
`
::Sample Question 1:: Question stem
{
#5..10
}`
) as ParsedGIFTQuestion[];
const question = questions[0] as NumericalQuestion;
describe('NumericalQuestion parse', () => {
const q = questions[0];
it('The question is Numerical', () => {
expect(q.type).toBe('Numerical');
});
});
describe('NumericalQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn();
const sampleStem = 'Sample question stem';
const mockHandleOnSubmitAnswer = jest.fn();
const sampleProps = {
questionTitle: 'Sample Question',
correctAnswers: {
numberHigh: 10,
numberLow: 5,
type: 'high-low'
},
handleOnSubmitAnswer: mockHandleSubmitAnswer,
question: question,
handleOnSubmitAnswer: mockHandleOnSubmitAnswer,
showAnswer: false
};
beforeEach(() => {
render(<NumericalQuestion questionContent={{text: sampleStem, format: 'plain'}} {...sampleProps} />);
render(
<MemoryRouter>
<NumericalQuestionDisplay
{...sampleProps}
/>
</MemoryRouter>);
});
it('renders correctly', () => {
expect(screen.getByText(sampleStem)).toBeInTheDocument();
expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument();
expect(screen.getByTestId('number-input')).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument();
});
@ -48,7 +66,7 @@ describe('NumericalQuestion Component', () => {
fireEvent.click(submitButton);
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
});
it('submits answer correctly', () => {
@ -59,6 +77,6 @@ describe('NumericalQuestion Component', () => {
fireEvent.click(submitButton);
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7);
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(7);
});
});

View file

@ -1,72 +1,51 @@
// Question.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, within } from '@testing-library/react';
import '@testing-library/jest-dom';
import Questions from 'src/components/Questions/Question';
import { GIFTQuestion } from 'gift-pegjs';
import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
import { parse, Question } from 'gift-pegjs';
//
describe('Questions Component', () => {
const mockHandleSubmitAnswer = jest.fn();
const sampleTrueFalseQuestion: GIFTQuestion = {
type: 'TF',
stem: { format: 'plain', text: 'Sample True/False Question' },
isTrue: true,
falseFeedback: null,
trueFeedback: null,
title: 'True/False Question',
hasEmbeddedAnswers: false,
globalFeedback: null,
const sampleTrueFalseQuestion =
parse('::Sample True/False Question:: Sample True/False Question {T}')[0];
const sampleMultipleChoiceQuestion =
parse('::Sample Multiple Choice Question:: Sample Multiple Choice Question {=Choice 1 ~Choice 2}')[0];
const sampleNumericalQuestion =
parse('::Sample Numerical Question:: Sample Numerical Question {#5..10}')[0];
const sampleShortAnswerQuestion =
parse('::Sample Short Answer Question:: Sample Short Answer Question {=Correct Answer =Another Answer}')[0];
const sampleProps = {
handleOnSubmitAnswer: mockHandleSubmitAnswer,
showAnswer: false
};
const sampleMultipleChoiceQuestion: GIFTQuestion = {
type: 'MC',
stem: { format: 'plain', text: 'Sample Multiple Choice Question' },
title: 'Multiple Choice Question',
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: [
{ feedback: null, isCorrect: true, text: { format: 'plain', text: 'Choice 1' }, weight: 1 },
{ feedback: null, isCorrect: false, text: { format: 'plain', text: 'Choice 2' }, weight: 0 },
],
const renderComponent = (question: Question) => {
render(<QuestionDisplay question={question} {...sampleProps} />);
};
const sampleNumericalQuestion: GIFTQuestion = {
type: 'Numerical',
stem: { format: 'plain', text: 'Sample Numerical Question' },
title: 'Numerical Question',
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: { numberHigh: 10, numberLow: 5, type: 'high-low' },
};
describe('question type parsing', () => {
it('parses true/false question type correctly', () => {
expect(sampleTrueFalseQuestion.type).toBe('TF');
});
const sampleShortAnswerQuestion: GIFTQuestion = {
type: 'Short',
stem: { format: 'plain', text: 'Sample short answer question' },
title: 'Short Answer Question Title',
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: [
{
feedback: { format: 'html', text: 'Correct answer feedback' },
isCorrect: true,
text: { format: 'html', text: 'Correct Answer' },
weight: 1,
},
{
feedback: { format: 'html', text: 'Incorrect answer feedback' },
isCorrect: false,
text: { format: 'html', text: 'Incorrect Answer' },
weight: 0,
},
],
};
it('parses multiple choice question type correctly', () => {
expect(sampleMultipleChoiceQuestion.type).toBe('MC');
});
const renderComponent = (question: GIFTQuestion) => {
render(<Questions question={question} handleOnSubmitAnswer={mockHandleSubmitAnswer} />);
};
it('parses numerical question type correctly', () => {
expect(sampleNumericalQuestion.type).toBe('Numerical');
});
it('parses short answer question type correctly', () => {
expect(sampleShortAnswerQuestion.type).toBe('Short');
});
});
it('renders correctly for True/False question', () => {
renderComponent(sampleTrueFalseQuestion);
@ -120,15 +99,19 @@ describe('Questions Component', () => {
it('renders correctly for Short Answer question', () => {
renderComponent(sampleShortAnswerQuestion);
expect(screen.getByText('Sample short answer question')).toBeInTheDocument();
expect(screen.getByTestId('text-input')).toBeInTheDocument();
expect(screen.getByText('Sample Short Answer Question')).toBeInTheDocument();
const container = screen.getByLabelText('short-answer-input');
const inputElement = within(container).getByRole('textbox') as HTMLInputElement;
expect(inputElement).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument();
});
it('handles input and submission for Short Answer question', () => {
renderComponent(sampleShortAnswerQuestion);
const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
const container = screen.getByLabelText('short-answer-input');
const inputElement = within(container).getByRole('textbox') as HTMLInputElement;
fireEvent.change(inputElement, { target: { value: 'User Input' } });
const submitButton = screen.getByText('Répondre');

View file

@ -1,54 +1,34 @@
// ShortAnswerQuestion.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, within } from '@testing-library/react';
import '@testing-library/jest-dom';
import ShortAnswerQuestion from 'src/components/Questions/ShortAnswerQuestion/ShortAnswerQuestion';
import { parse, ShortAnswerQuestion } from 'gift-pegjs';
import ShortAnswerQuestionDisplay from 'src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay';
describe('ShortAnswerQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn();
const sampleStem = 'Sample question stem';
const question =
parse('::Sample Short Answer Question:: Sample Short Answer Question {=Correct Answer ~Incorrect Answer}')[0] as ShortAnswerQuestion;
const sampleProps = {
questionTitle: 'Sample Question',
choices: [
{
id: '1',
feedback: {
format: 'text',
text: 'Correct answer feedback'
},
isCorrect: true,
text: {
format: 'text',
text: 'Correct Answer'
}
},
{
id: '2',
feedback: null,
isCorrect: false,
text: {
format: 'text',
text: 'Incorrect Answer'
}
}
],
handleOnSubmitAnswer: mockHandleSubmitAnswer,
showAnswer: false
};
beforeEach(() => {
render(<ShortAnswerQuestion questionContent={{text: sampleStem, format: 'plain'}} {...sampleProps} />);
render(<ShortAnswerQuestionDisplay question={question} {...sampleProps} />);
});
it('renders correctly', () => {
expect(screen.getByText(sampleStem)).toBeInTheDocument();
expect(screen.getByTestId('text-input')).toBeInTheDocument();
expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument();
const container = screen.getByLabelText('short-answer-input');
const inputElement = within(container).getByRole('textbox') as HTMLInputElement;
expect(inputElement).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument();
});
it('handles input change correctly', () => {
const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
const container = screen.getByLabelText('short-answer-input');
const inputElement = within(container).getByRole('textbox') as HTMLInputElement;
fireEvent.change(inputElement, { target: { value: 'User Input' } });
@ -70,7 +50,10 @@ describe('ShortAnswerQuestion Component', () => {
});
it('submits answer correctly', () => {
const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
const container = screen.getByLabelText('short-answer-input');
const inputElement = within(container).getByRole('textbox') as HTMLInputElement;
// const inputElement = screen.getByRole('textbox', { name: 'short-answer-input'}) as HTMLInputElement;
const submitButton = screen.getByText('Répondre');
fireEvent.change(inputElement, { target: { value: 'User Input' } });

View file

@ -2,16 +2,18 @@
import React from 'react';
import { render, fireEvent, screen, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import TrueFalseQuestion from 'src/components/Questions/TrueFalseQuestion/TrueFalseQuestion';
import { MemoryRouter } from 'react-router-dom';
import TrueFalseQuestionDisplay from 'src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay';
import { parse, TrueFalseQuestion } from 'gift-pegjs';
describe('TrueFalseQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn();
const sampleStem = 'Sample question stem';
const sampleStem = 'Sample True False Question';
const trueFalseQuestion =
parse(`${sampleStem}{T}`)[0] as TrueFalseQuestion;
const sampleProps = {
questionTitle: 'Sample True/False Question',
correctAnswer: true,
question: trueFalseQuestion,
handleOnSubmitAnswer: mockHandleSubmitAnswer,
showAnswer: false
};
@ -19,7 +21,7 @@ describe('TrueFalseQuestion Component', () => {
beforeEach(() => {
render(
<MemoryRouter>
<TrueFalseQuestion questionContent={{ text: sampleStem, format: 'plain' }} {...sampleProps} />
<TrueFalseQuestionDisplay {...sampleProps} />
</MemoryRouter>);
});

View file

@ -1,10 +1,10 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { parse } from 'gift-pegjs';
import { MemoryRouter } from 'react-router-dom';
import { QuestionType } from '../../../../Types/QuestionType';
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
import { BaseQuestion, parse } from 'gift-pegjs';
import { QuestionType } from 'src/Types/QuestionType';
const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
@ -12,11 +12,10 @@ const mockGiftQuestions = parse(
::Sample Question 2:: Sample Question 2 {T}`);
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
question.id = (index + 1).toString();
const newMockQuestion: QuestionType = {
question: question,
};
return newMockQuestion;
if (question.type !== "Category")
question.id = (index + 1).toString();
const newMockQuestion = question;
return {question : newMockQuestion as BaseQuestion};
});
const mockSubmitAnswer = jest.fn();

View file

@ -3,7 +3,7 @@ import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import { screen } from '@testing-library/dom';
import '@testing-library/jest-dom';
import { parse } from 'gift-pegjs';
import { MultipleChoiceQuestion, parse } from 'gift-pegjs';
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
import { MemoryRouter } from 'react-router-dom';
@ -14,7 +14,11 @@ const mockGiftQuestions = parse(
describe('TeacherModeQuiz', () => {
const mockQuestion = mockGiftQuestions[0];
it ('renders the initial question as MultipleChoiceQuestion', () => {
expect(mockGiftQuestions[0].type).toBe('MC');
});
const mockQuestion = mockGiftQuestions[0] as MultipleChoiceQuestion;
mockQuestion.id = '1';
const mockSubmitAnswer = jest.fn();

View file

@ -0,0 +1,30 @@
import { parse, NumericalQuestion, SimpleNumericalAnswer, } from "gift-pegjs";
import { isSimpleNumericalAnswer } from "gift-pegjs/typeGuards";
describe('Numerical Question Tests', () => {
// ::Ulysses birthdate::When was Ulysses S. Grant born? {#1822}
it('should produce a valid Question object for a Numerical question with Title', () => {
const input = `
::Ulysses birthdate::When was Ulysses S. Grant born? {#1822}
`;
const result = parse(input);
// Type assertion to ensure result matches the Question interface
const question = result[0];
// Example assertions to check specific properties
expect(question).toHaveProperty('type', 'Numerical');
const numericalQuestion = question as NumericalQuestion;
expect(numericalQuestion.title).toBe('Ulysses birthdate');
expect(numericalQuestion.formattedStem.text).toBe('When was Ulysses S. Grant born?');
expect(numericalQuestion.choices).toBeDefined();
expect(numericalQuestion.choices).toHaveLength(1);
const choice = numericalQuestion.choices[0];
expect(isSimpleNumericalAnswer(choice)).toBe(true);
const c = choice as SimpleNumericalAnswer;
expect(c.type).toBe('simple');
expect(c.number).toBe(1822);
});
});

View file

@ -23,8 +23,8 @@ const GiftCheatSheet: React.FC = () => {
const QuestionVraiFaux = "2+2 \\= 4 ? {T}\n// Utilisez les valeurs {T}, {F}, {TRUE} \net {FALSE}.";
const QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}\n// La bonne réponse est Ottawa";
const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n}\n/ Utilisez tilde (signe de vague) pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse.";
const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n/ Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte.";
const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n}\n// Utilisez tilde (signe de vague) pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse.";
const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n// Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte.";
const QuestionNum ="// Question de plage mathématique. \n Quel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
return (
<div className="gift-cheat-sheet">
@ -123,7 +123,7 @@ const GiftCheatSheet: React.FC = () => {
</div>
<div className="question-type">
<h4> 7. Paramètres optionnels </h4>
<h4> 7. Caractères spéciaux </h4>
<p>
Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés,
réponses ou feedback, vous devez «échapper» ces derniers en ajoutant un \
@ -155,7 +155,7 @@ const GiftCheatSheet: React.FC = () => {
<div className="question-type">
<h4> 9. Images </h4>
<p>Pour insérer une image dans une question ou dans une réponse, vous devez utiliser la syntaxe suivante:</p>
<p>Il est possible d&apos;insérer une image dans une question, une réponse (choix multiple) et dans une rétroaction. D&apos;abord, <strong>le format de l&apos;élément doit être [markdown]</strong>. Ensuite utilisez la syntaxe suivante&nbsp;:</p>
<pre>
<code className="question-code-block">
{'!['}
@ -173,8 +173,14 @@ const GiftCheatSheet: React.FC = () => {
{'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'}
</code>
</pre>
<p>Exemple d&apos;une question à choix multiple avec l&apos;image d&apos;un chat dans une rétroaction&nbsp;:</p>
<pre>
<code className="question-code-block">
{`[markdown]Qui a initié le développement d'ÉvalueTonSavoir {=ÉTS#OUI! ![](https\\://www.etsmtl.ca/assets/img/ets.svg "\\=50px")
~EPFL#Non...}`}
</code>
</pre>
<p>Note&nbsp;: les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l&apos;URL de l&apos;image.</p>
<p>Note&nbsp;: On ne peut utiliser les images dans les messages de rétroaction (GIFT), car les rétroactions ne supportent pas le texte avec formatage (Markdown).</p>
<p style={{ color: 'red' }}>
Attention: l&apos;ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n&apos;est plus
supportée.

View file

@ -1,256 +1,21 @@
import Template, { ErrorTemplate } from './templates';
import { GIFTQuestion } from './templates/types';
import './styles.css';
import { parse } from 'gift-pegjs';
const multiple = parse(`
Who's buried in Grant's tomb? {~%-50%Grant=%50%Jefferson=%50%no one####Not sure? There are many answers for this question so do not fret. Not sure? There are many answers for this question so do not fret.}
Grant is _____ in Grant's tomb. {=buried#No one is buried there.=entombed~living}
Grant is buried in Grant's tomb. {FALSE}
Who's buried in Grant's tomb? {=no one=nobody}
When was Ulysses S. Grant born? {#1822:5}
What is the capital of Canada? {=Canada -> Ottawa =Italy -> Rome =Japan -> Tokyo}
`);
const multiple: GIFTQuestion[] = [
{
type: 'MC',
title: null,
stem: { format: 'markdown', text: "Who's buried in Grant's \r\n tomb?" },
hasEmbeddedAnswers: false,
globalFeedback: {
format: 'moodle',
text: 'Not sure? There are many answers for this question so do not fret. Not sure? There are many answers for this question so do not fret.'
},
choices: [
{
isCorrect: false,
weight: -50,
text: { format: 'moodle', text: 'Grant' },
feedback: null
},
{
isCorrect: true,
weight: 50,
text: { format: 'moodle', text: 'Jefferson' },
feedback: null
},
{
isCorrect: true,
weight: 50,
text: { format: 'moodle', text: 'no one' },
feedback: null
}
]
},
{
type: 'MC',
title: null,
stem: { format: 'moodle', text: "Grant is _____ in Grant's tomb." },
hasEmbeddedAnswers: true,
globalFeedback: null,
choices: [
{
isCorrect: true,
weight: null,
text: { format: 'moodle', text: 'buried' },
feedback: null
},
{
isCorrect: true,
weight: null,
text: { format: 'moodle', text: 'entombed' },
feedback: null
},
{
isCorrect: false,
weight: null,
text: { format: 'moodle', text: 'living' },
feedback: null
}
]
},
{
type: 'TF',
title: null,
stem: { format: 'moodle', text: "Grant is buried in Grant's tomb." },
hasEmbeddedAnswers: false,
globalFeedback: null,
isTrue: false,
falseFeedback: null,
trueFeedback: null
},
{
type: 'Short',
title: null,
stem: { format: 'moodle', text: "Who's buried in Grant's tomb?" },
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: [
{
isCorrect: true,
weight: null,
text: { format: 'moodle', text: 'no one " has got me' },
feedback: null
},
{
isCorrect: true,
weight: null,
text: { format: 'moodle', text: 'nobody' },
feedback: null
}
]
},
{
type: 'Numerical',
title: null,
stem: { format: 'moodle', text: 'When was Ulysses S. Grant born?' },
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: {
type: 'range',
number: 1822,
range: 5
}
},
{
type: 'Matching',
title: null,
stem: {
format: 'moodle',
text: 'Match the following countries with their corresponding capitals.'
},
hasEmbeddedAnswers: false,
globalFeedback: null,
matchPairs: [
{
subquestion: { format: 'moodle', text: 'Canada' },
subanswer: 'Ottawa'
},
{
subquestion: { format: 'moodle', text: 'Italy' },
subanswer: 'Rome'
},
{
subquestion: { format: 'moodle', text: 'Japan' },
subanswer: 'Tokyo'
}
]
},
{
type: 'MC',
title: "Grant's Tomb",
stem: { format: 'moodle', text: "Grant is _____ in Grant's tomb." },
hasEmbeddedAnswers: true,
globalFeedback: null,
choices: [
{
isCorrect: false,
weight: null,
text: { format: 'moodle', text: 'buried' },
feedback: { format: 'moodle', text: 'No one is buried there.' }
},
{
isCorrect: true,
weight: null,
text: { format: 'moodle', text: 'entombed' },
feedback: { format: 'moodle', text: 'Right answer!' }
},
{
isCorrect: false,
weight: null,
text: { format: 'moodle', text: 'living' },
feedback: { format: 'moodle', text: 'We hope not!' }
}
]
},
{
type: 'MC',
title: null,
stem: { format: 'moodle', text: 'Difficult multiple choice question.' },
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: [
{
isCorrect: false,
weight: null,
text: { format: 'moodle', text: 'wrong answer' },
feedback: { format: 'moodle', text: 'comment on wrong answer' }
},
{
isCorrect: false,
weight: 50,
text: { format: 'moodle', text: 'half credit answer' },
feedback: { format: 'moodle', text: 'comment on answer' }
},
{
isCorrect: true,
weight: null,
text: { format: 'moodle', text: 'full credit answer' },
feedback: { format: 'moodle', text: 'well done!' }
}
]
},
{
type: 'Short',
title: "Jesus' hometown (Short answer ex.)",
stem: { format: 'moodle', text: 'Jesus Christ was from _____ .' },
hasEmbeddedAnswers: true,
globalFeedback: null,
choices: [
{
isCorrect: true,
weight: null,
text: { format: 'moodle', text: 'Nazareth' },
feedback: { format: 'moodle', text: "Yes! That's right!" }
},
{
isCorrect: true,
weight: 75,
text: { format: 'moodle', text: 'Nazereth' },
feedback: { format: 'moodle', text: 'Right, but misspelled.' }
},
{
isCorrect: true,
weight: 25,
text: { format: 'moodle', text: 'Bethlehem' },
feedback: {
format: 'moodle',
text: 'He was born here, but not raised here.'
}
}
]
},
{
type: 'Numerical',
title: 'Numerical example',
stem: { format: 'moodle', text: 'When was Ulysses S. Grant born?' },
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: [
{
isCorrect: true,
weight: null,
text: {
type: 'range',
number: 1822,
range: 0
},
feedback: { format: 'moodle', text: 'Correct! 100% credit' }
},
{
isCorrect: true,
weight: 50,
text: {
type: 'range',
number: 1822,
range: 2
},
feedback: {
format: 'moodle',
text: 'He was born in 1822. You get 50% credit for being close.'
}
}
]
},
{
type: 'Essay',
title: 'Essay Example',
stem: { format: 'moodle', text: 'This is an essay.' },
hasEmbeddedAnswers: false,
globalFeedback: null
}
];
const items = multiple.map((item) => Template(item, { theme: 'dark' })).join('');
const errorItemDark = ErrorTemplate('Hello');

View file

@ -72,6 +72,11 @@
font-size: 2rem;
font-weight: 500;
}
.present-question-stem {
margin-bottom: 2vh;
}
.preview-container {
margin-bottom: 2vh;
width: 60vw;

View file

@ -1,14 +0,0 @@
import { TemplateOptions, Category as CategoryType } from './types';
import QuestionContainer from './QuestionContainer';
import Title from './Title';
type CategoryOptions = TemplateOptions & CategoryType;
export default function Category({ title }: CategoryOptions): string {
return `${QuestionContainer({
children: Title({
type: 'Catégorie',
title: title
})
})}`;
}

View file

@ -0,0 +1,15 @@
import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainerTemplate';
import Title from './TitleTemplate';
import { Category } from 'gift-pegjs';
type CategoryOptions = TemplateOptions & Category;
export default function CategoryTemplate( { title }: CategoryOptions): string {
return `${QuestionContainer({
children: Title({
type: 'Catégorie',
title: title
})
})}`;
}

View file

@ -1,22 +0,0 @@
import { TemplateOptions, Description as DescriptionType } from './types';
import QuestionContainer from './QuestionContainer';
import Title from './Title';
import textType from './TextType';
import { ParagraphStyle } from '../constants';
import { state } from '.';
type DescriptionOptions = TemplateOptions & DescriptionType;
export default function Description({ title, stem }: DescriptionOptions): string {
return `${QuestionContainer({
children: [
Title({
type: 'Description',
title: title
}),
`<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem
})}</p>`
]
})}`;
}

View file

@ -0,0 +1,19 @@
import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainerTemplate';
import Title from './TitleTemplate';
import { Description } from 'gift-pegjs';
import StemTemplate from './StemTemplate';
type DescriptionOptions = TemplateOptions & Description;
export default function DescriptionTemplate({ title, formattedStem}: DescriptionOptions): string {
return `${QuestionContainer({
children: [
Title({
type: 'Description',
title: title
}),
StemTemplate({formattedStem}),
]
})}`;
}

View file

@ -1,27 +0,0 @@
import { TemplateOptions, Essay as EssayType } from './types';
import QuestionContainer from './QuestionContainer';
import Title from './Title';
import textType from './TextType';
import GlobalFeedback from './GlobalFeedback';
import { ParagraphStyle, TextAreaStyle } from '../constants';
import { state } from '.';
type EssayOptions = TemplateOptions & EssayType;
export default function Essay({ title, stem, globalFeedback }: EssayOptions): string {
return `${QuestionContainer({
children: [
Title({
type: 'Développement',
title: title
}),
`<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem
})}</p>`,
`<textarea class="gift-textarea" style="${TextAreaStyle(
state.theme
)}" placeholder="Entrez votre réponse ici..."></textarea>`,
GlobalFeedback({ feedback: globalFeedback })
]
})}`;
}

View file

@ -0,0 +1,27 @@
import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainerTemplate';
import Title from './TitleTemplate';
import GlobalFeedbackTemplate from './GlobalFeedbackTemplate';
import { TextAreaStyle } from '../constants';
import { state } from '.';
import { EssayQuestion } from 'gift-pegjs';
import StemTemplate from './StemTemplate';
type EssayOptions = TemplateOptions & EssayQuestion;
export default function EssayTemplate({ title, formattedStem, formattedGlobalFeedback }: EssayOptions): string {
return `${QuestionContainer({
children: [
Title({
type: 'Développement',
title: title
}),
StemTemplate({formattedStem}),
`<textarea class="gift-textarea" style="${TextAreaStyle(
state.theme
)}" placeholder="Entrez votre réponse ici..."></textarea>`,
(formattedGlobalFeedback && formattedGlobalFeedback.text) ?
GlobalFeedbackTemplate(formattedGlobalFeedback) : ``
]
})}`;
}

View file

@ -1,13 +1,12 @@
import { TemplateOptions, Question } from './types';
import textType from './TextType';
import { TemplateOptions } from './types';
import {FormattedTextTemplate} from './TextTypeTemplate';
import { state } from '.';
import { theme } from '../constants';
import { TextFormat } from 'gift-pegjs';
interface GlobalFeedbackOptions extends TemplateOptions {
feedback: Question['globalFeedback'];
}
type GlobalFeedbackOptions = TemplateOptions & TextFormat;
export default function GlobalFeedback({ feedback }: GlobalFeedbackOptions): string {
export default function GlobalFeedbackTemplate({ format, text }: GlobalFeedbackOptions): string {
const Container = `
position: relative;
margin-top: 1rem;
@ -19,9 +18,9 @@ export default function GlobalFeedback({ feedback }: GlobalFeedbackOptions): str
box-shadow: 0px 2px 5px ${theme(state.theme, 'gray400', 'black800')};
`;
return feedback !== null
return (format && text)
? `<div style="${Container}">
<p>${textType({ text: feedback })}</p>
<p>${FormattedTextTemplate({format: format, text: text})}</p>
</div>`
: ``;
}

View file

@ -1,22 +1,24 @@
import { TemplateOptions, Matching as MatchingType } from './types';
import QuestionContainer from './QuestionContainer';
import Title from './Title';
import textType from './TextType';
import GlobalFeedback from './GlobalFeedback';
import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainerTemplate';
import Title from './TitleTemplate';
import {FormattedTextTemplate} from './TextTypeTemplate';
import GlobalFeedback from './GlobalFeedbackTemplate';
import { ParagraphStyle, SelectStyle } from '../constants';
import { state } from '.';
import { MatchingQuestion } from 'gift-pegjs';
import StemTemplate from './StemTemplate';
type MatchingOptions = TemplateOptions & MatchingType;
type MatchingOptions = TemplateOptions & MatchingQuestion;
interface MatchAnswerOptions extends TemplateOptions {
choices: MatchingType['matchPairs'];
choices: MatchingQuestion['matchPairs'];
}
export default function Matching({
export default function MatchingTemplate({
title,
stem,
formattedStem,
matchPairs,
globalFeedback
formattedGlobalFeedback
}: MatchingOptions): string {
return `${QuestionContainer({
children: [
@ -24,11 +26,9 @@ export default function Matching({
type: 'Appariement',
title: title
}),
`<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem
})}</p>`,
StemTemplate({formattedStem}),
MatchAnswers({ choices: matchPairs }),
GlobalFeedback({ feedback: globalFeedback })
formattedGlobalFeedback ? GlobalFeedback(formattedGlobalFeedback) : ''
]
})}`;
}
@ -64,10 +64,10 @@ function MatchAnswers({ choices }: MatchAnswerOptions): string {
const uniqueMatchOptions = Array.from(new Set(choices.map(({ subanswer }) => subanswer)));
const result = choices
.map(({ subquestion }) => {
.map(({ formattedSubquestion }) => {
return `
<div style="${OptionTable} ${ParagraphStyle(state.theme)}">
${textType({ text: subquestion })}
${FormattedTextTemplate(formattedSubquestion)}
</div>
<div>
<select class="gift-select" style="${SelectStyle(state.theme)} ${Dropdown}">

View file

@ -1,31 +0,0 @@
import { TemplateOptions, MultipleChoice as MultipleChoiceType } from './types';
import QuestionContainer from './QuestionContainer';
import GlobalFeedback from './GlobalFeedback';
import Title from './Title';
import textType from './TextType';
import MultipleChoiceAnswers from './MultipleChoiceAnswers';
import { ParagraphStyle } from '../constants';
import { state } from '.';
type MultipleChoiceOptions = TemplateOptions & MultipleChoiceType;
export default function MultipleChoice({
title,
stem,
choices,
globalFeedback
}: MultipleChoiceOptions): string {
return `${QuestionContainer({
children: [
Title({
type: 'Choix multiple',
title: title
}),
`<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem
})}</p>`,
MultipleChoiceAnswers({ choices: choices }),
GlobalFeedback({ feedback: globalFeedback })
]
})}`;
}

View file

@ -1,89 +0,0 @@
import { nanoid } from 'nanoid';
import { TemplateOptions, TextFormat, Choice, MultipleChoice as MultipleChoiceType } from './types';
import textType from './TextType';
import AnswerIcon from './AnswerIcon';
import { state } from '.';
import { ParagraphStyle, theme } from '../constants';
type MultipleChoiceAnswerOptions = TemplateOptions & Pick<MultipleChoiceType, 'choices'>;
type AnswerFeedbackOptions = TemplateOptions & Pick<Choice, 'feedback'>;
interface AnswerWeightOptions extends TemplateOptions {
weight: Choice['weight'];
correct: Choice['isCorrect'];
}
export default function MultipleChoiceAnswers({ choices }: MultipleChoiceAnswerOptions) {
const id = `id${nanoid(8)}`;
const isMultipleAnswer = choices.filter(({ isCorrect }) => isCorrect === true).length === 0;
const prompt = `<span style="${ParagraphStyle(state.theme)}">Choisir une réponse${
isMultipleAnswer ? ` ou plusieurs` : ``
}:</span>`;
const result = choices
.map(({ weight, isCorrect, text, feedback }) => {
const CustomLabel = `
display: inline-block;
padding: 0.2em 0 0.2em 0;
`;
const inputId = `id${nanoid(6)}`;
const isPositiveWeight = weight !== null && weight > 0;
const isCorrectOption = isMultipleAnswer ? isPositiveWeight : isCorrect;
return `
<div class='multiple-choice-answers-container'>
<input class="gift-input" type="${
isMultipleAnswer ? 'checkbox' : 'radio'
}" id="${inputId}" name="${id}">
${AnswerWeight({ correct: isCorrectOption, weight: weight })}
<label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}">
${textType({ text: text as TextFormat })}
</label>
${AnswerIcon({ correct: isCorrectOption })}
${AnswerFeedback({ feedback: feedback })}
</input>
</div>
`;
})
.join('');
return `${prompt}${result}`;
}
function AnswerWeight({ weight, correct }: AnswerWeightOptions): string {
const Container = `
box-shadow: 0px 1px 1px ${theme(state.theme, 'gray400', 'black900')};
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
`;
const CorrectWeight = `
color: ${theme(state.theme, 'green700', 'green100')};
background-color: ${theme(state.theme, 'green100', 'greenGray700')};
`;
const IncorrectWeight = `
color: ${theme(state.theme, 'beige600', 'beige100')};
background-color: ${theme(state.theme, 'beige300', 'beigeGray800')};
`;
return weight
? `<span style="${Container} ${
correct ? `${CorrectWeight}` : `${IncorrectWeight}`
}">${weight}%</span>`
: ``;
}
function AnswerFeedback({ feedback }: AnswerFeedbackOptions): string {
const Container = `
color: ${theme(state.theme, 'teal700', 'gray700')};
`;
return feedback ? `<span style="${Container}">${textType({ text: feedback })}</span>` : ``;
}

View file

@ -0,0 +1,63 @@
import { nanoid } from 'nanoid';
import { TemplateOptions } from './types';
import {FormattedTextTemplate} from './TextTypeTemplate';
import AnswerIcon from './AnswerIconTemplate';
import { state } from '.';
import { ParagraphStyle } from '../constants';
import { MultipleChoiceQuestion, TextChoice } from 'gift-pegjs';
type MultipleChoiceAnswerOptions = TemplateOptions & Pick<MultipleChoiceQuestion, 'choices'>;
type AnswerFeedbackOptions = TemplateOptions & Pick<TextChoice, 'formattedFeedback'>;
interface AnswerWeightOptions extends TemplateOptions {
weight: TextChoice['weight'];
}
export default function MultipleChoiceAnswersTemplate({ choices }: MultipleChoiceAnswerOptions) {
const id = `id${nanoid(8)}`;
const isMultipleAnswer = choices.filter(({ isCorrect }) => isCorrect === true).length === 0;
const prompt = `<span style="${ParagraphStyle(state.theme)}">Choisir une réponse${
isMultipleAnswer ? ` ou plusieurs` : ``
}:</span>`;
const result = choices
.map(({ weight, isCorrect, formattedText, formattedFeedback }) => {
const CustomLabel = `
display: inline-block;
padding: 0.2em 0 0.2em 0;
`;
const inputId = `id${nanoid(6)}`;
const isPositiveWeight = (weight != undefined) && (weight > 0);
const isCorrectOption = isMultipleAnswer ? isPositiveWeight : isCorrect;
return `
<div class='multiple-choice-answers-container'>
<input class="gift-input" type="${
isMultipleAnswer ? 'checkbox' : 'radio'
}" id="${inputId}" name="${id}">
${AnswerWeight({ weight: weight })}
<label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}">
${FormattedTextTemplate(formattedText)}
</label>
${AnswerIcon({ correct: isCorrectOption })}
${AnswerFeedback({ formattedFeedback: formattedFeedback })}
</input>
</div>
`;
})
.join('');
return `${prompt}${result}`;
}
function AnswerWeight({ weight }: AnswerWeightOptions): string {
return weight ? `<span class="answer-weight-container ${weight > 0 ? 'answer-positive-weight' : 'answer-zero-or-less-weight'}">${weight}%</span>` : ``;
}
function AnswerFeedback({ formattedFeedback }: AnswerFeedbackOptions): string {
return formattedFeedback ? `<span class="feedback-container">${FormattedTextTemplate(formattedFeedback)}</span>` : ``;
}

View file

@ -0,0 +1,28 @@
import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainerTemplate';
import GlobalFeedback from './GlobalFeedbackTemplate';
import Title from './TitleTemplate';
import MultipleChoiceAnswers from './MultipleChoiceAnswersTemplate';
import { MultipleChoiceQuestion } from 'gift-pegjs';
import StemTemplate from './StemTemplate';
type MultipleChoiceOptions = TemplateOptions & MultipleChoiceQuestion;
export default function MultipleChoiceTemplate({
title,
formattedStem,
choices,
formattedGlobalFeedback
}: MultipleChoiceOptions): string {
return `${QuestionContainer({
children: [
Title({
type: 'Choix multiple',
title: title
}),
StemTemplate({formattedStem}),
MultipleChoiceAnswers({ choices: choices }),
formattedGlobalFeedback ? GlobalFeedback(formattedGlobalFeedback) : ''
]
})}`;
}

View file

@ -1,60 +0,0 @@
import { TemplateOptions, Numerical as NumericalType, NumericalFormat } from './types';
import QuestionContainer from './QuestionContainer';
import Title from './Title';
import textType from './TextType';
import GlobalFeedback from './GlobalFeedback';
import { ParagraphStyle, InputStyle } from '../constants';
import { state } from '.';
type NumericalOptions = TemplateOptions & NumericalType;
type NumericalAnswerOptions = TemplateOptions & Pick<NumericalType, 'choices'>;
export default function Numerical({
title,
stem,
choices,
globalFeedback
}: NumericalOptions): string {
return `${QuestionContainer({
children: [
Title({
type: 'Numérique',
title: title
}),
`<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem
})}</p>`,
NumericalAnswers({ choices: choices }),
GlobalFeedback({ feedback: globalFeedback })
]
})}`;
}
function NumericalAnswers({ choices }: NumericalAnswerOptions): string {
const placeholder = Array.isArray(choices)
? choices.map(({ text }) => Answer(text)).join(', ')
: Answer(choices);
return `
<div>
<span style="${ParagraphStyle(
state.theme
)}">Réponse: </span><input class="gift-input" type="text" style="${InputStyle(
state.theme
)}" placeholder="${placeholder}">
</div>
`;
}
function Answer({ type, number, range, numberLow, numberHigh }: NumericalFormat): string {
switch (type) {
case 'simple':
return `${number}`;
case 'range':
return `${number} ± ${range}`;
case 'high-low':
return `${numberLow} - ${numberHigh}`;
default:
return ``;
}
}

View file

@ -0,0 +1,97 @@
import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainerTemplate';
import Title from './TitleTemplate';
import GlobalFeedback from './GlobalFeedbackTemplate';
import { ParagraphStyle, InputStyle } from '../constants';
import { state } from '.';
import { NumericalAnswer, NumericalQuestion, TextFormat } from 'gift-pegjs';
import { isHighLowNumericalAnswer, isMultipleNumericalAnswer, isRangeNumericalAnswer, isSimpleNumericalAnswer } from 'gift-pegjs/typeGuards';
import StemTemplate from './StemTemplate';
import { FormattedTextTemplate } from './TextTypeTemplate';
type NumericalOptions = TemplateOptions & NumericalQuestion;
type NumericalAnswerOptions = TemplateOptions & Pick<NumericalQuestion, 'choices'>;
export default function NumericalTemplate({
title,
formattedStem,
choices,
formattedGlobalFeedback
}: NumericalOptions): string {
return `${QuestionContainer({
children: [
Title({
type: 'Numérique',
title: title
}),
StemTemplate({formattedStem}),
NumericalAnswers2({ choices: choices }),
formattedGlobalFeedback ? GlobalFeedback(formattedGlobalFeedback) : ''
]
})}`;
}
function NumericalAnswers2({ choices }: NumericalAnswerOptions): string {
// for each choice, return a separate <input> for each answer - answer should show weight and feedback
const answers = choices.map(choice => {
if (isMultipleNumericalAnswer(choice)) {
return { answer: Answer(choice.answer), weight: choice.weight, feedback: choice.formattedFeedback };
} else {
return { answer: Answer(choice) };
}
});
let result = '<div>';
answers.forEach(answer => {
const weight = answer.weight ?
`<span class="numerical-answer-weight-container ${answer.weight > 0 ? 'answer-positive-weight' : 'answer-zero-or-less-weight'}">${answer.weight}%</span>` :
''
result +=
`<p style="${ParagraphStyle(state.theme)}">Réponse: </p>`
+ `<input class="gift-input" type="text" style="${InputStyle(state.theme)};width: 100%" placeholder="${answer.answer}">`
+ weight
+ (answer.feedback ? `<div class="feedback-container">${FormattedTextTemplate(answer.feedback)}</div>` : '');
});
result += '</div>';
return result;
}
// function NumericalAnswers({ choices }: NumericalAnswerOptions): string {
// const placeholder = isMultipleNumericalAnswer(choices[0])
// ? choices.map(choice => {
// console.log(JSON.stringify(choice));
// const c = choice as MultipleNumericalAnswer;
// return Answer(c.answer, c.formattedFeedback)
// }).join(', ')
// : Answer(choices[0]);
// // TODO return a separate <input> for each answer - answer should show weight and feedback
// return `
// <div>
// <span style="${ParagraphStyle(
// state.theme
// )}">Réponse: </span><input class="gift-input" type="text" style="${InputStyle(
// state.theme
// )}" placeholder="${placeholder}">
// </div>
// `;
// }
function Answer(choice: NumericalAnswer, formattedFeedback?: TextFormat): string {
const formattedFeedbackString = formattedFeedback ? ` (${FormattedTextTemplate(formattedFeedback)})` : '';
switch (true) {
case isSimpleNumericalAnswer(choice):
return `${choice.number}${formattedFeedbackString}`;
case isRangeNumericalAnswer(choice):
return `${choice.number} &plusmn; ${choice.range}${formattedFeedbackString}`;
case isHighLowNumericalAnswer(choice):
return `${choice.numberLow}..${choice.numberHigh}${formattedFeedbackString}`;
default:
return ``;
}
}

View file

@ -1,46 +0,0 @@
import { TemplateOptions, ShortAnswer as ShortAnswerType, TextFormat } from './types';
import QuestionContainer from './QuestionContainer';
import Title from './Title';
import textType from './TextType';
import GlobalFeedback from './GlobalFeedback';
import { ParagraphStyle, InputStyle } from '../constants';
import { state } from './index';
type ShortAnswerOptions = TemplateOptions & ShortAnswerType;
type AnswerOptions = TemplateOptions & Pick<ShortAnswerType, 'choices'>;
export default function ShortAnswer({
title,
stem,
choices,
globalFeedback
}: ShortAnswerOptions): string {
return `${QuestionContainer({
children: [
Title({
type: 'Réponse courte',
title: title
}),
`<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem
})}</p>`,
Answers({ choices: choices }),
GlobalFeedback({ feedback: globalFeedback })
]
})}`;
}
function Answers({ choices }: AnswerOptions): string {
const placeholder = choices
.map(({ text }) => textType({ text: text as TextFormat }))
.join(', ');
return `
<div>
<span style="${ParagraphStyle(
state.theme
)}">Réponse: </span><input class="gift-input" type="text" style="${InputStyle(
state.theme
)}" placeholder="${placeholder}">
</div>
`;
}

View file

@ -0,0 +1,46 @@
import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainerTemplate';
import Title from './TitleTemplate';
import {FormattedTextTemplate} from './TextTypeTemplate';
import GlobalFeedback from './GlobalFeedbackTemplate';
import { ParagraphStyle, InputStyle } from '../constants';
import { state } from './index';
import { ShortAnswerQuestion } from 'gift-pegjs';
import StemTemplate from './StemTemplate';
type ShortAnswerOptions = TemplateOptions & ShortAnswerQuestion;
type AnswerOptions = TemplateOptions & Pick<ShortAnswerQuestion, 'choices'>;
export default function ShortAnswerTemplate({
title,
formattedStem,
choices,
formattedGlobalFeedback
}: ShortAnswerOptions): string {
return `${QuestionContainer({
children: [
Title({
type: 'Réponse courte',
title: title
}),
StemTemplate({formattedStem}),
Answers({ choices: choices }),
formattedGlobalFeedback ? GlobalFeedback(formattedGlobalFeedback) : ''
]
})}`;
}
function Answers({ choices }: AnswerOptions): string {
const placeholder = choices
.map(({ text }) => FormattedTextTemplate({ format: '', text: text }))
.join(', ');
return `
<div>
<span style="${ParagraphStyle(
state.theme
)}">Réponse: </span><input class="gift-input" type="text" style="${InputStyle(
state.theme
)}" placeholder="${placeholder}">
</div>
`;
}

View file

@ -0,0 +1,26 @@
import { TemplateOptions } from './types';
import { state } from '.';
import { ParagraphStyle } from '../constants';
import { BaseQuestion } from 'gift-pegjs';
import { FormattedTextTemplate } from './TextTypeTemplate';
// Type is string to allow for custom question type text (e,g, "Multiple Choice")
interface StemOptions extends TemplateOptions {
formattedStem: BaseQuestion['formattedStem'];
}
export default function StemTemplate({ formattedStem }: StemOptions): string {
const Container = `
display: flex;
`;
return `
<div style="${Container}">
<span>
<p style="${ParagraphStyle(state.theme)}" class="present-question-stem">
${FormattedTextTemplate(formattedStem)}
</p>
</span>
</div>
`;
}

View file

@ -1,10 +1,8 @@
import { marked } from 'marked';
import katex from 'katex';
import { TemplateOptions, TextFormat } from './types';
import marked from 'src/markedConfig';
interface TextTypeOptions extends TemplateOptions {
text: TextFormat;
}
import katex from 'katex';
import { TextFormat } from 'gift-pegjs';
import DOMPurify from 'dompurify'; // cleans HTML to prevent XSS attacks, etc.
export function formatLatex(text: string): string {
return text
@ -28,23 +26,29 @@ export function formatLatex(text: string): string {
* @see marked
* @see katex
*/
export default function textType({ text }: TextTypeOptions) {
const formatText = formatLatex(text.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped
export function FormattedTextTemplate(formattedText: TextFormat): string {
const formatText = formatLatex(formattedText.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped
let parsedText = '';
switch (text.format) {
let result = '';
switch (formattedText.format) {
case '':
case 'moodle':
case 'plain':
// Replace newlines with <br> tags
return replaceNewlinesOutsideSVG(formatText);
result = replaceNewlinesOutsideSVG(formatText);
break;
case 'html':
// Strip outer paragraph tags (not a great approach with regex)
return formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
result = formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
break;
case 'markdown':
parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219
return parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
parsedText = marked.parse(formatText, { breaks: true, gfm: true }) as string; // <br> for newlines
result = parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
break;
default:
throw new Error(`Unsupported text format: ${text.format}`);
throw new Error(`Unsupported text format: ${formattedText.format}`);
}
return DOMPurify.sanitize(result);
}
// Function to replace \n outside of SVG paths

View file

@ -1,6 +1,7 @@
import { TemplateOptions, Question } from './types';
import { TemplateOptions } from './types';
import { state } from '.';
import { theme } from '../constants';
import { Question } from 'gift-pegjs';
// Type is string to allow for custom question type text (e,g, "Multiple Choice")
interface TitleOptions extends TemplateOptions {
@ -43,7 +44,7 @@ export default function Title({ type, title }: TitleOptions): string {
<div style="${Container}">
<span>
${
title !== null
title
? `<span style="${QuestionTitle}">${title}</span>`
: `<span style="${OptionalTitle}"><em>(Sans titre)</em></span>`
}

View file

@ -1,54 +0,0 @@
import { TemplateOptions, TextChoice, TrueFalse as TrueFalseType } from './types';
import QuestionContainer from './QuestionContainer';
import textType from './TextType';
import GlobalFeedback from './GlobalFeedback';
import MultipleChoiceAnswers from './MultipleChoiceAnswers';
import Title from './Title';
import { ParagraphStyle } from '../constants';
import { state } from '.';
type TrueFalseOptions = TemplateOptions & TrueFalseType;
export default function TrueFalse({
title,
isTrue,
stem,
trueFeedback: trueFeedback,
falseFeedback: falseFeedback,
globalFeedback
}: TrueFalseOptions): string {
const choices: TextChoice[] = [
{
text: {
format: 'moodle',
text: 'Vrai'
},
isCorrect: isTrue,
weight: null,
feedback: isTrue ? trueFeedback : falseFeedback
},
{
text: {
format: 'moodle',
text: 'Faux'
},
isCorrect: !isTrue,
weight: null,
feedback: !isTrue ? trueFeedback : falseFeedback
}
];
return `${QuestionContainer({
children: [
Title({
type: 'Vrai/Faux',
title: title
}),
`<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem
})}</p>`,
MultipleChoiceAnswers({ choices: choices }),
GlobalFeedback({ feedback: globalFeedback })
]
})}`;
}

View file

@ -0,0 +1,47 @@
import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainerTemplate';
import GlobalFeedback from './GlobalFeedbackTemplate';
import MultipleChoiceAnswersTemplate from './MultipleChoiceAnswersTemplate';
import Title from './TitleTemplate';
import { TextChoice, TrueFalseQuestion } from 'gift-pegjs';
import StemTemplate from './StemTemplate';
type TrueFalseOptions = TemplateOptions & TrueFalseQuestion;
export default function TrueFalseTemplate({
isTrue,
title,
formattedStem,
trueFormattedFeedback, falseFormattedFeedback,
formattedGlobalFeedback
}: TrueFalseOptions): string {
const choices: TextChoice[] = [
{
formattedText: {
format: 'moodle',
text: 'Vrai'
},
isCorrect: isTrue,
formattedFeedback: trueFormattedFeedback
},
{
formattedText: {
format: 'moodle',
text: 'Faux'
},
isCorrect: !isTrue,
formattedFeedback: falseFormattedFeedback
}
];
return `${QuestionContainer({
children: [
Title({
type: 'Vrai/Faux',
title: title
}),
StemTemplate({formattedStem}),
MultipleChoiceAnswersTemplate({ choices: choices }),
formattedGlobalFeedback ? GlobalFeedback(formattedGlobalFeedback) : ``
]
})}`;
}

View file

@ -1,24 +1,25 @@
import Category from './Category';
import Description from './Description';
import Essay from './Essay';
import Matching from './Matching';
import MultipleChoice from './MultipleChoice';
import Numerical from './Numerical';
import ShortAnswer from './ShortAnswer';
import TrueFalse from './TrueFalse';
import Error from './Error';
import {
GIFTQuestion,
ParsedGIFTQuestion as GIFTQuestion,
// Category as CategoryType,
// Description as DescriptionType,
MultipleChoice as MultipleChoiceType,
Numerical as NumericalType,
ShortAnswer as ShortAnswerType,
MultipleChoiceQuestion as MultipleChoiceType,
NumericalQuestion as NumericalType,
ShortAnswerQuestion as ShortAnswerType,
// Essay as EssayType,
TrueFalse as TrueFalseType,
Matching as MatchingType,
DisplayOptions
} from './types';
TrueFalseQuestion as TrueFalseType,
// MatchingQuestion as MatchingType,
} from 'gift-pegjs';
import { DisplayOptions } from './types';
import DescriptionTemplate from './DescriptionTemplate';
import EssayTemplate from './EssayTemplate';
import MatchingTemplate from './MatchingTemplate';
import MultipleChoiceTemplate from './MultipleChoiceTemplate';
import NumericalTemplate from './NumericalTemplate';
import ShortAnswerTemplate from './ShortAnswerTemplate';
import TrueFalseTemplate from './TrueFalseTemplate';
import Error from './ErrorTemplate';
import CategoryTemplate from './CategoryTemplate';
export const state: DisplayOptions = { preview: true, theme: 'light' };
@ -37,21 +38,21 @@ export default function Template(
// ...(keys as DescriptionType)
// });
case 'MC':
return MultipleChoice({
return MultipleChoiceTemplate({
...(keys as MultipleChoiceType)
});
case 'Numerical':
return Numerical({ ...(keys as NumericalType) });
return NumericalTemplate({ ...(keys as NumericalType) });
case 'Short':
return ShortAnswer({
return ShortAnswerTemplate({
...(keys as ShortAnswerType)
});
// case 'Essay':
// return Essay({ ...(keys as EssayType) });
case 'TF':
return TrueFalse({ ...(keys as TrueFalseType) });
case 'Matching':
return Matching({ ...(keys as MatchingType) });
return TrueFalseTemplate({ ...(keys as TrueFalseType) });
// case 'Matching':
// return Matching({ ...(keys as MatchingType) });
default:
// TODO: throw error for unsupported question types?
// throw new Error(`Unsupported question type: ${type}`);
@ -66,13 +67,13 @@ export function ErrorTemplate(text: string, options?: Partial<DisplayOptions>):
}
export {
Category,
Description,
Essay,
Matching,
MultipleChoice,
Numerical,
ShortAnswer,
TrueFalse,
CategoryTemplate,
DescriptionTemplate as Description,
EssayTemplate as Essay,
MatchingTemplate as Matching,
MultipleChoiceTemplate as MultipleChoice,
NumericalTemplate as Numerical,
ShortAnswerTemplate as ShortAnswer,
TrueFalseTemplate as TrueFalse,
Error
};

View file

@ -12,13 +12,13 @@ export interface DisplayOptions {
preview: boolean;
}
export {
QuestionType, FormatType, NumericalType, TextFormat, NumericalFormat, TextChoice, NumericalChoice, Question, Description, Category, MultipleChoice, ShortAnswer, Numerical, Essay, TrueFalse,
Matching, Match, GIFTQuestion } from 'gift-pegjs';
// export {
// QuestionType, FormatType, NumericalType, TextFormat, NumericalFormat, TextChoice, NumericalChoice, Question, Description, Category, MultipleChoice, ShortAnswer, Numerical, Essay, TrueFalse,
// Matching, Match, GIFTQuestion } from 'gift-pegjs';
export interface Choice {
isCorrect: boolean;
weight: number | null;
text: TextFormat | NumericalFormat;
feedback: TextFormat | null;
}
// export interface Choice {
// isCorrect: boolean;
// weight: number | null;
// text: TextFormat | NumericalFormat;
// feedback: TextFormat | null;
// }

View file

@ -20,7 +20,7 @@ import {
TableRow
} from '@mui/material';
import { StudentType } from '../../Types/StudentType';
import { formatLatex } from '../GiftTemplate/templates/TextType';
import { formatLatex } from '../GiftTemplate/templates/TextTypeTemplate';
interface LiveResultsProps {
socket: Socket | null;

View file

@ -1,80 +0,0 @@
// NumericalQuestion.tsx
import React, { useState } from 'react';
import '../questionStyle.css';
import { Button, TextField } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
type CorrectAnswer = {
numberHigh?: number;
numberLow?: number;
number?: number;
type: string;
};
interface Props {
questionContent: TextFormat;
correctAnswers: CorrectAnswer;
globalFeedback?: string | undefined;
handleOnSubmitAnswer?: (answer: number) => void;
showAnswer?: boolean;
}
const NumericalQuestion: React.FC<Props> = (props) => {
const { questionContent, correctAnswers, showAnswer, handleOnSubmitAnswer, globalFeedback } =
props;
const [answer, setAnswer] = useState<number>();
const correctAnswer =
correctAnswers.type === 'high-low'
? `Entre ${correctAnswers.numberLow} et ${correctAnswers.numberHigh}`
: correctAnswers.number;
return (
<div className="question-wrapper">
<div>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div>
{showAnswer ? (
<>
<div className="correct-answer-text mb-2">{correctAnswer}</div>
{globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}
</>
) : (
<>
<div className="answer-wrapper mb-1">
<TextField
type="number"
id={questionContent.text}
name={questionContent.text}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAnswer(e.target.valueAsNumber);
}}
inputProps={{ 'data-testid': 'number-input' }}
/>
</div>
{globalFeedback && showAnswer && (
<div className="global-feedback mb-2">{globalFeedback}</div>
)}
{handleOnSubmitAnswer && (
<Button
variant="contained"
onClick={() =>
answer !== undefined &&
handleOnSubmitAnswer &&
handleOnSubmitAnswer(answer)
}
disabled={answer === undefined || isNaN(answer)}
>
Répondre
</Button>
)}
</>
)}
</div>
);
};
export default NumericalQuestion;

View file

@ -1,109 +0,0 @@
// Question;tsx
import React, { useMemo } from 'react';
import { GIFTQuestion } from 'gift-pegjs';
import TrueFalseQuestion from './TrueFalseQuestion/TrueFalseQuestion';
import MultipleChoiceQuestion from './MultipleChoiceQuestion/MultipleChoiceQuestion';
import NumericalQuestion from './NumericalQuestion/NumericalQuestion';
import ShortAnswerQuestion from './ShortAnswerQuestion/ShortAnswerQuestion';
import useCheckMobileScreen from '../../services/useCheckMobileScreen';
interface QuestionProps {
question: GIFTQuestion | undefined;
handleOnSubmitAnswer?: (answer: string | number | boolean) => void;
showAnswer?: boolean;
imageUrl?: string;
}
const Question: React.FC<QuestionProps> = ({
question,
handleOnSubmitAnswer,
showAnswer,
imageUrl
}) => {
const isMobile = useCheckMobileScreen();
const imgWidth = useMemo(() => {
return isMobile ? '100%' : '20%';
}, [isMobile]);
let questionTypeComponent = null;
switch (question?.type) {
case 'TF':
questionTypeComponent = (
<TrueFalseQuestion
questionContent={question.stem}
correctAnswer={question.isTrue}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text}
/>
);
break;
case 'MC':
questionTypeComponent = (
<MultipleChoiceQuestion
questionStem={question.stem}
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text}
/>
);
break;
case 'Numerical':
if (question.choices) {
if (!Array.isArray(question.choices)) {
questionTypeComponent = (
<NumericalQuestion
questionContent={question.stem}
correctAnswers={question.choices}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text}
/>
);
} else {
questionTypeComponent = (
<NumericalQuestion
questionContent={question.stem}
correctAnswers={question.choices[0].text}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text}
/>
);
}
}
break;
case 'Short':
questionTypeComponent = (
<ShortAnswerQuestion
questionContent={question.stem}
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text}
/>
);
break;
}
return (
<div className="question-container">
{questionTypeComponent ? (
<>
{imageUrl && (
<img
src={imageUrl}
alt="QuestionImage"
style={{ width: imgWidth, marginBottom: '2rem' }}
/>
)}
{questionTypeComponent}
</>
) : (
<div>Question de type inconnue</div>
)}
</div>
);
};
export default Question;

View file

@ -1,35 +1,24 @@
// MultipleChoiceQuestion.tsx
// MultipleChoiceQuestionDisplay.tsx
import React, { useEffect, useState } from 'react';
import '../questionStyle.css';
import { Button } from '@mui/material';
import textType, { formatLatex } from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
// import Latex from 'react-latex';
type Choices = {
feedback: { format: string; text: string } | null;
isCorrect: boolean;
text: { format: string; text: string };
weigth?: number;
};
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { MultipleChoiceQuestion } from 'gift-pegjs';
interface Props {
questionStem: TextFormat;
choices: Choices[];
globalFeedback?: string | undefined;
question: MultipleChoiceQuestion;
handleOnSubmitAnswer?: (answer: string) => void;
showAnswer?: boolean;
}
const MultipleChoiceQuestion: React.FC<Props> = (props) => {
const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
const { questionStem: questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props;
const { question, showAnswer, handleOnSubmitAnswer } = props;
const [answer, setAnswer] = useState<string>();
useEffect(() => {
setAnswer(undefined);
}, [questionContent]);
}, [question]);
const handleOnClickAnswer = (choice: string) => {
setAnswer(choice);
@ -41,38 +30,40 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
return (
<div className="question-container">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div>
<div className="choices-wrapper mb-1">
{choices.map((choice, i) => {
const selected = answer === choice.text.text ? 'selected' : '';
{question.choices.map((choice, i) => {
const selected = answer === choice.formattedText.text ? 'selected' : '';
return (
<div key={choice.text.text + i} className="choice-container">
<div key={choice.formattedText.text + i} className="choice-container">
<Button
variant="text"
className="button-wrapper"
onClick={() => !showAnswer && handleOnClickAnswer(choice.text.text)}
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}
>
{choice.feedback === null &&
{choice.formattedFeedback === null &&
showAnswer &&
(choice.isCorrect ? '✅' : '❌')}
<div className={`circle ${selected}`}>{alphabet[i]}</div>
<div className={`answer-text ${selected}`}>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(formatLatex(choice.text.text)) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedText) }} />
</div>
</Button>
{choice.feedback && showAnswer && (
{choice.formattedFeedback && showAnswer && (
<div className="feedback-container mb-1 mt-1/2">
{choice.isCorrect ? '✅' : '❌'}
{choice.feedback?.text}
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedFeedback) }} />
</div>
)}
</div>
);
})}
</div>
{globalFeedback && showAnswer && (
<div className="global-feedback mb-2">{globalFeedback}</div>
{question.formattedGlobalFeedback && showAnswer && (
<div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>
)}
{!showAnswer && handleOnSubmitAnswer && (
@ -92,4 +83,4 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
);
};
export default MultipleChoiceQuestion;
export default MultipleChoiceQuestionDisplay;

View file

@ -0,0 +1,89 @@
// NumericalQuestion.tsx
import React, { useState } from 'react';
import '../questionStyle.css';
import { Button, TextField } from '@mui/material';
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { NumericalQuestion, SimpleNumericalAnswer, RangeNumericalAnswer, HighLowNumericalAnswer } from 'gift-pegjs';
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer, isMultipleNumericalAnswer } from 'gift-pegjs/typeGuards';
interface Props {
question: NumericalQuestion;
handleOnSubmitAnswer?: (answer: number) => void;
showAnswer?: boolean;
}
const NumericalQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer } =
props;
const [answer, setAnswer] = useState<number>();
const correctAnswers = question.choices;
let correctAnswer = '';
//const isSingleAnswer = correctAnswers.length === 1;
if (isSimpleNumericalAnswer(correctAnswers[0])) {
correctAnswer = `${(correctAnswers[0] as SimpleNumericalAnswer).number}`;
} else if (isRangeNumericalAnswer(correctAnswers[0])) {
const choice = correctAnswers[0] as RangeNumericalAnswer;
correctAnswer = `Entre ${choice.number - choice.range} et ${choice.number + choice.range}`;
} else if (isHighLowNumericalAnswer(correctAnswers[0])) {
const choice = correctAnswers[0] as HighLowNumericalAnswer;
correctAnswer = `Entre ${choice.numberLow} et ${choice.numberHigh}`;
} else if (isMultipleNumericalAnswer(correctAnswers[0])) {
correctAnswer = `MultipleNumericalAnswer is not supported yet`;
} else {
throw new Error('Unknown numerical answer type');
}
return (
<div className="question-wrapper">
<div>
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div>
{showAnswer ? (
<>
<div className="correct-answer-text mb-2">{correctAnswer}</div>
{question.formattedGlobalFeedback && <div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>}
</>
) : (
<>
<div className="answer-wrapper mb-1">
<TextField
type="number"
id={question.formattedStem.text}
name={question.formattedStem.text}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAnswer(e.target.valueAsNumber);
}}
inputProps={{ 'data-testid': 'number-input' }}
/>
</div>
{question.formattedGlobalFeedback && showAnswer && (
<div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>
)}
{handleOnSubmitAnswer && (
<Button
variant="contained"
onClick={() =>
answer !== undefined &&
handleOnSubmitAnswer &&
handleOnSubmitAnswer(answer)
}
disabled={answer === undefined || isNaN(answer)}
>
Répondre
</Button>
)}
</>
)}
</div>
);
};
export default NumericalQuestionDisplay;

View file

@ -0,0 +1,89 @@
import React from 'react';
import { Question } from 'gift-pegjs';
import TrueFalseQuestionDisplay from './TrueFalseQuestionDisplay/TrueFalseQuestionDisplay';
import MultipleChoiceQuestionDisplay from './MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay';
import NumericalQuestionDisplay from './NumericalQuestionDisplay/NumericalQuestionDisplay';
import ShortAnswerQuestionDisplay from './ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay';
// import useCheckMobileScreen from '../../services/useCheckMobileScreen';
interface QuestionProps {
question: Question;
handleOnSubmitAnswer?: (answer: string | number | boolean) => void;
showAnswer?: boolean;
}
const QuestionDisplay: React.FC<QuestionProps> = ({
question,
handleOnSubmitAnswer,
showAnswer,
}) => {
// const isMobile = useCheckMobileScreen();
// const imgWidth = useMemo(() => {
// return isMobile ? '100%' : '20%';
// }, [isMobile]);
let questionTypeComponent = null;
switch (question?.type) {
case 'TF':
questionTypeComponent = (
<TrueFalseQuestionDisplay
question={question}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
/>
);
break;
case 'MC':
questionTypeComponent = (
<MultipleChoiceQuestionDisplay
question={question}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
/>
);
break;
case 'Numerical':
if (question.choices) {
if (!Array.isArray(question.choices)) {
questionTypeComponent = (
<NumericalQuestionDisplay
question={question}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
/>
);
} else {
questionTypeComponent = ( // TODO fix NumericalQuestion (correctAnswers is borked)
<NumericalQuestionDisplay
question={question}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
/>
);
}
}
break;
case 'Short':
questionTypeComponent = (
<ShortAnswerQuestionDisplay
question={question}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
/>
);
break;
}
return (
<div className="question-container">
{questionTypeComponent ? (
<>
{questionTypeComponent}
</>
) : (
<div>Question de type inconnue</div>
)}
</div>
);
};
export default QuestionDisplay;

View file

@ -1,59 +1,49 @@
// ShortAnswerQuestion.tsx
import React, { useState } from 'react';
import '../questionStyle.css';
import { Button, TextField } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
type Choices = {
feedback: { format: string; text: string } | null;
isCorrect: boolean;
text: { format: string; text: string };
weigth?: number;
id: string;
};
import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { ShortAnswerQuestion } from 'gift-pegjs';
interface Props {
questionContent: TextFormat;
choices: Choices[];
globalFeedback?: string | undefined;
question: ShortAnswerQuestion;
handleOnSubmitAnswer?: (answer: string) => void;
showAnswer?: boolean;
}
const ShortAnswerQuestion: React.FC<Props> = (props) => {
const { questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props;
const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer } = props;
const [answer, setAnswer] = useState<string>();
return (
<div className="question-wrapper">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div>
{showAnswer ? (
<>
<div className="correct-answer-text mb-1">
{choices.map((choice) => (
<div key={choice.id} className="mb-1">
{choice.text.text}
{question.choices.map((choice) => (
<div key={choice.text} className="mb-1">
{choice.text}
</div>
))}
</div>
{globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}
{question.formattedGlobalFeedback && <div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>}
</>
) : (
<>
<div className="answer-wrapper mb-1">
<TextField
type="text"
id={questionContent.text}
name={questionContent.text}
id={question.formattedStem.text}
name={question.formattedStem.text}
onChange={(e) => {
setAnswer(e.target.value);
}}
disabled={showAnswer}
inputProps={{ 'data-testid': 'text-input' }}
aria-label="short-answer-input"
/>
</div>
{handleOnSubmitAnswer && (
@ -75,4 +65,4 @@ const ShortAnswerQuestion: React.FC<Props> = (props) => {
);
};
export default ShortAnswerQuestion;
export default ShortAnswerQuestionDisplay;

View file

@ -2,33 +2,31 @@
import React, { useState, useEffect } from 'react';
import '../questionStyle.css';
import { Button } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
import { TrueFalseQuestion } from 'gift-pegjs';
import { FormattedTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate';
interface Props {
questionContent: TextFormat;
correctAnswer: boolean;
globalFeedback?: string | undefined;
question: TrueFalseQuestion;
handleOnSubmitAnswer?: (answer: boolean) => void;
showAnswer?: boolean;
}
const TrueFalseQuestion: React.FC<Props> = (props) => {
const { questionContent, correctAnswer, showAnswer, handleOnSubmitAnswer, globalFeedback } =
const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer } =
props;
const [answer, setAnswer] = useState<boolean | undefined>(undefined);
useEffect(() => {
setAnswer(undefined);
}, [questionContent]);
}, [question]);
const selectedTrue = answer ? 'selected' : '';
const selectedFalse = answer !== undefined && !answer ? 'selected' : '';
const correctAnswer = question.isTrue === answer;
return (
<div className="question-container">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({ text: questionContent })) }} />
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div>
<div className="choices-wrapper mb-1">
<Button
@ -50,8 +48,22 @@ const TrueFalseQuestion: React.FC<Props> = (props) => {
<div className={`answer-text ${selectedFalse}`}>Faux</div>
</Button>
</div>
{globalFeedback && showAnswer && (
<div className="global-feedback mb-2">{globalFeedback}</div>
{/* selected TRUE, show True feedback if it exists */}
{showAnswer && answer && question.trueFormattedFeedback && (
<div className="true-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} />
</div>
)}
{/* selected FALSE, show False feedback if it exists */}
{showAnswer && !answer && question.falseFormattedFeedback && (
<div className="false-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.falseFormattedFeedback) }} />
</div>
)}
{question.formattedGlobalFeedback && showAnswer && (
<div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>
)}
{!showAnswer && handleOnSubmitAnswer && (
<Button
@ -68,4 +80,4 @@ const TrueFalseQuestion: React.FC<Props> = (props) => {
);
};
export default TrueFalseQuestion;
export default TrueFalseQuestionDisplay;

View file

@ -76,6 +76,39 @@
box-shadow: 0 0 1px #3a3a3a;
}
.answer-weight-container {
display: flow;
/* float: left; */
box-shadow: 0px 1px 1px #3a3a3a;
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
}
.numerical-answer-weight-container {
display: inline;
box-shadow: 0px 1px 1px #3a3a3a;
border-radius: 3px;
padding-left: 0.2rem;
padding-right: 0.2rem;
padding-top: 0.05rem;
padding-bottom: 0.05rem;
}
.answer-positive-weight {
background-color: hsl(
120, 100%, 90%
);
}
.answer-zero-or-less-weight {
background-color: hsl(
0, 100%, 90%
);
}
.answer-text.selected {
background-color: var(--main-color);
color: white;
@ -88,8 +121,23 @@
.feedback-container {
margin-left: 1.1rem;
display: inline-flex !important; /* override the parent */
align-items: center;
position: relative;
padding: 0 0.5rem;
background-color: hsl(43, 100%, 94%);
color: hsl(43, 95%, 9%);
border: hsl(36, 84%, 93%) 1px solid;
border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
}
.feedback-container img {
vertical-align: middle;
/* height: 1em; */
}
.global-feedback {
position: relative;
padding: 0 1rem;

View file

@ -1,6 +1,6 @@
// StudentModeQuiz.tsx
import React, { useEffect, useState } from 'react';
import QuestionComponent from '../Questions/Question';
import QuestionComponent from '../QuestionsDisplay/QuestionDisplay';
import '../../pages/Student/JoinRoom/joinRoom.css';
import { QuestionType } from '../../Types/QuestionType';
// import { QuestionService } from '../../services/QuestionService';
@ -8,6 +8,7 @@ import { Button } from '@mui/material';
//import QuestionNavigation from '../QuestionNavigation/QuestionNavigation';
//import { ChevronLeft, ChevronRight } from '@mui/icons-material';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import { Question } from 'gift-pegjs';
interface StudentModeQuizProps {
questions: QuestionType[];
@ -63,7 +64,7 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
</div>
<QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question}
question={questionInfos.question as Question}
showAnswer={isAnswerSubmitted}
/>
<div className="center-h-align mt-1/2">

View file

@ -1,13 +1,14 @@
// TeacherModeQuiz.tsx
import React, { useEffect, useState } from 'react';
import QuestionComponent from '../Questions/Question';
import QuestionComponent from '../QuestionsDisplay/QuestionDisplay';
import '../../pages/Student/JoinRoom/joinRoom.css';
import { QuestionType } from '../../Types/QuestionType';
// import { QuestionService } from '../../services/QuestionService';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
import { Question } from 'gift-pegjs';
interface TeacherModeQuizProps {
questionInfos: QuestionType;
@ -63,7 +64,7 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
) : (
<QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question}
question={questionInfos.question as Question}
/>
)}
@ -76,7 +77,7 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
{feedbackMessage}
<QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question}
question={questionInfos.question as Question}
showAnswer={true}
/>
</DialogContent>

View file

@ -0,0 +1,16 @@
import { marked, Renderer } from 'marked';
// Customized renderer to support image width and height
// see https://github.com/markedjs/marked/issues/339#issuecomment-1146363560
const renderer = new Renderer();
renderer.image = ({href, title, text}) => {
const [width, height] = title?.startsWith('=') ? title.slice(1).split('x').map(v => v.trim()).filter(Boolean) : [];
return `<img src="${href}" alt="${text}"${width ? ` width="${width}"` : ''}${height ? ` height="${height}"` : ''}>`;
}
marked.use({
renderer: renderer
});
export default marked;

View file

@ -101,12 +101,12 @@ const Dashboard: React.FC = () => {
if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes")
//console.log("show all quizzes")
let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);
console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
//console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
// add the folder.title to the QuizType if the folderQuizzes is an array
addFolderTitleToQuizzes(folderQuizzes, folder.title);
quizzes = quizzes.concat(folderQuizzes as QuizType[])
@ -294,15 +294,25 @@ const Dashboard: React.FC = () => {
try {
// folderId: string GET THIS FROM CURRENT FOLDER
// currentTitle: string GET THIS FROM CURRENT FOLDER
const newTitle = prompt('Entrée le nouveau nom du fichier', "Nouveau nom de dossier");
const newTitle = prompt('Entrée le nouveau nom du fichier', folders.find((folder) => folder._id === selectedFolderId)?.title);
if (newTitle) {
await ApiService.renameFolder(selectedFolderId, newTitle);
const renamedFolderId = selectedFolderId;
const result = await ApiService.renameFolder(selectedFolderId, newTitle);
if (result !== true ) {
window.alert(`Une erreur est survenue: ${result}`);
return;
}
const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
// refresh the page
setSelectedFolderId('');
setSelectedFolderId(renamedFolderId);
}
} catch (error) {
console.error('Error renaming folder:', error);
alert('Erreur lors du renommage du dossier: ' + error);
}
};

View file

@ -2,8 +2,8 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client';
import { GIFTQuestion, parse } from 'gift-pegjs';
import { QuestionType } from '../../../Types/QuestionType';
import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs';
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer } from "gift-pegjs/typeGuards";
import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService';
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
@ -18,8 +18,9 @@ import { Refresh, Error } from '@mui/icons-material';
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
//import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation';
import Question from 'src/components/Questions/Question';
import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
import ApiService from '../../../services/ApiService';
import { QuestionType } from 'src/Types/QuestionType';
const ManageRoom: React.FC = () => {
const navigate = useNavigate();
@ -277,7 +278,7 @@ const ManageRoom: React.FC = () => {
const parsedQuestions = [] as QuestionType[];
quizQuestionArray.forEach((question, index) => {
parsedQuestions.push({ question: parse(question)[0] });
parsedQuestions.push({ question: parse(question)[0] as BaseQuestion });
parsedQuestions[index].question.id = (index + 1).toString();
});
if (parsedQuestions.length === 0) return null;
@ -347,7 +348,7 @@ const ManageRoom: React.FC = () => {
const answerText = answer.toString();
if (questionInfo) {
const question = questionInfo.question as GIFTQuestion;
const question = questionInfo.question as ParsedGIFTQuestion;
if (question.type === 'TF') {
return (
(question.isTrue && answerText == 'true') ||
@ -355,53 +356,39 @@ const ManageRoom: React.FC = () => {
);
} else if (question.type === 'MC') {
return question.choices.some(
(choice) => choice.isCorrect && choice.text.text === answerText
(choice) => choice.isCorrect && choice.formattedText.text === answerText
);
} else if (question.type === 'Numerical') {
if (question.choices && !Array.isArray(question.choices)) {
if (
question.choices.type === 'high-low' &&
question.choices.numberHigh &&
question.choices.numberLow
) {
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return (
answerNumber <= question.choices.numberHigh &&
answerNumber >= question.choices.numberLow
);
}
if (isHighLowNumericalAnswer(question.choices[0])) {
const choice = question.choices[0];
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return (
answerNumber <= choice.numberHigh &&
answerNumber >= choice.numberLow
);
}
}
if (question.choices && Array.isArray(question.choices)) {
if (
question.choices[0].text.type === 'range' &&
question.choices[0].text.number &&
question.choices[0].text.range
) {
const answerNumber = parseFloat(answerText);
const range = question.choices[0].text.range;
const correctAnswer = question.choices[0].text.number;
if (!isNaN(answerNumber)) {
return (
answerNumber <= correctAnswer + range &&
answerNumber >= correctAnswer - range
);
}
if (isRangeNumericalAnswer(question.choices[0])) {
const answerNumber = parseFloat(answerText);
const range = question.choices[0].range;
const correctAnswer = question.choices[0].number;
if (!isNaN(answerNumber)) {
return (
answerNumber <= correctAnswer + range &&
answerNumber >= correctAnswer - range
);
}
if (
question.choices[0].text.type === 'simple' &&
question.choices[0].text.number
) {
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return answerNumber === question.choices[0].text.number;
}
}
if (isSimpleNumericalAnswer(question.choices[0])) {
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return answerNumber === question.choices[0].number;
}
}
} else if (question.type === 'Short') {
return question.choices.some(
(choice) => choice.text.text.toUpperCase() === answerText.toUpperCase()
(choice) => choice.text.toUpperCase() === answerText.toUpperCase()
);
}
}
@ -474,9 +461,9 @@ const ManageRoom: React.FC = () => {
<div className="preview-and-result-container">
{currentQuestion && (
<Question
<QuestionDisplay
showAnswer={false}
question={currentQuestion?.question}
question={currentQuestion?.question as Question}
/>
)}

View file

@ -419,7 +419,7 @@ class ApiService {
*/
public async renameFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
try {
console.log(`rename folder: folderId: ${folderId}, newTitle: ${newTitle}`);
if (!folderId || !newTitle) {
throw new Error(`Le folderId et le nouveau titre sont requis.`);
}
@ -428,7 +428,9 @@ class ApiService {
const headers = this.constructRequestHeaders();
const body = { folderId, newTitle };
const result: AxiosResponse = await axios.put(url, body, { headers: headers });
const result = await axios.put(url, body, { headers: headers });
console.log(`rename folder: result: ${result.status}, ${result.data}`);
if (result.status !== 200) {
throw new Error(`Le changement de nom de dossier a échoué. Status: ${result.status}`);
}
@ -440,6 +442,7 @@ class ApiService {
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
console.log(JSON.stringify(err));
const data = err.response?.data as { error: string } | undefined;
return data?.error || 'Erreur serveur inconnue lors de la requête.';
}

View file

@ -21,7 +21,7 @@ class WebSocketService {
private socket: Socket | null = null;
connect(backendUrl: string): Socket {
console.log(`WebSocketService.connect('${backendUrl}')`);
// console.log(`WebSocketService.connect('${backendUrl}')`);
// // Ensure the URL uses wss: if the URL starts with https:
// const protocol = backendUrl.startsWith('https:') ? 'wss:' : 'ws:';

View file

@ -2,14 +2,19 @@
"compilerOptions": {
"baseUrl": "./",
"paths": {
"src/*": ["src/*"]
"src/*": [
"src/*"
]
},
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@ -17,7 +22,6 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
/* Linting */
"strict": true,
"noUnusedLocals": true,
@ -25,6 +29,16 @@
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
// "exclude": [
// // "src/components/GiftTemplate/**/*",
// // "src/__tests__/components/GiftTemplate/**/*",
// ],
"include": [
"src"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View file

@ -197,32 +197,52 @@ describe('Folders', () => {
it('should rename a folder and return true', async () => {
const folderId = '60c72b2f9b1d8b3a4c8e4d3b';
const newTitle = 'New Folder Name';
const userId = '12345';
// Mock the database response
collection.updateOne.mockResolvedValue({ modifiedCount: 1 });
const result = await folders.rename(folderId, newTitle);
const result = await folders.rename(folderId, userId, newTitle);
expect(db.connect).toHaveBeenCalled();
expect(db.collection).toHaveBeenCalledWith('folders');
expect(collection.updateOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }, { $set: { title: newTitle } });
// { _id: ObjectId.createFromHexString(folderId), userId: userId }, { $set: { title: newTitle } }
expect(collection.updateOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId), userId: userId }, { $set: { title: newTitle } });
expect(result).toBe(true);
});
it('should return false if the folder does not exist', async () => {
const folderId = '60c72b2f9b1d8b3a4c8e4d3b';
const newTitle = 'New Folder Name';
const userId = '12345';
// Mock the database response
collection.updateOne.mockResolvedValue({ modifiedCount: 0 });
const result = await folders.rename(folderId, newTitle);
const result = await folders.rename(folderId, userId, newTitle);
expect(db.connect).toHaveBeenCalled();
expect(db.collection).toHaveBeenCalledWith('folders');
expect(collection.updateOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }, { $set: { title: newTitle } });
expect(collection.updateOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId), userId: userId }, { $set: { title: newTitle } });
expect(result).toBe(false);
});
it('should throw an error if the new title is already in use', async () => {
const folderId = '60c72b2f9b1d8b3a4c8e4d3b';
const newTitle = 'Existing Folder';
const userId = '12345';
// Mock the database response
collection.findOne.mockResolvedValue({ title: newTitle });
collection.updateOne.mockResolvedValue({ modifiedCount: 0 });
await expect(folders.rename(folderId, userId, newTitle)).rejects.toThrow(`Folder with name '${newTitle}' already exists.`);
expect(db.connect).toHaveBeenCalled();
expect(db.collection).toHaveBeenCalledWith('folders');
// expect(collection.updateOne).toHaveBeenCalledWith({ _id: new ObjectId(folderId) }, { $set: { title: newTitle } });
expect(collection.findOne).toHaveBeenCalledWith({ userId: userId, title: newTitle });
});
});
// duplicate

View file

@ -29,7 +29,7 @@ exports.UPDATE_PASSWORD_ERROR = {
code: 400
}
exports.DELETE_USER_ERROR = {
message: 'Une erreur s\'est produite lors de supression de l\'utilisateur.',
message: 'Une erreur s\'est produite lors de suppression de l\'utilisateur.',
code: 400
}
@ -43,15 +43,15 @@ exports.QUIZ_NOT_FOUND = {
code: 404
}
exports.QUIZ_ALREADY_EXISTS = {
message: 'Le quiz existe déja.',
message: 'Le quiz existe déjà.',
code: 400
}
exports.UPDATE_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la mise à jours du quiz.',
message: 'Une erreur s\'est produite lors de la mise à jour du quiz.',
code: 400
}
exports.DELETE_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la supression du quiz.',
message: 'Une erreur s\'est produite lors de la suppression du quiz.',
code: 400
}
exports.GETTING_QUIZ_ERROR = {
@ -76,15 +76,15 @@ exports.FOLDER_NOT_FOUND = {
code: 404
}
exports.FOLDER_ALREADY_EXISTS = {
message: 'Le dossier existe déja.',
code: 400
message: 'Le dossier existe déjà.',
code: 409
}
exports.UPDATE_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la mise à jours du dossier.',
message: 'Une erreur s\'est produite lors de la mise à jour du dossier.',
code: 400
}
exports.DELETE_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la supression du dossier.',
message: 'Une erreur s\'est produite lors de la suppression du dossier.',
code: 400
}
exports.GETTING_FOLDER_ERROR = {

View file

@ -127,7 +127,14 @@ class FoldersController {
throw new AppError(FOLDER_NOT_FOUND);
}
const result = await this.folders.rename(folderId, newTitle);
// Is this the new title already taken by another folder that I own?
const exists = await this.folders.folderExists(newTitle, req.user.userId);
if (exists) {
throw new AppError(FOLDER_ALREADY_EXISTS);
}
const result = await this.folders.rename(folderId, req.user.userId, newTitle);
if (!result) {
throw new AppError(UPDATE_FOLDER_ERROR);

View file

@ -2,6 +2,7 @@ class AppError extends Error {
constructor (errorCode) {
super(errorCode.message)
this.statusCode = errorCode.code;
this.isOperational = true; // Optional: to distinguish operational errors from programming errors
}
}

View file

@ -1,8 +1,7 @@
const AppError = require("./AppError");
const fs = require('fs');
const errorHandler = (error, req, res) => {
console.log("ERROR", error);
const errorHandler = (error, req, res, _next) => {
if (error instanceof AppError) {
logError(error);

View file

@ -10,8 +10,6 @@ class Folders {
async create(title, userId) {
console.log("LOG: create", title, userId);
if (!title || !userId) {
throw new Error('Missing required parameter(s)');
}
@ -86,13 +84,18 @@ class Folders {
return true;
}
async rename(folderId, newTitle) {
async rename(folderId, userId, newTitle) {
await this.db.connect()
const conn = this.db.getConnection();
const foldersCollection = conn.collection('folders');
const result = await foldersCollection.updateOne({ _id: ObjectId.createFromHexString(folderId) }, { $set: { title: newTitle } })
// see if a folder exists for this user with the new title
const existingFolder = await foldersCollection.findOne({ title: newTitle, userId: userId });
if (existingFolder) throw new Error(`Folder with name '${newTitle}' already exists.`);
const result = await foldersCollection.updateOne({ _id: ObjectId.createFromHexString(folderId), userId: userId }, { $set: { title: newTitle } })
if (result.modifiedCount != 1) return false;
@ -100,7 +103,6 @@ class Folders {
}
async duplicate(folderId, userId) {
console.log("LOG: duplicate", folderId, userId);
const conn = this.db.getConnection();
const foldersCollection = conn.collection('folders');
@ -112,7 +114,7 @@ class Folders {
const theUserId = userId;
// Use the utility function to generate a unique title
const newFolderTitle = await generateUniqueTitle(sourceFolder.title, async (title) => {
console.log(`generateUniqueTitle(${title}): userId`, theUserId);
// console.log(`generateUniqueTitle(${title}): userId`, theUserId);
return await foldersCollection.findOne({ title: title, userId: theUserId });
});
@ -124,9 +126,9 @@ class Folders {
// copy the quizzes from source folder to destination folder
const content = await this.getContent(folderId);
console.log("folders.duplicate: found content", content);
// console.log("folders.duplicate: found content", content);
for (const quiz of content) {
console.log("folders.duplicate: creating quiz (copy)", quiz);
// console.log("folders.duplicate: creating quiz (copy)", quiz);
const result = await this.quizModel.create(quiz.title, quiz.content, newFolderId.toString(), userId);
if (!result) {
throw new Error('Failed to create duplicate quiz');
@ -137,14 +139,12 @@ class Folders {
}
async folderExists(title, userId) {
console.log("LOG: folderExists", title, userId);
await this.db.connect();
const conn = this.db.getConnection();
const foldersCollection = conn.collection('folders');
const existingFolder = await foldersCollection.findOne({ title: title, userId: userId });
return !!existingFolder;
return existingFolder ? true : false;
}

View file

@ -9,7 +9,7 @@ class Quiz {
}
async create(title, content, folderId, userId) {
console.log(`quizzes: create title: ${title}, folderId: ${folderId}, userId: ${userId}`);
// console.log(`quizzes: create title: ${title}, folderId: ${folderId}, userId: ${userId}`);
await this.db.connect()
const conn = this.db.getConnection();
@ -31,7 +31,7 @@ class Quiz {
}
const result = await quizCollection.insertOne(newQuiz);
console.log("quizzes: create insertOne result", result);
// console.log("quizzes: create insertOne result", result);
return result.insertedId;
}

View file

@ -1,6 +1,6 @@
// utils.js
async function generateUniqueTitle(baseTitle, existsCallback) {
console.log(`generateUniqueTitle(${baseTitle})`);
// console.log(`generateUniqueTitle(${baseTitle})`);
let newTitle = baseTitle;
let counter = 1;
@ -19,12 +19,12 @@ async function generateUniqueTitle(baseTitle, existsCallback) {
newTitle = `${baseTitle} (${counter})`;
}
console.log(`first check of newTitle: ${newTitle}`);
// console.log(`first check of newTitle: ${newTitle}`);
while (await existsCallback(newTitle)) {
counter++;
newTitle = `${baseTitle} (${counter})`;
console.log(`trying newTitle: ${newTitle}`);
// console.log(`trying newTitle: ${newTitle}`);
}
return newTitle;

View file

@ -35,7 +35,7 @@
"supertest": "^6.3.4"
},
"engines": {
"node": "18.x"
"node": "20.x"
},
"jest": {
"testEnvironment": "node",

View file

@ -2,17 +2,17 @@ const express = require('express');
const router = express.Router();
const jwt = require('../middleware/jwtToken.js');
const folders = require('../app.js').folders;
const asyncHandler = require('./routerUtils.js');
router.post("/create", jwt.authenticate, folders.create);
router.get("/getUserFolders", jwt.authenticate, folders.getUserFolders);
router.get("/getFolderContent/:folderId", jwt.authenticate, folders.getFolderContent);
router.delete("/delete/:folderId", jwt.authenticate, folders.delete);
router.put("/rename", jwt.authenticate, folders.rename);
router.post("/create", jwt.authenticate, asyncHandler(folders.create));
router.get("/getUserFolders", jwt.authenticate, asyncHandler(folders.getUserFolders));
router.get("/getFolderContent/:folderId", jwt.authenticate, asyncHandler(folders.getFolderContent));
router.delete("/delete/:folderId", jwt.authenticate, asyncHandler(folders.delete));
router.put("/rename", jwt.authenticate, asyncHandler(folders.rename));
//router.post("/duplicate", jwt.authenticate, foldersController.duplicate);
router.post("/duplicate", jwt.authenticate, folders.duplicate);
router.post("/duplicate", jwt.authenticate, asyncHandler(folders.duplicate));
router.post("/copy/:folderId", jwt.authenticate, folders.copy);
router.post("/copy/:folderId", jwt.authenticate, asyncHandler(folders.copy));
module.exports = router;

View file

@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const images = require('../app.js').images;
const asyncHandler = require('./routerUtils.js');
const jwt = require('../middleware/jwtToken.js');
@ -9,7 +10,7 @@ const multer = require('multer');
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
router.post("/upload", jwt.authenticate, upload.single('image'), images.upload);
router.get("/get/:id", images.get);
router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(images.upload));
router.get("/get/:id", asyncHandler(images.get));
module.exports = router;

View file

@ -2,21 +2,22 @@ const express = require('express');
const router = express.Router();
const quizzes = require('../app.js').quizzes;
const jwt = require('../middleware/jwtToken.js');
const asyncHandler = require('./routerUtils.js');
if (!quizzes) {
console.error("quizzes is not defined");
}
router.post("/create", jwt.authenticate, quizzes.create);
router.get("/get/:quizId", jwt.authenticate, quizzes.get);
router.delete("/delete/:quizId", jwt.authenticate, quizzes.delete);
router.put("/update", jwt.authenticate, quizzes.update);
router.put("/move", jwt.authenticate, quizzes.move);
router.post("/create", jwt.authenticate, asyncHandler(quizzes.create));
router.get("/get/:quizId", jwt.authenticate, asyncHandler(asyncHandler(quizzes.get)));
router.delete("/delete/:quizId", jwt.authenticate, asyncHandler(quizzes.delete));
router.put("/update", jwt.authenticate, asyncHandler(quizzes.update));
router.put("/move", jwt.authenticate, asyncHandler(quizzes.move));
router.post("/duplicate", jwt.authenticate, quizzes.duplicate);
router.post("/copy/:quizId", jwt.authenticate, quizzes.copy);
router.put("/Share", jwt.authenticate, quizzes.share);
router.get("/getShare/:quizId", jwt.authenticate, quizzes.getShare);
router.post("/receiveShare", jwt.authenticate, quizzes.receiveShare);
router.post("/duplicate", jwt.authenticate, asyncHandler(quizzes.duplicate));
router.post("/copy/:quizId", jwt.authenticate, asyncHandler(quizzes.copy));
router.put("/Share", jwt.authenticate, asyncHandler(quizzes.share));
router.get("/getShare/:quizId", jwt.authenticate, asyncHandler(quizzes.getShare));
router.post("/receiveShare", jwt.authenticate, asyncHandler(quizzes.receiveShare));
module.exports = router;

View file

@ -0,0 +1,6 @@
// asyncHandler is a wrapper for async functions that catch errors and pass them to the next middleware
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
module.exports = asyncHandler;

View file

@ -2,11 +2,12 @@ const express = require('express');
const router = express.Router();
const users = require('../app.js').users;
const jwt = require('../middleware/jwtToken.js');
const asyncHandler = require('./routerUtils.js');
router.post("/register", users.register);
router.post("/login", users.login);
router.post("/reset-password", users.resetPassword);
router.post("/change-password", jwt.authenticate, users.changePassword);
router.post("/delete-user", jwt.authenticate, users.delete);
router.post("/register", asyncHandler(users.register));
router.post("/login", asyncHandler(users.login));
router.post("/reset-password", asyncHandler(users.resetPassword));
router.post("/change-password", jwt.authenticate, asyncHandler(users.changePassword));
router.post("/delete-user", jwt.authenticate, asyncHandler(users.delete));
module.exports = router;