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 - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: '20'
- name: Install Dependencies, lint and Run Tests - name: Install Dependencies, lint and Run Tests
run: | run: |

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { GIFTQuestion } from 'gift-pegjs'; import { BaseQuestion } from "gift-pegjs";
export interface QuestionType { 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 // constants.tsx
const ENV_VARIABLES = { const ENV_VARIABLES = {
@ -7,7 +7,7 @@ const ENV_VARIABLES = {
VITE_BACKEND_SOCKET_URL: process.env.VITE_BACKEND_SOCKET_URL || "", 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_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_SOCKET_URL=${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
export { ENV_VARIABLES }; export { ENV_VARIABLES };

View file

@ -1,6 +1,6 @@
//QuestionType.test.tsx //QuestionType.test.tsx
import { GIFTQuestion } from 'gift-pegjs'; // Superfluous test now that gift-pegjs has TypeScript types (and its own tests)
import { QuestionType } from '../../Types/QuestionType'; import { Question } from 'gift-pegjs';
const sampleStem = 'Sample question stem'; const sampleStem = 'Sample question stem';
const options = ['Option A', 'Option B']; const options = ['Option A', 'Option B'];
@ -8,30 +8,28 @@ const sampleFormat = 'plain';
const sampleType = 'MC'; const sampleType = 'MC';
const sampleTitle = 'Sample Question'; const sampleTitle = 'Sample Question';
const mockQuestion: GIFTQuestion = { const mockQuestion: Question = {
id: '1', id: '1',
type: sampleType, type: sampleType,
stem: { format: sampleFormat, text: sampleStem }, formattedStem: { format: sampleFormat, text: sampleStem },
title: sampleTitle, title: sampleTitle,
hasEmbeddedAnswers: false, hasEmbeddedAnswers: false,
globalFeedback: null,
choices: [ choices: [
{ text: { format: sampleFormat, text: options[0] }, isCorrect: true, weight: 1, feedback: null }, { formattedText: { format: sampleFormat, text: options[0] }, isCorrect: true, weight: 1 },
{ text: { format: sampleFormat, text: options[1] }, isCorrect: false, weight: 0, feedback: null }, { formattedText: { format: sampleFormat, text: options[1] }, isCorrect: false, weight: 0 },
], ],
}; };
const mockQuestionType: QuestionType = { const mockQuestionType = mockQuestion;
question: 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', () => { test('has the expected structure', () => {
expect(mockQuestionType).toEqual(expect.objectContaining({ expect(mockQuestionType).toEqual(expect.objectContaining({
question: expect.any(Object), question: expect.any(Object),
})); }));
expect(mockQuestionType.question).toEqual(expect.objectContaining({ expect(mockQuestionType).toEqual(expect.objectContaining({
id: expect.any(String), id: expect.any(String),
type: expect.any(String), type: expect.any(String),
stem: expect.objectContaining({ 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 { TextFormat } from "gift-pegjs";
import textType from "src/components/GiftTemplate/templates/TextType";
describe('TextType', () => { describe('TextType', () => {
it('should format text with basic characters correctly', () => { it('should format text with basic characters correctly', () => {
const input: TextFormat = { 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?', text: 'Hello, world! 5 > 3, right?',
format: 'plain' format: 'moodle'
}; };
const expectedOutput = 'Hello, world! 5 > 3, right?'; 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 = { 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?', text: 'Hello,\nworld!\n5 > 3, right?',
format: 'plain' format: 'moodle'
}; };
const expectedOutput = 'Hello,<br>world!<br>5 > 3, right?'; const expectedOutput = 'Hello,<br>world!<br>5 &gt; 3, right?';
expect(textType({ text: input })).toBe(expectedOutput); 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 = { 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$$', text: '$$E=mc^2$$',
format: 'plain' 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 // 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 // by running the test and copying the "Received string:" in jest output
// when it fails (assuming the output is correct) // 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>'; 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(textType({ text: input })).toContain(expectedOutput); expect(FormattedTextTemplate(input)).toContain(expectedOutput);
}); });
it('should format text with two equations (inline and separate) correctly', () => { it('should format text with two equations (inline and separate) correctly', () => {
const input: TextFormat = { 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$$', 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"><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>';
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(FormattedTextTemplate(input)).toContain(expectedOutput);
expect(textType({ text: 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 = { const input: TextFormat = {
// eslint-disable-next-line no-useless-escape // Text here would already be past the GIFT parsing stage, so we don't need to escape GIFT special characters
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}\n a&b \\\\\n c&d\n\\end\{pmatrix\}`, text: `Inline matrix: \\( \\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\)`,
format: 'plain' 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', () => { it('should format text with Markdown correctly', () => {
@ -63,7 +78,7 @@ describe('TextType', () => {
}; };
// TODO: investigate why the output has an extra newline // TODO: investigate why the output has an extra newline
const expectedOutput = '<strong>Bold</strong>\n'; const expectedOutput = '<strong>Bold</strong>\n';
expect(textType({ text: input })).toBe(expectedOutput); expect(FormattedTextTemplate(input)).toBe(expectedOutput);
}); });
it('should format text with HTML correctly', () => { it('should format text with HTML correctly', () => {
@ -72,7 +87,7 @@ describe('TextType', () => {
format: 'html' format: 'html'
}; };
const expectedOutput = '<em>yes</em>'; const expectedOutput = '<em>yes</em>';
expect(textType({ text: input })).toBe(expectedOutput); expect(FormattedTextTemplate(input)).toBe(expectedOutput);
}); });
it('should format plain text correctly', () => { it('should format plain text correctly', () => {
@ -81,8 +96,16 @@ describe('TextType', () => {
format: 'plain' format: 'plain'
}; };
const expectedOutput = 'Just plain text'; const expectedOutput = 'Just plain text';
expect(textType({ text: input })).toBe(expectedOutput); expect(FormattedTextTemplate(input)).toBe(expectedOutput);
}); });
// Add more tests for other formats if needed // 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 React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom'; 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'; import DOMPurify from 'dompurify';
describe('AnswerIcon', () => { describe('AnswerIcon', () => {

View file

@ -2,99 +2,100 @@ import React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { MultipleChoice } from 'src/components/GiftTemplate/templates'; 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 // Mock the nanoid function
jest.mock('nanoid', () => ({ jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id') nanoid: jest.fn(() => 'mocked-id')
})); }));
const mockProps: TemplateOptions & MultipleChoiceType = { const mockProps: TemplateOptions & MultipleChoiceQuestion = {
type: 'MC', type: 'MC',
hasEmbeddedAnswers: false, hasEmbeddedAnswers: false,
title: 'Sample Title', title: 'Sample Title',
stem: { format: 'plain' , text: 'Sample Stem'}, formattedStem: { format: 'plain' , text: 'Sample Stem'},
choices: [ choices: [
{ text: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }, { formattedText: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { 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 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', type: 'MC',
hasEmbeddedAnswers: false, hasEmbeddedAnswers: false,
title: 'Sample Title', title: 'Sample Title',
stem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'}, formattedStem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'},
choices: [ choices: [
{ text: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }, { formattedText: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { 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 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', type: 'MC',
hasEmbeddedAnswers: false, hasEmbeddedAnswers: false,
title: 'Sample Title with Image', title: 'Sample Title with Image',
stem: { format: 'plain', text: 'Sample Stem with Image' }, formattedStem: { format: 'plain', text: 'Sample Stem with Image' },
choices: [ choices: [
{ text: { format: 'plain', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 }, { formattedText: { format: 'plain', text: 'Choice 1' }, isCorrect: true, formattedFeedback: { 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 2' }, isCorrect: false, formattedFeedback: { 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: '<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', type: 'MC',
hasEmbeddedAnswers: false, hasEmbeddedAnswers: false,
title: 'Sample Title', title: 'Sample Title',
stem: { format: 'moodle' , text: 'Sample Stem'}, formattedStem: { format: 'moodle' , text: 'Sample Stem'},
choices: [ choices: [
{ text: { format: 'moodle' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }, { formattedText: { format: 'moodle' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { 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 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', type: 'MC',
hasEmbeddedAnswers: false, hasEmbeddedAnswers: false,
title: 'Sample Title', title: 'Sample Title',
stem: { format: 'html' , text: '$$\\frac{zzz}{yyy}$$'}, formattedStem: { format: 'html' , text: '$$\\frac{zzz}{yyy}$$'},
choices: [ choices: [
{ text: { format: 'html' , text: 'Choice 1'}, isCorrect: true, feedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }, { formattedText: { format: 'html' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { 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 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', type: 'MC',
hasEmbeddedAnswers: false, hasEmbeddedAnswers: false,
title: 'Sample Title with Image', title: 'Sample Title with Image',
stem: { format: 'markdown', text: 'Sample Stem with Image' }, formattedStem: { format: 'markdown', text: 'Sample Stem with Image' },
choices: [ choices: [
{ text: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 }, { formattedText: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, formattedFeedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 }, { formattedText: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { 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: '<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', type: 'MC',
hasEmbeddedAnswers: false, hasEmbeddedAnswers: false,
title: 'Sample Title with Image', 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: [ choices: [
{ text: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, feedback: { format: 'plain', text: 'Correct!' }, weight: 1 }, { formattedText: { format: 'markdown', text: 'Choice 1' }, isCorrect: true, formattedFeedback: { format: 'plain', text: 'Correct!' }, weight: 1 },
{ text: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, feedback: { format: 'plain', text: 'Incorrect!' }, weight: 1 }, { formattedText: { format: 'markdown', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { 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: '<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', () => { test('MultipleChoice snapshot test', () => {

View file

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

View file

@ -1,63 +1,35 @@
import React from 'react'; import React from 'react';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import ShortAnswer from 'src/components/GiftTemplate/templates/ShortAnswer'; import ShortAnswer from 'src/components/GiftTemplate/templates/ShortAnswerTemplate';
import { TemplateOptions, ShortAnswer as ShortAnswerType } from 'src/components/GiftTemplate/templates/types'; import { TemplateOptions } from 'src/components/GiftTemplate/templates/types';
import { parse, ShortAnswerQuestion } from 'gift-pegjs';
// Mock the nanoid function // Mock the nanoid function
jest.mock('nanoid', () => ({ jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id') nanoid: jest.fn(() => 'mocked-id')
})); }));
const plainTextMock: TemplateOptions & ShortAnswerType = { const plainTextMock: TemplateOptions & ShortAnswerQuestion =
type: 'Short', parse(`
hasEmbeddedAnswers: false, ::Sample Short Answer Title:: Sample Stem {=%1%Answer 1#Correct! =%1%Answer 2#Correct!####Sample Global Feedback}
title: 'Sample Short Answer Title', `)[0] as ShortAnswerQuestion;
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 katexMock: TemplateOptions & ShortAnswerType = { const katexMock: TemplateOptions & ShortAnswerQuestion =
type: 'Short', parse(`
hasEmbeddedAnswers: false, ::Sample Short Answer Title:: $$\\frac\\{zzz\\}\\{yyy\\}$$ {=%1%Answer 1#Correct! =%1%Answer 2#Correct!####[html]Sample Global Feedback}
title: 'Sample Short Answer Title', `)[0] as ShortAnswerQuestion;
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 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 = { const moodleMock: TemplateOptions & ShortAnswerQuestion =
type: 'Short', parse(`
hasEmbeddedAnswers: false, ::Sample Short Answer Title:: Sample Stem {=%1%Answer 1#Correct! =%1%Answer 2#Correct!####[moodle]Sample Global Feedback}
title: 'Sample Short Answer Title with Image', `)[0] as ShortAnswerQuestion;
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 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', () => { test('ShortAnswer snapshot test with plain text', () => {
const { asFragment } = render(<ShortAnswer {...plainTextMock} />); const { asFragment } = render(<ShortAnswer {...plainTextMock} />);

View file

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

View file

@ -38,24 +38,24 @@ exports[`MultipleChoice snapshot test 1`] = `
"&gt;Choix multiple&lt;/span&gt; "&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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%); color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt; "&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -73,25 +73,13 @@ exports[`MultipleChoice snapshot test 1`] = `
width: 1em; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -109,9 +97,7 @@ exports[`MultipleChoice snapshot test 1`] = `
width: 0.75em; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;InCorrect!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;InCorrect!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &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; "&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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%); color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt; "&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; 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; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; 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; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt; " 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;/label&gt;
&lt;svg style=" &lt;svg style="
vertical-align: text-bottom; vertical-align: text-bottom;
@ -276,9 +238,7 @@ exports[`MultipleChoice snapshot test with 2 images using markdown text format 1
width: 0.75em; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;Image Feedback&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Image Feedback&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="
@ -335,24 +295,24 @@ exports[`MultipleChoice snapshot test with Moodle text format 1`] = `
"&gt;Choix multiple&lt;/span&gt; "&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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%); color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt; "&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -370,25 +330,13 @@ exports[`MultipleChoice snapshot test with Moodle text format 1`] = `
width: 1em; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -406,9 +354,7 @@ exports[`MultipleChoice snapshot test with Moodle text format 1`] = `
width: 0.75em; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;InCorrect!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;InCorrect!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="
@ -464,24 +410,24 @@ exports[`MultipleChoice snapshot test with image 1`] = `
"&gt;Choix multiple&lt;/span&gt; "&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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%); color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt; "&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -499,25 +445,13 @@ exports[`MultipleChoice snapshot test with image 1`] = `
width: 1em; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -535,32 +469,20 @@ exports[`MultipleChoice snapshot test with image 1`] = `
width: 0.75em; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt; " 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;/label&gt;
&lt;svg style=" &lt;svg style="
vertical-align: text-bottom; vertical-align: text-bottom;
@ -571,9 +493,7 @@ exports[`MultipleChoice snapshot test with image 1`] = `
width: 0.75em; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;Image Feedback&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Image Feedback&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="
@ -629,25 +549,25 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
"&gt;Choix multiple&lt;/span&gt; "&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image " class="present-question-stem"&gt;
&lt;/p&gt;&lt;span style=" Sample Stem with Image
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;span style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt; "&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -666,25 +586,13 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
width: 1em; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; 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; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt; " 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;/label&gt;
&lt;svg style=" &lt;svg style="
vertical-align: text-bottom; vertical-align: text-bottom;
@ -739,9 +635,7 @@ exports[`MultipleChoice snapshot test with image using markdown text format 1`]
width: 0.75em; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;Image Feedback&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Image Feedback&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="
@ -798,24 +692,24 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
"&gt;Choix multiple&lt;/span&gt; "&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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%); color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt; "&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -833,25 +727,13 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
width: 1em; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -869,9 +751,7 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
width: 1em; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="
@ -927,24 +807,24 @@ exports[`MultipleChoice snapshot test with katex, using html text format 1`] = `
"&gt;Choix multiple&lt;/span&gt; "&gt;Choix multiple&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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%); color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt; "&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -962,25 +842,13 @@ exports[`MultipleChoice snapshot test with katex, using html text format 1`] = `
width: 1em; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span style=" &lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
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;label style=" &lt;label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; 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; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;InCorrect!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;InCorrect!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="

View file

@ -38,13 +38,21 @@ exports[`Numerical snapshot test with html 1`] = `
"&gt;Numérique&lt;/span&gt; "&gt;Numérique&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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;div&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;span style=" &lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;&lt;p style="
color: hsl(0, 0%, 0%); 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; display: inline-block;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
line-height: 1.5; line-height: 1.5;
@ -58,9 +66,23 @@ exports[`Numerical snapshot test with html 1`] = `
font-size: inherit; font-size: inherit;
line-height: inherit; line-height: inherit;
width: 90%; width: 90%;
" placeholder="42, 43"&gt; ;width: 100%" placeholder="42"&gt;&lt;div class="feedback-container"&gt;Correct&lt;/div&gt;&lt;p style="
&lt;/div&gt; color: hsl(0, 0%, 0%);
&lt;div 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;
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; position: relative;
margin-top: 1rem; margin-top: 1rem;
padding: 0 1rem; padding: 0 1rem;
@ -113,13 +135,22 @@ exports[`Numerical snapshot test with image 1`] = `
"&gt;Numérique&lt;/span&gt; "&gt;Numérique&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image&lt;/p&gt; " class="present-question-stem"&gt;
&lt;div&gt; Sample Stem with Image &lt;img alt="" src="https://example.com/cat.jpg"&gt;
&lt;span style="
&lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;&lt;p style="
color: hsl(0, 0%, 0%); 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; display: inline-block;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
line-height: 1.5; line-height: 1.5;
@ -133,9 +164,42 @@ exports[`Numerical snapshot test with image 1`] = `
font-size: inherit; font-size: inherit;
line-height: inherit; line-height: inherit;
width: 90%; width: 90%;
" placeholder="42, 43, 44"&gt; ;width: 100%" placeholder="42"&gt;&lt;div class="feedback-container"&gt;Correct!
&lt;/div&gt; &lt;/div&gt;&lt;p style="
&lt;div 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; position: relative;
margin-top: 1rem; margin-top: 1rem;
padding: 0 1rem; padding: 0 1rem;
@ -145,7 +209,8 @@ exports[`Numerical snapshot test with image 1`] = `
border-radius: 6px; border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%); box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
"&gt; "&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; &lt;/div&gt;&lt;/section&gt;
</DocumentFragment> </DocumentFragment>
`; `;
@ -188,13 +253,21 @@ exports[`Numerical snapshot test with moodle 1`] = `
"&gt;Numérique&lt;/span&gt; "&gt;Numérique&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt; " class="present-question-stem"&gt;
&lt;div&gt; Sample Stem
&lt;span style=" &lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;&lt;p style="
color: hsl(0, 0%, 0%); 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; display: inline-block;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
line-height: 1.5; line-height: 1.5;
@ -208,9 +281,23 @@ exports[`Numerical snapshot test with moodle 1`] = `
font-size: inherit; font-size: inherit;
line-height: inherit; line-height: inherit;
width: 90%; width: 90%;
" placeholder="42, 43"&gt; ;width: 100%" placeholder="42"&gt;&lt;div class="feedback-container"&gt;Correct!&lt;/div&gt;&lt;p style="
&lt;/div&gt; color: hsl(0, 0%, 0%);
&lt;div 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;
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; position: relative;
margin-top: 1rem; margin-top: 1rem;
padding: 0 1rem; padding: 0 1rem;
@ -263,13 +350,21 @@ exports[`Numerical snapshot test with plain text 1`] = `
"&gt;Numérique&lt;/span&gt; "&gt;Numérique&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt; " class="present-question-stem"&gt;
&lt;div&gt; Sample Stem
&lt;span style=" &lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt;&lt;p style="
color: hsl(0, 0%, 0%); 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; display: inline-block;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
line-height: 1.5; line-height: 1.5;
@ -283,9 +378,23 @@ exports[`Numerical snapshot test with plain text 1`] = `
font-size: inherit; font-size: inherit;
line-height: inherit; line-height: inherit;
width: 90%; width: 90%;
" placeholder="42, 43"&gt; ;width: 100%" placeholder="42"&gt;&lt;div class="feedback-container"&gt;Correct!&lt;/div&gt;&lt;p style="
&lt;/div&gt; color: hsl(0, 0%, 0%);
&lt;div 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;
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; position: relative;
margin-top: 1rem; margin-top: 1rem;
padding: 0 1rem; padding: 0 1rem;

View file

@ -38,10 +38,20 @@ exports[`ShortAnswer snapshot test with image 1`] = `
"&gt;Réponse courte&lt;/span&gt; "&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image " class="present-question-stem"&gt;
Sample Stem with Image &lt;img alt="" src="https://example.com/cat.jpg"&gt;
&lt;/p&gt; &lt;/p&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;div&gt; &lt;div&gt;
&lt;span style=" &lt;span style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
@ -59,9 +69,7 @@ exports[`ShortAnswer snapshot test with image 1`] = `
font-size: inherit; font-size: inherit;
line-height: inherit; line-height: inherit;
width: 90%; width: 90%;
" placeholder="Answer 1 " placeholder="Answer 1, Answer 2"&gt;
, Answer 2
, &lt;img src="https://via.placeholder.com/150" alt="Sample Image" /&gt;"&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="
position: relative; position: relative;
@ -73,7 +81,8 @@ exports[`ShortAnswer snapshot test with image 1`] = `
border-radius: 6px; border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%); box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
"&gt; "&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; &lt;/div&gt;&lt;/section&gt;
</DocumentFragment> </DocumentFragment>
`; `;
@ -116,9 +125,19 @@ exports[`ShortAnswer snapshot test with katex 1`] = `
"&gt;Réponse courte&lt;/span&gt; "&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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;div&gt;
&lt;span style=" &lt;span style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
@ -191,9 +210,19 @@ exports[`ShortAnswer snapshot test with moodle 1`] = `
"&gt;Réponse courte&lt;/span&gt; "&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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;div&gt;
&lt;span style=" &lt;span style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
@ -266,9 +295,19 @@ exports[`ShortAnswer snapshot test with plain text 1`] = `
"&gt;Réponse courte&lt;/span&gt; "&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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;div&gt;
&lt;span style=" &lt;span style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);

View file

@ -35,64 +35,41 @@ exports[`TrueFalse snapshot test with image 1`] = `
border-radius: 4px; border-radius: 4px;
background-color: hsl(0, 0%, 100%); background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%); color: hsl(180, 15%, 41%);
"&gt;Vrai/Faux&lt;/span&gt; "&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image&lt;/p&gt;&lt;span style=" " class="present-question-stem"&gt;
color: hsl(0, 0%, 0%); Sample Stem with Image &lt;img alt="" src="https://example.com/cat.gif"&gt;
"&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=" &lt;/p&gt;
display: inline-block; &lt;/span&gt;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Vrai
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
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;
&lt;/div&gt; &lt;/div&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div&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=" &lt;span style="
color: hsl(180, 15%, 41%); color: hsl(0, 0%, 0%);
"&gt;Incorrect!&lt;/span&gt; "&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
&lt;/input&gt; 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%;
" placeholder="A, B, C"&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="
position: relative; position: relative;
@ -104,7 +81,7 @@ exports[`TrueFalse snapshot test with image 1`] = `
border-radius: 6px; border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%); box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
"&gt; "&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; &lt;/div&gt;&lt;/section&gt;
</DocumentFragment> </DocumentFragment>
`; `;
@ -147,9 +124,19 @@ exports[`TrueFalse snapshot test with katex 1`] = `
"&gt;Vrai/Faux&lt;/span&gt; "&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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%); color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt; "&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
@ -172,9 +159,7 @@ exports[`TrueFalse snapshot test with katex 1`] = `
width: 1em; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
@ -198,9 +183,7 @@ exports[`TrueFalse snapshot test with katex 1`] = `
width: 0.75em; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="
@ -213,8 +196,7 @@ exports[`TrueFalse snapshot test with katex 1`] = `
border-radius: 6px; border-radius: 6px;
box-shadow: 0px 2px 5px hsl(0, 0%, 74%); box-shadow: 0px 2px 5px hsl(0, 0%, 74%);
"&gt; "&gt;
&lt;p&gt;Sample Global Feedback &lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt; &lt;/div&gt;&lt;/section&gt;
</DocumentFragment> </DocumentFragment>
`; `;
@ -257,9 +239,19 @@ exports[`TrueFalse snapshot test with moodle 1`] = `
"&gt;Vrai/Faux&lt;/span&gt; "&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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%); color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt; "&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
@ -282,9 +274,7 @@ exports[`TrueFalse snapshot test with moodle 1`] = `
width: 1em; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
@ -308,9 +298,7 @@ exports[`TrueFalse snapshot test with moodle 1`] = `
width: 0.75em; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="
@ -366,9 +354,19 @@ exports[`TrueFalse snapshot test with plain text 1`] = `
"&gt;Vrai/Faux&lt;/span&gt; "&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt; &lt;/span&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style="
display: flex;
"&gt;
&lt;span&gt;
&lt;p style=" &lt;p style="
color: hsl(0, 0%, 0%); 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%); color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt; "&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
@ -391,9 +389,7 @@ exports[`TrueFalse snapshot test with plain text 1`] = `
width: 1em; width: 1em;
color: hsl(120, 39%, 54%); 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; " 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=" &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
@ -417,9 +413,7 @@ exports[`TrueFalse snapshot test with plain text 1`] = `
width: 0.75em; width: 0.75em;
color: hsl(2, 64%, 58%); 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; " 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=" &lt;span class="feedback-container"&gt;Incorrect!&lt;/span&gt;
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;
&lt;div style=" &lt;div style="

View file

@ -1,35 +1,43 @@
import React from 'react'; import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import MultipleChoiceQuestion from 'src/components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
import { act } from 'react'; import { act } from 'react';
import { MemoryRouter } from 'react-router-dom'; 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 questions = parse(
const sampleFeedback = 'Feedback'; `::Sample Question 1:: Question stem
{
=Choice 1
~Choice 2
}`) as MultipleChoiceQuestion[];
describe('MultipleChoiceQuestion', () => { const question = questions[0];
describe('MultipleChoiceQuestionDisplay', () => {
const mockHandleOnSubmitAnswer = jest.fn(); const mockHandleOnSubmitAnswer = jest.fn();
const choices = [ const sampleProps = {
{ feedback: null, isCorrect: true, text: { format: 'plain', text: 'Choice 1' } }, question: question,
{ feedback: null, isCorrect: false, text: { format: 'plain', text: 'Choice 2' } } handleOnSubmitAnswer: mockHandleOnSubmitAnswer,
]; showAnswer: false
};
const choices = question.choices;
beforeEach(() => { beforeEach(() => {
render( render(
<MemoryRouter> <MemoryRouter>
<MultipleChoiceQuestion <MultipleChoiceQuestionDisplay
globalFeedback={sampleFeedback} {...sampleProps}
choices={choices} />
handleOnSubmitAnswer={mockHandleOnSubmitAnswer}
questionStem={{ text: questionStem, format: 'plain' }} />
</MemoryRouter>); </MemoryRouter>);
}); });
test('renders the question and choices', () => { test('renders the question and choices', () => {
expect(screen.getByText(questionStem)).toBeInTheDocument(); expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument();
choices.forEach((choice) => { 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 React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; 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', () => { describe('NumericalQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn(); const mockHandleOnSubmitAnswer = jest.fn();
const sampleStem = 'Sample question stem';
const sampleProps = { const sampleProps = {
questionTitle: 'Sample Question', question: question,
correctAnswers: { handleOnSubmitAnswer: mockHandleOnSubmitAnswer,
numberHigh: 10,
numberLow: 5,
type: 'high-low'
},
handleOnSubmitAnswer: mockHandleSubmitAnswer,
showAnswer: false showAnswer: false
}; };
beforeEach(() => { beforeEach(() => {
render(<NumericalQuestion questionContent={{text: sampleStem, format: 'plain'}} {...sampleProps} />); render(
<MemoryRouter>
<NumericalQuestionDisplay
{...sampleProps}
/>
</MemoryRouter>);
}); });
it('renders correctly', () => { it('renders correctly', () => {
expect(screen.getByText(sampleStem)).toBeInTheDocument(); expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument();
expect(screen.getByTestId('number-input')).toBeInTheDocument(); expect(screen.getByTestId('number-input')).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument(); expect(screen.getByText('Répondre')).toBeInTheDocument();
}); });
@ -48,7 +66,7 @@ describe('NumericalQuestion Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled(); expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
}); });
it('submits answer correctly', () => { it('submits answer correctly', () => {
@ -59,6 +77,6 @@ describe('NumericalQuestion Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7); expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(7);
}); });
}); });

View file

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

View file

@ -1,54 +1,34 @@
// ShortAnswerQuestion.test.tsx
import React from 'react'; 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 '@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', () => { describe('ShortAnswerQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn(); 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 = { 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, handleOnSubmitAnswer: mockHandleSubmitAnswer,
showAnswer: false showAnswer: false
}; };
beforeEach(() => { beforeEach(() => {
render(<ShortAnswerQuestion questionContent={{text: sampleStem, format: 'plain'}} {...sampleProps} />); render(<ShortAnswerQuestionDisplay question={question} {...sampleProps} />);
}); });
it('renders correctly', () => { it('renders correctly', () => {
expect(screen.getByText(sampleStem)).toBeInTheDocument(); expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument();
expect(screen.getByTestId('text-input')).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(); expect(screen.getByText('Répondre')).toBeInTheDocument();
}); });
it('handles input change correctly', () => { 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' } }); fireEvent.change(inputElement, { target: { value: 'User Input' } });
@ -70,7 +50,10 @@ describe('ShortAnswerQuestion Component', () => {
}); });
it('submits answer correctly', () => { 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'); const submitButton = screen.getByText('Répondre');
fireEvent.change(inputElement, { target: { value: 'User Input' } }); fireEvent.change(inputElement, { target: { value: 'User Input' } });

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import React from 'react';
import { render, fireEvent, act } from '@testing-library/react'; import { render, fireEvent, act } from '@testing-library/react';
import { screen } from '@testing-library/dom'; import { screen } from '@testing-library/dom';
import '@testing-library/jest-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 TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
@ -14,7 +14,11 @@ const mockGiftQuestions = parse(
describe('TeacherModeQuiz', () => { 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'; mockQuestion.id = '1';
const mockSubmitAnswer = jest.fn(); 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 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 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 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 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}"; 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 ( return (
<div className="gift-cheat-sheet"> <div className="gift-cheat-sheet">
@ -123,7 +123,7 @@ const GiftCheatSheet: React.FC = () => {
</div> </div>
<div className="question-type"> <div className="question-type">
<h4> 7. Paramètres optionnels </h4> <h4> 7. Caractères spéciaux </h4>
<p> <p>
Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés, 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 \ réponses ou feedback, vous devez «échapper» ces derniers en ajoutant un \
@ -155,7 +155,7 @@ const GiftCheatSheet: React.FC = () => {
<div className="question-type"> <div className="question-type">
<h4> 9. Images </h4> <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> <pre>
<code className="question-code-block"> <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}'} {'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'}
</code> </code>
</pre> </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;: 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' }}> <p style={{ color: 'red' }}>
Attention: l&apos;ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n&apos;est plus Attention: l&apos;ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n&apos;est plus
supportée. supportée.

View file

@ -1,256 +1,21 @@
import Template, { ErrorTemplate } from './templates'; import Template, { ErrorTemplate } from './templates';
import { GIFTQuestion } from './templates/types';
import './styles.css'; 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 items = multiple.map((item) => Template(item, { theme: 'dark' })).join('');
const errorItemDark = ErrorTemplate('Hello'); const errorItemDark = ErrorTemplate('Hello');

View file

@ -72,6 +72,11 @@
font-size: 2rem; font-size: 2rem;
font-weight: 500; font-weight: 500;
} }
.present-question-stem {
margin-bottom: 2vh;
}
.preview-container { .preview-container {
margin-bottom: 2vh; margin-bottom: 2vh;
width: 60vw; 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 { TemplateOptions } from './types';
import textType from './TextType'; import {FormattedTextTemplate} from './TextTypeTemplate';
import { state } from '.'; import { state } from '.';
import { theme } from '../constants'; import { theme } from '../constants';
import { TextFormat } from 'gift-pegjs';
interface GlobalFeedbackOptions extends TemplateOptions { type GlobalFeedbackOptions = TemplateOptions & TextFormat;
feedback: Question['globalFeedback'];
}
export default function GlobalFeedback({ feedback }: GlobalFeedbackOptions): string { export default function GlobalFeedbackTemplate({ format, text }: GlobalFeedbackOptions): string {
const Container = ` const Container = `
position: relative; position: relative;
margin-top: 1rem; margin-top: 1rem;
@ -19,9 +18,9 @@ export default function GlobalFeedback({ feedback }: GlobalFeedbackOptions): str
box-shadow: 0px 2px 5px ${theme(state.theme, 'gray400', 'black800')}; box-shadow: 0px 2px 5px ${theme(state.theme, 'gray400', 'black800')};
`; `;
return feedback !== null return (format && text)
? `<div style="${Container}"> ? `<div style="${Container}">
<p>${textType({ text: feedback })}</p> <p>${FormattedTextTemplate({format: format, text: text})}</p>
</div>` </div>`
: ``; : ``;
} }

View file

@ -1,22 +1,24 @@
import { TemplateOptions, Matching as MatchingType } from './types'; import { TemplateOptions } from './types';
import QuestionContainer from './QuestionContainer'; import QuestionContainer from './QuestionContainerTemplate';
import Title from './Title'; import Title from './TitleTemplate';
import textType from './TextType'; import {FormattedTextTemplate} from './TextTypeTemplate';
import GlobalFeedback from './GlobalFeedback'; import GlobalFeedback from './GlobalFeedbackTemplate';
import { ParagraphStyle, SelectStyle } from '../constants'; import { ParagraphStyle, SelectStyle } from '../constants';
import { state } from '.'; import { state } from '.';
import { MatchingQuestion } from 'gift-pegjs';
import StemTemplate from './StemTemplate';
type MatchingOptions = TemplateOptions & MatchingType; type MatchingOptions = TemplateOptions & MatchingQuestion;
interface MatchAnswerOptions extends TemplateOptions { interface MatchAnswerOptions extends TemplateOptions {
choices: MatchingType['matchPairs']; choices: MatchingQuestion['matchPairs'];
} }
export default function Matching({ export default function MatchingTemplate({
title, title,
stem, formattedStem,
matchPairs, matchPairs,
globalFeedback formattedGlobalFeedback
}: MatchingOptions): string { }: MatchingOptions): string {
return `${QuestionContainer({ return `${QuestionContainer({
children: [ children: [
@ -24,11 +26,9 @@ export default function Matching({
type: 'Appariement', type: 'Appariement',
title: title title: title
}), }),
`<p style="${ParagraphStyle(state.theme)}">${textType({ StemTemplate({formattedStem}),
text: stem
})}</p>`,
MatchAnswers({ choices: matchPairs }), 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 uniqueMatchOptions = Array.from(new Set(choices.map(({ subanswer }) => subanswer)));
const result = choices const result = choices
.map(({ subquestion }) => { .map(({ formattedSubquestion }) => {
return ` return `
<div style="${OptionTable} ${ParagraphStyle(state.theme)}"> <div style="${OptionTable} ${ParagraphStyle(state.theme)}">
${textType({ text: subquestion })} ${FormattedTextTemplate(formattedSubquestion)}
</div> </div>
<div> <div>
<select class="gift-select" style="${SelectStyle(state.theme)} ${Dropdown}"> <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 marked from 'src/markedConfig';
import katex from 'katex';
import { TemplateOptions, TextFormat } from './types';
interface TextTypeOptions extends TemplateOptions { import katex from 'katex';
text: TextFormat; import { TextFormat } from 'gift-pegjs';
} import DOMPurify from 'dompurify'; // cleans HTML to prevent XSS attacks, etc.
export function formatLatex(text: string): string { export function formatLatex(text: string): string {
return text return text
@ -28,23 +26,29 @@ export function formatLatex(text: string): string {
* @see marked * @see marked
* @see katex * @see katex
*/ */
export default function textType({ text }: TextTypeOptions) { export function FormattedTextTemplate(formattedText: TextFormat): string {
const formatText = formatLatex(text.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped const formatText = formatLatex(formattedText.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped
let parsedText = ''; let parsedText = '';
switch (text.format) { let result = '';
switch (formattedText.format) {
case '':
case 'moodle': case 'moodle':
case 'plain': case 'plain':
// Replace newlines with <br> tags // Replace newlines with <br> tags
return replaceNewlinesOutsideSVG(formatText); result = replaceNewlinesOutsideSVG(formatText);
break;
case 'html': case 'html':
// Strip outer paragraph tags (not a great approach with regex) // 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': case 'markdown':
parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219 parsedText = marked.parse(formatText, { breaks: true, gfm: true }) as string; // <br> for newlines
return parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2'); result = parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
break;
default: 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 // 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 { state } from '.';
import { theme } from '../constants'; import { theme } from '../constants';
import { Question } from 'gift-pegjs';
// Type is string to allow for custom question type text (e,g, "Multiple Choice") // Type is string to allow for custom question type text (e,g, "Multiple Choice")
interface TitleOptions extends TemplateOptions { interface TitleOptions extends TemplateOptions {
@ -43,7 +44,7 @@ export default function Title({ type, title }: TitleOptions): string {
<div style="${Container}"> <div style="${Container}">
<span> <span>
${ ${
title !== null title
? `<span style="${QuestionTitle}">${title}</span>` ? `<span style="${QuestionTitle}">${title}</span>`
: `<span style="${OptionalTitle}"><em>(Sans titre)</em></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 { import {
GIFTQuestion, ParsedGIFTQuestion as GIFTQuestion,
// Category as CategoryType, // Category as CategoryType,
// Description as DescriptionType, // Description as DescriptionType,
MultipleChoice as MultipleChoiceType, MultipleChoiceQuestion as MultipleChoiceType,
Numerical as NumericalType, NumericalQuestion as NumericalType,
ShortAnswer as ShortAnswerType, ShortAnswerQuestion as ShortAnswerType,
// Essay as EssayType, // Essay as EssayType,
TrueFalse as TrueFalseType, TrueFalseQuestion as TrueFalseType,
Matching as MatchingType, // MatchingQuestion as MatchingType,
DisplayOptions } from 'gift-pegjs';
} from './types'; 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' }; export const state: DisplayOptions = { preview: true, theme: 'light' };
@ -37,21 +38,21 @@ export default function Template(
// ...(keys as DescriptionType) // ...(keys as DescriptionType)
// }); // });
case 'MC': case 'MC':
return MultipleChoice({ return MultipleChoiceTemplate({
...(keys as MultipleChoiceType) ...(keys as MultipleChoiceType)
}); });
case 'Numerical': case 'Numerical':
return Numerical({ ...(keys as NumericalType) }); return NumericalTemplate({ ...(keys as NumericalType) });
case 'Short': case 'Short':
return ShortAnswer({ return ShortAnswerTemplate({
...(keys as ShortAnswerType) ...(keys as ShortAnswerType)
}); });
// case 'Essay': // case 'Essay':
// return Essay({ ...(keys as EssayType) }); // return Essay({ ...(keys as EssayType) });
case 'TF': case 'TF':
return TrueFalse({ ...(keys as TrueFalseType) }); return TrueFalseTemplate({ ...(keys as TrueFalseType) });
case 'Matching': // case 'Matching':
return Matching({ ...(keys as MatchingType) }); // return Matching({ ...(keys as MatchingType) });
default: default:
// TODO: throw error for unsupported question types? // TODO: throw error for unsupported question types?
// throw new Error(`Unsupported question type: ${type}`); // throw new Error(`Unsupported question type: ${type}`);
@ -66,13 +67,13 @@ export function ErrorTemplate(text: string, options?: Partial<DisplayOptions>):
} }
export { export {
Category, CategoryTemplate,
Description, DescriptionTemplate as Description,
Essay, EssayTemplate as Essay,
Matching, MatchingTemplate as Matching,
MultipleChoice, MultipleChoiceTemplate as MultipleChoice,
Numerical, NumericalTemplate as Numerical,
ShortAnswer, ShortAnswerTemplate as ShortAnswer,
TrueFalse, TrueFalseTemplate as TrueFalse,
Error Error
}; };

View file

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

View file

@ -20,7 +20,7 @@ import {
TableRow TableRow
} from '@mui/material'; } from '@mui/material';
import { StudentType } from '../../Types/StudentType'; import { StudentType } from '../../Types/StudentType';
import { formatLatex } from '../GiftTemplate/templates/TextType'; import { formatLatex } from '../GiftTemplate/templates/TextTypeTemplate';
interface LiveResultsProps { interface LiveResultsProps {
socket: Socket | null; 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 React, { useEffect, useState } from 'react';
import '../questionStyle.css'; import '../questionStyle.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import textType, { formatLatex } from '../../GiftTemplate/templates/TextType'; import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { MultipleChoiceQuestion } from 'gift-pegjs';
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;
};
interface Props { interface Props {
questionStem: TextFormat; question: MultipleChoiceQuestion;
choices: Choices[];
globalFeedback?: string | undefined;
handleOnSubmitAnswer?: (answer: string) => void; handleOnSubmitAnswer?: (answer: string) => void;
showAnswer?: boolean; 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>(); const [answer, setAnswer] = useState<string>();
useEffect(() => { useEffect(() => {
setAnswer(undefined); setAnswer(undefined);
}, [questionContent]); }, [question]);
const handleOnClickAnswer = (choice: string) => { const handleOnClickAnswer = (choice: string) => {
setAnswer(choice); setAnswer(choice);
@ -41,38 +30,40 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} /> <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
{choices.map((choice, i) => { {question.choices.map((choice, i) => {
const selected = answer === choice.text.text ? 'selected' : ''; const selected = answer === choice.formattedText.text ? 'selected' : '';
return ( return (
<div key={choice.text.text + i} className="choice-container"> <div key={choice.formattedText.text + i} className="choice-container">
<Button <Button
variant="text" variant="text"
className="button-wrapper" className="button-wrapper"
onClick={() => !showAnswer && handleOnClickAnswer(choice.text.text)} onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}
> >
{choice.feedback === null && {choice.formattedFeedback === null &&
showAnswer && showAnswer &&
(choice.isCorrect ? '✅' : '❌')} (choice.isCorrect ? '✅' : '❌')}
<div className={`circle ${selected}`}>{alphabet[i]}</div> <div className={`circle ${selected}`}>{alphabet[i]}</div>
<div className={`answer-text ${selected}`}> <div className={`answer-text ${selected}`}>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(formatLatex(choice.text.text)) }} /> <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedText) }} />
</div> </div>
</Button> </Button>
{choice.feedback && showAnswer && ( {choice.formattedFeedback && showAnswer && (
<div className="feedback-container mb-1 mt-1/2"> <div className="feedback-container mb-1 mt-1/2">
{choice.isCorrect ? '✅' : '❌'} {choice.isCorrect ? '✅' : '❌'}
{choice.feedback?.text} <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedFeedback) }} />
</div> </div>
)} )}
</div> </div>
); );
})} })}
</div> </div>
{globalFeedback && showAnswer && ( {question.formattedGlobalFeedback && showAnswer && (
<div className="global-feedback mb-2">{globalFeedback}</div> <div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
</div>
)} )}
{!showAnswer && handleOnSubmitAnswer && ( {!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 React, { useState } from 'react';
import '../questionStyle.css'; import '../questionStyle.css';
import { Button, TextField } from '@mui/material'; import { Button, TextField } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType'; import { FormattedTextTemplate } from '../../GiftTemplate/templates/TextTypeTemplate';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { ShortAnswerQuestion } from 'gift-pegjs';
import DOMPurify from 'dompurify';
type Choices = {
feedback: { format: string; text: string } | null;
isCorrect: boolean;
text: { format: string; text: string };
weigth?: number;
id: string;
};
interface Props { interface Props {
questionContent: TextFormat; question: ShortAnswerQuestion;
choices: Choices[];
globalFeedback?: string | undefined;
handleOnSubmitAnswer?: (answer: string) => void; handleOnSubmitAnswer?: (answer: string) => void;
showAnswer?: boolean; showAnswer?: boolean;
} }
const ShortAnswerQuestion: React.FC<Props> = (props) => { const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
const { questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props; const { question, showAnswer, handleOnSubmitAnswer } = props;
const [answer, setAnswer] = useState<string>(); const [answer, setAnswer] = useState<string>();
return ( return (
<div className="question-wrapper"> <div className="question-wrapper">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} /> <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div> </div>
{showAnswer ? ( {showAnswer ? (
<> <>
<div className="correct-answer-text mb-1"> <div className="correct-answer-text mb-1">
{choices.map((choice) => ( {question.choices.map((choice) => (
<div key={choice.id} className="mb-1"> <div key={choice.text} className="mb-1">
{choice.text.text} {choice.text}
</div> </div>
))} ))}
</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"> <div className="answer-wrapper mb-1">
<TextField <TextField
type="text" type="text"
id={questionContent.text} id={question.formattedStem.text}
name={questionContent.text} name={question.formattedStem.text}
onChange={(e) => { onChange={(e) => {
setAnswer(e.target.value); setAnswer(e.target.value);
}} }}
disabled={showAnswer} disabled={showAnswer}
inputProps={{ 'data-testid': 'text-input' }} aria-label="short-answer-input"
/> />
</div> </div>
{handleOnSubmitAnswer && ( {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 React, { useState, useEffect } from 'react';
import '../questionStyle.css'; import '../questionStyle.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType'; import { TrueFalseQuestion } from 'gift-pegjs';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { FormattedTextTemplate } from 'src/components/GiftTemplate/templates/TextTypeTemplate';
import DOMPurify from 'dompurify';
interface Props { interface Props {
questionContent: TextFormat; question: TrueFalseQuestion;
correctAnswer: boolean;
globalFeedback?: string | undefined;
handleOnSubmitAnswer?: (answer: boolean) => void; handleOnSubmitAnswer?: (answer: boolean) => void;
showAnswer?: boolean; showAnswer?: boolean;
} }
const TrueFalseQuestion: React.FC<Props> = (props) => { const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
const { questionContent, correctAnswer, showAnswer, handleOnSubmitAnswer, globalFeedback } = const { question, showAnswer, handleOnSubmitAnswer } =
props; props;
const [answer, setAnswer] = useState<boolean | undefined>(undefined); const [answer, setAnswer] = useState<boolean | undefined>(undefined);
useEffect(() => { useEffect(() => {
setAnswer(undefined); setAnswer(undefined);
}, [questionContent]); }, [question]);
const selectedTrue = answer ? 'selected' : ''; const selectedTrue = answer ? 'selected' : '';
const selectedFalse = answer !== undefined && !answer ? 'selected' : ''; const selectedFalse = answer !== undefined && !answer ? 'selected' : '';
const correctAnswer = question.isTrue === answer;
return ( return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({ text: questionContent })) }} /> <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
<Button <Button
@ -50,8 +48,22 @@ const TrueFalseQuestion: React.FC<Props> = (props) => {
<div className={`answer-text ${selectedFalse}`}>Faux</div> <div className={`answer-text ${selectedFalse}`}>Faux</div>
</Button> </Button>
</div> </div>
{globalFeedback && showAnswer && ( {/* selected TRUE, show True feedback if it exists */}
<div className="global-feedback mb-2">{globalFeedback}</div> {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 && ( {!showAnswer && handleOnSubmitAnswer && (
<Button <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; 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 { .answer-text.selected {
background-color: var(--main-color); background-color: var(--main-color);
color: white; color: white;
@ -88,8 +121,23 @@
.feedback-container { .feedback-container {
margin-left: 1.1rem; 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 { .global-feedback {
position: relative; position: relative;
padding: 0 1rem; padding: 0 1rem;

View file

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

View file

@ -1,13 +1,14 @@
// TeacherModeQuiz.tsx // TeacherModeQuiz.tsx
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import QuestionComponent from '../Questions/Question'; import QuestionComponent from '../QuestionsDisplay/QuestionDisplay';
import '../../pages/Student/JoinRoom/joinRoom.css'; import '../../pages/Student/JoinRoom/joinRoom.css';
import { QuestionType } from '../../Types/QuestionType'; import { QuestionType } from '../../Types/QuestionType';
// import { QuestionService } from '../../services/QuestionService'; // import { QuestionService } from '../../services/QuestionService';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
import { Question } from 'gift-pegjs';
interface TeacherModeQuizProps { interface TeacherModeQuizProps {
questionInfos: QuestionType; questionInfos: QuestionType;
@ -63,7 +64,7 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
) : ( ) : (
<QuestionComponent <QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question} question={questionInfos.question as Question}
/> />
)} )}
@ -76,7 +77,7 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
{feedbackMessage} {feedbackMessage}
<QuestionComponent <QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question} question={questionInfos.question as Question}
showAnswer={true} showAnswer={true}
/> />
</DialogContent> </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 == '') { if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load 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[] = []; let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) { for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id); 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 // add the folder.title to the QuizType if the folderQuizzes is an array
addFolderTitleToQuizzes(folderQuizzes, folder.title); addFolderTitleToQuizzes(folderQuizzes, folder.title);
quizzes = quizzes.concat(folderQuizzes as QuizType[]) quizzes = quizzes.concat(folderQuizzes as QuizType[])
@ -294,15 +294,25 @@ const Dashboard: React.FC = () => {
try { try {
// folderId: string GET THIS FROM CURRENT FOLDER // folderId: string GET THIS FROM CURRENT FOLDER
// currentTitle: 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) { 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(); const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]); setFolders(userFolders as FolderType[]);
// refresh the page
setSelectedFolderId('');
setSelectedFolderId(renamedFolderId);
} }
} catch (error) { } catch (error) {
console.error('Error renaming folder:', 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 React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { GIFTQuestion, parse } from 'gift-pegjs'; import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs';
import { QuestionType } from '../../../Types/QuestionType'; import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer } from "gift-pegjs/typeGuards";
import LiveResultsComponent from 'src/components/LiveResults/LiveResults'; import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService'; // import { QuestionService } from '../../../services/QuestionService';
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService'; 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 StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
//import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation'; //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 ApiService from '../../../services/ApiService';
import { QuestionType } from 'src/Types/QuestionType';
const ManageRoom: React.FC = () => { const ManageRoom: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -277,7 +278,7 @@ const ManageRoom: React.FC = () => {
const parsedQuestions = [] as QuestionType[]; const parsedQuestions = [] as QuestionType[];
quizQuestionArray.forEach((question, index) => { 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(); parsedQuestions[index].question.id = (index + 1).toString();
}); });
if (parsedQuestions.length === 0) return null; if (parsedQuestions.length === 0) return null;
@ -347,7 +348,7 @@ const ManageRoom: React.FC = () => {
const answerText = answer.toString(); const answerText = answer.toString();
if (questionInfo) { if (questionInfo) {
const question = questionInfo.question as GIFTQuestion; const question = questionInfo.question as ParsedGIFTQuestion;
if (question.type === 'TF') { if (question.type === 'TF') {
return ( return (
(question.isTrue && answerText == 'true') || (question.isTrue && answerText == 'true') ||
@ -355,33 +356,23 @@ const ManageRoom: React.FC = () => {
); );
} else if (question.type === 'MC') { } else if (question.type === 'MC') {
return question.choices.some( return question.choices.some(
(choice) => choice.isCorrect && choice.text.text === answerText (choice) => choice.isCorrect && choice.formattedText.text === answerText
); );
} else if (question.type === 'Numerical') { } else if (question.type === 'Numerical') {
if (question.choices && !Array.isArray(question.choices)) { if (isHighLowNumericalAnswer(question.choices[0])) {
if ( const choice = question.choices[0];
question.choices.type === 'high-low' &&
question.choices.numberHigh &&
question.choices.numberLow
) {
const answerNumber = parseFloat(answerText); const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) { if (!isNaN(answerNumber)) {
return ( return (
answerNumber <= question.choices.numberHigh && answerNumber <= choice.numberHigh &&
answerNumber >= question.choices.numberLow answerNumber >= choice.numberLow
); );
} }
} }
} if (isRangeNumericalAnswer(question.choices[0])) {
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 answerNumber = parseFloat(answerText);
const range = question.choices[0].text.range; const range = question.choices[0].range;
const correctAnswer = question.choices[0].text.number; const correctAnswer = question.choices[0].number;
if (!isNaN(answerNumber)) { if (!isNaN(answerNumber)) {
return ( return (
answerNumber <= correctAnswer + range && answerNumber <= correctAnswer + range &&
@ -389,19 +380,15 @@ const ManageRoom: React.FC = () => {
); );
} }
} }
if ( if (isSimpleNumericalAnswer(question.choices[0])) {
question.choices[0].text.type === 'simple' &&
question.choices[0].text.number
) {
const answerNumber = parseFloat(answerText); const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) { if (!isNaN(answerNumber)) {
return answerNumber === question.choices[0].text.number; return answerNumber === question.choices[0].number;
}
} }
} }
} else if (question.type === 'Short') { } else if (question.type === 'Short') {
return question.choices.some( 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"> <div className="preview-and-result-container">
{currentQuestion && ( {currentQuestion && (
<Question <QuestionDisplay
showAnswer={false} 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> { public async renameFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
try { try {
console.log(`rename folder: folderId: ${folderId}, newTitle: ${newTitle}`);
if (!folderId || !newTitle) { if (!folderId || !newTitle) {
throw new Error(`Le folderId et le nouveau titre sont requis.`); throw new Error(`Le folderId et le nouveau titre sont requis.`);
} }
@ -428,7 +428,9 @@ class ApiService {
const headers = this.constructRequestHeaders(); const headers = this.constructRequestHeaders();
const body = { folderId, newTitle }; 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) { if (result.status !== 200) {
throw new Error(`Le changement de nom de dossier a échoué. Status: ${result.status}`); throw new Error(`Le changement de nom de dossier a échoué. Status: ${result.status}`);
} }
@ -440,6 +442,7 @@ class ApiService {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
const err = error as AxiosError; const err = error as AxiosError;
console.log(JSON.stringify(err));
const data = err.response?.data as { error: string } | undefined; const data = err.response?.data as { error: string } | undefined;
return data?.error || 'Erreur serveur inconnue lors de la requête.'; return data?.error || 'Erreur serveur inconnue lors de la requête.';
} }

View file

@ -21,7 +21,7 @@ class WebSocketService {
private socket: Socket | null = null; private socket: Socket | null = null;
connect(backendUrl: string): Socket { 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: // // Ensure the URL uses wss: if the URL starts with https:
// const protocol = backendUrl.startsWith('https:') ? 'wss:' : 'ws:'; // const protocol = backendUrl.startsWith('https:') ? 'wss:' : 'ws:';

View file

@ -2,14 +2,19 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"src/*": ["src/*"] "src/*": [
"src/*"
]
}, },
"target": "ESNext", "target": "ESNext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@ -17,7 +22,6 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react", "jsx": "react",
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
@ -25,6 +29,16 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"esModuleInterop": true "esModuleInterop": true
}, },
"include": ["src"], // "exclude": [
"references": [{ "path": "./tsconfig.node.json" }] // // "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 () => { it('should rename a folder and return true', async () => {
const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; const folderId = '60c72b2f9b1d8b3a4c8e4d3b';
const newTitle = 'New Folder Name'; const newTitle = 'New Folder Name';
const userId = '12345';
// Mock the database response // Mock the database response
collection.updateOne.mockResolvedValue({ modifiedCount: 1 }); 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.connect).toHaveBeenCalled();
expect(db.collection).toHaveBeenCalledWith('folders'); 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); expect(result).toBe(true);
}); });
it('should return false if the folder does not exist', async () => { it('should return false if the folder does not exist', async () => {
const folderId = '60c72b2f9b1d8b3a4c8e4d3b'; const folderId = '60c72b2f9b1d8b3a4c8e4d3b';
const newTitle = 'New Folder Name'; const newTitle = 'New Folder Name';
const userId = '12345';
// Mock the database response // Mock the database response
collection.updateOne.mockResolvedValue({ modifiedCount: 0 }); 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.connect).toHaveBeenCalled();
expect(db.collection).toHaveBeenCalledWith('folders'); 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); 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 // duplicate

View file

@ -29,7 +29,7 @@ exports.UPDATE_PASSWORD_ERROR = {
code: 400 code: 400
} }
exports.DELETE_USER_ERROR = { 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 code: 400
} }
@ -43,15 +43,15 @@ exports.QUIZ_NOT_FOUND = {
code: 404 code: 404
} }
exports.QUIZ_ALREADY_EXISTS = { exports.QUIZ_ALREADY_EXISTS = {
message: 'Le quiz existe déja.', message: 'Le quiz existe déjà.',
code: 400 code: 400
} }
exports.UPDATE_QUIZ_ERROR = { 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 code: 400
} }
exports.DELETE_QUIZ_ERROR = { 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 code: 400
} }
exports.GETTING_QUIZ_ERROR = { exports.GETTING_QUIZ_ERROR = {
@ -76,15 +76,15 @@ exports.FOLDER_NOT_FOUND = {
code: 404 code: 404
} }
exports.FOLDER_ALREADY_EXISTS = { exports.FOLDER_ALREADY_EXISTS = {
message: 'Le dossier existe déja.', message: 'Le dossier existe déjà.',
code: 400 code: 409
} }
exports.UPDATE_FOLDER_ERROR = { 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 code: 400
} }
exports.DELETE_FOLDER_ERROR = { 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 code: 400
} }
exports.GETTING_FOLDER_ERROR = { exports.GETTING_FOLDER_ERROR = {

View file

@ -127,7 +127,14 @@ class FoldersController {
throw new AppError(FOLDER_NOT_FOUND); 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) { if (!result) {
throw new AppError(UPDATE_FOLDER_ERROR); throw new AppError(UPDATE_FOLDER_ERROR);

View file

@ -2,6 +2,7 @@ class AppError extends Error {
constructor (errorCode) { constructor (errorCode) {
super(errorCode.message) super(errorCode.message)
this.statusCode = errorCode.code; 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 AppError = require("./AppError");
const fs = require('fs'); const fs = require('fs');
const errorHandler = (error, req, res) => { const errorHandler = (error, req, res, _next) => {
console.log("ERROR", error);
if (error instanceof AppError) { if (error instanceof AppError) {
logError(error); logError(error);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,21 +2,22 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const quizzes = require('../app.js').quizzes; const quizzes = require('../app.js').quizzes;
const jwt = require('../middleware/jwtToken.js'); const jwt = require('../middleware/jwtToken.js');
const asyncHandler = require('./routerUtils.js');
if (!quizzes) { if (!quizzes) {
console.error("quizzes is not defined"); console.error("quizzes is not defined");
} }
router.post("/create", jwt.authenticate, quizzes.create); router.post("/create", jwt.authenticate, asyncHandler(quizzes.create));
router.get("/get/:quizId", jwt.authenticate, quizzes.get); router.get("/get/:quizId", jwt.authenticate, asyncHandler(asyncHandler(quizzes.get)));
router.delete("/delete/:quizId", jwt.authenticate, quizzes.delete); router.delete("/delete/:quizId", jwt.authenticate, asyncHandler(quizzes.delete));
router.put("/update", jwt.authenticate, quizzes.update); router.put("/update", jwt.authenticate, asyncHandler(quizzes.update));
router.put("/move", jwt.authenticate, quizzes.move); router.put("/move", jwt.authenticate, asyncHandler(quizzes.move));
router.post("/duplicate", jwt.authenticate, quizzes.duplicate); router.post("/duplicate", jwt.authenticate, asyncHandler(quizzes.duplicate));
router.post("/copy/:quizId", jwt.authenticate, quizzes.copy); router.post("/copy/:quizId", jwt.authenticate, asyncHandler(quizzes.copy));
router.put("/Share", jwt.authenticate, quizzes.share); router.put("/Share", jwt.authenticate, asyncHandler(quizzes.share));
router.get("/getShare/:quizId", jwt.authenticate, quizzes.getShare); router.get("/getShare/:quizId", jwt.authenticate, asyncHandler(quizzes.getShare));
router.post("/receiveShare", jwt.authenticate, quizzes.receiveShare); router.post("/receiveShare", jwt.authenticate, asyncHandler(quizzes.receiveShare));
module.exports = router; 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 router = express.Router();
const users = require('../app.js').users; const users = require('../app.js').users;
const jwt = require('../middleware/jwtToken.js'); const jwt = require('../middleware/jwtToken.js');
const asyncHandler = require('./routerUtils.js');
router.post("/register", users.register); router.post("/register", asyncHandler(users.register));
router.post("/login", users.login); router.post("/login", asyncHandler(users.login));
router.post("/reset-password", users.resetPassword); router.post("/reset-password", asyncHandler(users.resetPassword));
router.post("/change-password", jwt.authenticate, users.changePassword); router.post("/change-password", jwt.authenticate, asyncHandler(users.changePassword));
router.post("/delete-user", jwt.authenticate, users.delete); router.post("/delete-user", jwt.authenticate, asyncHandler(users.delete));
module.exports = router; module.exports = router;