Compare commits

...

94 commits

Author SHA1 Message Date
Christopher (Cris) Fuhrman
6f270b5436
Merge pull request #198 from ets-cfuhrman-pfe/JubaAzul/issue197
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Has been cancelled
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Has been cancelled
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Has been cancelled
Tests / tests (client) (push) Has been cancelled
Tests / tests (server) (push) Has been cancelled
Juba azul/issue197
2025-01-22 20:55:08 -05:00
Christopher (Cris) Fuhrman
4b133400d4
Merge pull request #205 from ets-cfuhrman-pfe/JubaAzul/issue145
Juba azul/issue145
2025-01-22 20:49:54 -05:00
Christopher (Cris) Fuhrman
70e74e2342
Merge pull request #206 from ets-cfuhrman-pfe/JubaAzul/issue74
Update editorQuiz.css
2025-01-22 20:48:13 -05:00
JubaAzul
da4e54534e Update editorQuiz.css 2025-01-22 16:46:09 -05:00
JubaAzul
5018625693 Fix tests 2025-01-22 16:06:23 -05:00
JubaAzul
22f988f2ad Confusion avec la navigation dans les questions à rythme de l'enseignant
Fixes #145
2025-01-22 15:28:45 -05:00
JubaAzul
3e9152fa5c Confusion avec la navigation dans les questions à rythme de l'enseignant
Fixes #145
2025-01-21 15:35:07 -05:00
JubaAzul
394bcf8bc4 enlever modifications de eslint 2025-01-20 12:23:02 -05:00
JubaAzul
70bad72350 Update eslint.config.js 2025-01-20 12:17:50 -05:00
JubaAzul
0a6383b2ec Exemples (copiés) ne sont pas valides et les directives ne sont pas bonnes
Fixes #197
2025-01-20 12:16:21 -05:00
C. Fuhrman
a44cded030 Ajouter watchtower_once -- chercher les mises à jour immédiatement
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Has been cancelled
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Has been cancelled
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Has been cancelled
Tests / tests (client) (push) Has been cancelled
Tests / tests (server) (push) Has been cancelled
2025-01-17 23:00:48 -05:00
Christopher (Cris) Fuhrman
df061fae1a
Merge pull request #183 from ets-cfuhrman-pfe/add-composant-and-snapshot-tests-for-the-EditorQuiz
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
Add composant and snapshot tests for the editor quiz
2025-01-17 18:58:55 -05:00
JubaAzul
a04ec3a2e7
Merge branch 'main' into add-composant-and-snapshot-tests-for-the-EditorQuiz 2025-01-17 10:50:04 -05:00
Christopher (Cris) Fuhrman
4f58c4d412
Merge pull request #194 from ets-cfuhrman-pfe/Scroll-up-arrow-inside-quizEditor
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
Button pour monter en haut de la page d'éditeur de quiz
2025-01-16 17:05:41 -05:00
Christopher (Cris) Fuhrman
b9eb2201d7
Merge pull request #195 from ets-cfuhrman-pfe/JubaAzul/issue192
Risques sécurité dangerouslySetInnerHTML()
2025-01-16 17:00:00 -05:00
JubaAzul
734f48b30a Ajout de configuration de jest.config.cjs 2025-01-16 16:22:16 -05:00
JubaAzul
f997fea3c3 Enlever erreur dans .yml 2025-01-16 12:40:04 -05:00
JubaAzul
1c928c8350 Chemin absolu pour les imports 2025-01-16 12:37:07 -05:00
JubaAzul
89dd55ca8f try import react inside class 2025-01-16 12:02:13 -05:00
JubaAzul
b46e2e3985 removed import in every class, try to update react 2025-01-16 12:02:13 -05:00
JubaAzul
6d56454b02 remove change in .eslintrc.cjs, workflow did not run 2025-01-16 12:02:07 -05:00
JubaAzul
0a932d9617 ignore @ts-expect-error in pipeline 2025-01-16 12:02:07 -05:00
JubaAzul
3eb7ef084c Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer 2025-01-16 12:00:48 -05:00
JubaAzul
3c9b14dd9d Used "@ts-expect-error" instead of "@ts-ignore" 2025-01-16 12:00:48 -05:00
JubaAzul
15805e2e7e import react inside class for pipeline, not IDE 2025-01-16 11:59:02 -05:00
C. Fuhrman
66d57fd451 Merge branch 'main' of https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir
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
2025-01-16 11:20:44 -05:00
JubaAzul
159ed5fa5c removed import in every class, try to update react 2025-01-16 10:40:27 -05:00
JubaAzul
bf97931995 Run with change ob frontend.yml 2025-01-15 13:20:50 -05:00
JubaAzul
f595ccca20 remove change in .eslintrc.cjs, workflow did not run 2025-01-15 11:47:52 -05:00
JubaAzul
d6774b300f ignore @ts-expect-error in pipeline 2025-01-15 11:08:17 -05:00
JubaAzul
470d856afc Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer 2025-01-15 10:40:40 -05:00
JubaAzul
8d14e49095 Used "@ts-expect-error" instead of "@ts-ignore" 2025-01-15 10:30:45 -05:00
JubaAzul
9444603a93 import react inside class for pipeline, not IDE 2025-01-15 10:27:28 -05:00
JubaAzul
77d5ae2862 Fix pipeline, error tests 2025-01-15 09:29:46 -05:00
JubaAzul
f43da4c8ba Risques sécurité dangerouslySetInnerHTML()
Fixes #192
2025-01-15 09:07:56 -05:00
JubaAzul
d18c2c502a Button pour monter en haut de la page d'éditeur de quiz 2025-01-14 17:37:29 -05:00
C. Fuhrman
86b127378b Script to backup the mongo db 2025-01-14 11:18:52 -05:00
JubaAzul
59b7fa02e0 try import react inside class 2025-01-13 20:03:12 -05:00
JubaAzul
b834c8f06f Import react in tests classes 2025-01-13 19:47:37 -05:00
JubaAzul
f201305064 Revert node change 2025-01-13 18:47:28 -05:00
JubaAzul
12b376d606 Correciton pull 2025-01-13 18:34:25 -05:00
JubaAzul
6e39587c68 Update node 2025-01-13 10:25:09 -05:00
Christopher (Cris) Fuhrman
a7e65d4095
Merge pull request #189 from ets-cfuhrman-pfe/valkyrie1-patch-1
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Has been cancelled
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Has been cancelled
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Has been cancelled
Tests / tests (client) (push) Has been cancelled
Tests / tests (server) (push) Has been cancelled
Update LICENSE
2025-01-12 22:40:04 -05:00
valkyrie1
c290ba0080
Update LICENSE
ajout des noms pour hiver 2024
2025-01-12 22:15:09 -05:00
Christopher (Cris) Fuhrman
4c1f2b4501
Merge pull request #188 from ets-cfuhrman-pfe/fuhrmanator/issue187
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Has been cancelled
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Has been cancelled
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Has been cancelled
Tests / tests (client) (push) Has been cancelled
Tests / tests (server) (push) Has been cancelled
Corriger la config des variables d'environnement development/production (docker)
2025-01-11 11:33:40 -05:00
C. Fuhrman
afef820765 isDev n'était pas passé en paramètre (undefined) 2025-01-11 11:30:01 -05:00
C. Fuhrman
7c5c6739fa Définir les variables VITE_* (vide)
Vite est censé ne pas les changer par un .env.* (selon copilot)
2025-01-11 11:28:51 -05:00
C. Fuhrman
ec2f6cc358 Renommer .env à .env.development
Vite est censé ignorer ça en production (docker) tandis que .env serait toujours chargé
2025-01-11 11:27:36 -05:00
Christopher (Cris) Fuhrman
da3e810a32
Merge pull request #186 from ets-cfuhrman-pfe/fuhrmanator/issue185
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
Fuhrmanator/issue185
2025-01-11 03:03:18 -05:00
C. Fuhrman
3c9a11e668 tweak eslint command 2025-01-11 03:01:40 -05:00
C. Fuhrman
236aaa6d09 more feedback on action 2025-01-11 02:47:48 -05:00
C. Fuhrman
63a3131729 run lint 2025-01-11 02:41:26 -05:00
C. Fuhrman
9fa277c8d7 fix errors: 'React' is declared but its value is never read. 2025-01-11 02:32:14 -05:00
C. Fuhrman
50e92fb458 install eslint (client), fix errors (many) 2025-01-11 02:22:14 -05:00
C. Fuhrman
fd81e3b0d1 Install eslint in server, fix errors
configure
2025-01-11 00:03:05 -05:00
Christopher (Cris) Fuhrman
e5e7f61b71
Merge pull request #184 from ets-cfuhrman-pfe/fuhrmanator/issue171
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
Fuhrmanator/issue171
2025-01-10 23:46:01 -05:00
C. Fuhrman
90e42865ba Clean up server startup
Fixes #171
2025-01-10 23:43:15 -05:00
Christopher (Cris) Fuhrman
839ee79912
Merge pull request #153 from ets-cfuhrman-pfe/fuhrmanator/issue78
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
Nom du dossier pour chaque quiz devrait être affiché lorsque "tous les dossiers" sont affichés
2025-01-10 22:38:07 -05:00
Christopher (Cris) Fuhrman
2f14ac0ad2
Merge pull request #182 from ets-cfuhrman-pfe/JubaAzul/issue181
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
Impossible de téleverser une image (Erreur 505)
2025-01-10 18:06:08 -05:00
JubaAzul
427182e7a8 Adding snapshot tests for editor 2025-01-10 17:39:19 -05:00
JubaAzul
ee00feef69 Impossible de téleverser une image (Erreur 505)
Fixes #181
2025-01-10 16:27:00 -05:00
C. Fuhrman
ea6454550c vite needs to understand the src mapping (redundant with tsconfig.json) 2025-01-10 15:56:43 -05:00
C. Fuhrman
6c73cfddc9 use src/constants (abs path) so it can remap when jest runs 2025-01-10 15:46:17 -05:00
C. Fuhrman
51d3d9f473 got back to import.meta (so dev version works) 2025-01-10 15:45:07 -05:00
C. Fuhrman
3faed3625e allow always using 'src/constants' (a kind of absolute path) 2025-01-10 15:44:22 -05:00
C. Fuhrman
535e726e6e use correct env variable 2025-01-10 15:43:12 -05:00
C. Fuhrman
3366fbe18c map the constants import to a mocked one 2025-01-10 15:42:47 -05:00
Christopher (Cris) Fuhrman
0a7f507a47
Merge pull request #180 from ets-cfuhrman-pfe/JubaAzul/issue179
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
Login affiche "Nom de la salle" plutôt que "Mot de passe" et "Nom d'utilisateur" au lieu d' "Adresse courriel"
2025-01-10 13:39:44 -05:00
JubaAzul
81eedfbd29 Login affiche "Nom de la salle" plutôt que "Mot de passe" et "Nom d'utilisateur" au lieu d' "Adresse courriel"
Fixes #179
2025-01-10 13:20:40 -05:00
JubaAzul
ebc0c64ef4 Added tests 2025-01-10 12:39:52 -05:00
C. Fuhrman
4959e02acf Fix broken merge, npm audit fix 2025-01-10 11:09:06 -05:00
Christopher (Cris) Fuhrman
e0ac770230
Merge branch 'main' into fuhrmanator/issue78 2025-01-10 11:01:29 -05:00
Christopher (Cris) Fuhrman
f4e21ee7a9
Merge pull request #175 from ets-cfuhrman-pfe/JubaAzul/issue170
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
LaTeX mal affichée quand on lance le quiz (mais ok en Prévisualisation)
2025-01-09 16:30:09 -05:00
Christopher (Cris) Fuhrman
ecf4f9a819
Merge pull request #174 from ets-cfuhrman-pfe/JubaAzul/issue74
Ajustement de l'interface problématique dans l'éditeur de quiz
2025-01-09 16:28:57 -05:00
JubaAzul
ebd6101a64 LaTeX mal affichée quand on lance le quiz (mais ok en Prévisualisation)
Fixes #170
2025-01-08 16:45:48 -05:00
JubaAzul
c3de76cd20 Ajustement de l'interface problématique dans l'éditeur de quiz
Fixes #74
2025-01-08 13:48:03 -05:00
Christopher (Cris) Fuhrman
b46c1c1934
Merge pull request #172 from ets-cfuhrman-pfe/JubaAzul/issue146
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Has been cancelled
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Has been cancelled
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Has been cancelled
Tests / tests (client) (push) Has been cancelled
Tests / tests (server) (push) Has been cancelled
Icône pour dupliquer est l'icône pour copier (presse papiers)
2025-01-08 10:39:47 -05:00
JubaAzul
5c64f60021 Icône pour dupliquer est l'icône pour copier (presse papiers)
Fixes #146
2025-01-08 10:35:56 -05:00
Christopher (Cris) Fuhrman
f2fe6031bb
Merge pull request #162 from ets-cfuhrman-pfe/ssl-changes
Clean up ENV variables, default to window.location.host when no socket url
2024-10-31 11:36:35 -04:00
C. Fuhrman
3712464873 Clean up ENV variables
Define one for SOCKET url (testing), if it's not defined socket.io defaults to window.location.host
2024-10-31 11:31:26 -04:00
Christopher (Cris) Fuhrman
c754d71623
Merge pull request #161 from ets-cfuhrman-pfe/ssl-changes
Change the way the back-end url is set to a socket url
2024-10-31 01:03:54 -04:00
C. Fuhrman
e2c4e5cba2 look at protocol in the env variable 2024-10-31 01:00:26 -04:00
C. Fuhrman
e113e2f9e9 Merge branch 'main' of https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir into ssl-changes 2024-10-31 00:02:16 -04:00
C. Fuhrman
327875dceb make sure backend env is set for client 2024-10-30 23:58:56 -04:00
Christopher (Cris) Fuhrman
00c0abdf73
Merge pull request #160 from ets-cfuhrman-pfe/ssl-changes
Ssl changes
2024-10-30 23:44:17 -04:00
C. Fuhrman
ed78dc12ef be sure to get the right variable 2024-10-30 23:42:21 -04:00
C. Fuhrman
d75850dac4 remove obsolete version info 2024-10-30 23:41:55 -04:00
Christopher (Cris) Fuhrman
92b97681fc
Merge pull request #159 from ets-cfuhrman-pfe/ssl-changes
Enable source maps for Vite (frontend) to help debugging in the browser.
2024-10-30 22:06:06 -04:00
C. Fuhrman
e04d09b8c5 npm update and set rollupOptions in vite config
The goal is to prevent errors with source maps
2024-10-30 22:04:48 -04:00
C. Fuhrman
123c49662f Enable source maps for Vite (frontend) to help debugging in the browser. 2024-10-30 21:40:35 -04:00
Christopher (Cris) Fuhrman
4181a73a7e
Merge pull request #157 from ets-cfuhrman-pfe/ssl-changes
Ssl changes (debugging, trying to make wss work in prod)
2024-10-30 17:23:43 -04:00
C. Fuhrman
8be2efbe48 try to force the correct url (ws: or wss:) on the socket when it's created 2024-10-30 17:20:13 -04:00
C. Fuhrman
46dae02d47 Console.log debug info 2024-10-30 17:19:11 -04:00
C. Fuhrman
d57c61f78f Nom du dossier pour chaque quiz devrait être affiché lorsque "tous les dossiers" sont affichés
Fixes #78
Uses Cards in Material UI to display the quizzes by folder
2024-10-19 22:58:49 -04:00
101 changed files with 8045 additions and 3598 deletions

View file

@ -23,4 +23,4 @@ jobs:
context: ./client
file: ./client/Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_FRONTEND_REPO }}:latest
tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_FRONTEND_REPO }}:latest

View file

@ -21,9 +21,13 @@ jobs:
with:
node-version: '18'
- name: Install Dependencies and Run Tests
- name: Install Dependencies, lint and Run Tests
run: |
echo "Installing dependencies..."
npm ci
echo "Running ESLint..."
npx eslint .
echo "Running tests..."
npm test
working-directory: ${{ matrix.directory }}

2
.gitignore vendored
View file

@ -73,7 +73,7 @@ web_modules/
.yarn-integrity
# dotenv environment variable files
.env
server/.env
.env.development.local
.env.test.local
.env.production.local

View file

@ -1,6 +1,7 @@
MIT License
Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest
Copyright (c) 2024 Louis-Antoine Caron, Mathieu Roy, Mélanie St-Hilaire, Samy Waddah
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -18,4 +19,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View file

@ -1,2 +1 @@
**/node_modules
.env

2
client/.env.development Normal file
View file

@ -0,0 +1,2 @@
VITE_BACKEND_URL=http://localhost:4400
VITE_BACKEND_SOCKET_URL=http://localhost:4400

View file

@ -1,3 +1,4 @@
// eslint-disable-next-line no-undef
module.exports = {
root: true,
env: { browser: true, es2020: true },

View file

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
};

29
client/eslint.config.js Normal file
View file

@ -0,0 +1,29 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
languageOptions: {
globals: globals.browser,
},
rules: {
"no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_" // Ignore catch clause parameters that start with _
}],
},
settings: {
react: {
version: "detect", // Automatically detect the React version
},
},
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
];

View file

@ -1,3 +1,4 @@
/* eslint-disable no-undef */
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
@ -11,7 +12,11 @@ module.exports = {
//moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFiles: ['./jest.setup.cjs'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
// Permet de mocker les constantes pour les tests avec un chemin absolue (ex: import { ENV_VARIABLES } from 'src/constants';). Voir les "paths" dans tsconfig.json.
'^src/constants$': '<rootDir>/src/__mocks__/constantsMock.tsx',
// Dû au fait que tous les imports de "src/" sont normalisés, Jest doit comprendre le chemin réel. TODO: Trouver une solution pour que Jest se fie à tsconfig.json.
'^src/(.*)$': '<rootDir>/src/$1',
},
transformIgnorePatterns: ['node_modules/(?!nanoid/)'],
};

View file

@ -1,7 +1,3 @@
global.import = {
meta: {
env: {
VITE_BACKEND_URL: 'https://ets-glitch-backend.glitch.me/'
}
}
};
/* eslint-disable no-undef */
process.env.VITE_BACKEND_URL = 'http://localhost:4000/';
process.env.VITE_BACKEND_SOCKET_URL = 'https://ets-glitch-backend.glitch.me/';

7236
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,7 @@
"@mui/material": "^6.1.0",
"@types/uuid": "^9.0.7",
"axios": "^1.6.7",
"dompurify": "^3.2.3",
"esbuild": "^0.23.1",
"gift-pegjs": "^1.0.2",
"jest-environment-jsdom": "^29.7.0",
@ -43,6 +44,7 @@
"@babel/preset-env": "^7.23.3",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@eslint/js": "^9.18.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
@ -54,14 +56,17 @@
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"eslint": "^9.10.0",
"eslint": "^9.18.0",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
"eslint-plugin-react-refresh": "^0.4.12",
"globals": "^15.14.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"typescript": "^5.6.2",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.5",
"vite-plugin-environment": "^1.1.3"
}
}
}

View file

@ -1,3 +1,4 @@
import React from 'react';
// App.tsx
import { Routes, Route } from 'react-router-dom';

View file

@ -2,6 +2,7 @@
export interface QuizType {
_id: string;
folderId: string;
folderName: string;
userId: string;
title: string;
content: string[];

View file

@ -0,0 +1,13 @@
console.log('constantsMock.tsx is loaded');
// constants.tsx
const ENV_VARIABLES = {
MODE: 'production',
VITE_BACKEND_URL: process.env.VITE_BACKEND_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_SOCKET_URL=${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
export { ENV_VARIABLES };

View file

@ -9,6 +9,7 @@ describe('isQuizValid function', () => {
const validQuiz: QuizType = {
_id: '1',
folderId: 'test',
folderName: 'test',
userId: 'user',
created_at: new Date('2021-10-01'),
updated_at: new Date('2021-10-02'),
@ -24,6 +25,7 @@ describe('isQuizValid function', () => {
const invalidQuiz: QuizType = {
_id: '2',
folderId: 'test',
folderName: 'test',
userId: 'user',
title: '',
created_at: new Date('2021-10-01'),
@ -39,6 +41,7 @@ describe('isQuizValid function', () => {
const invalidQuiz: QuizType = {
_id: '2',
folderId: 'test',
folderName: 'test',
userId: 'user',
title: 'sample',
created_at: new Date('2021-10-01'),

View file

@ -1,7 +1,8 @@
// Modal.test.tsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import ConfirmDialog from '../../../components/ConfirmDialog/ConfirmDialog';
import ConfirmDialog from 'src/components/ConfirmDialog/ConfirmDialog';
describe('ConfirmDialog Component', () => {
const mockOnConfirm = jest.fn();

View file

@ -1,7 +1,8 @@
// Editor.test.tsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import Editor from '../../../components/Editor/Editor';
import Editor from 'src/components/Editor/Editor';
describe('Editor Component', () => {
const mockOnEditorChange = jest.fn();

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePreview';
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
describe('GIFTTemplatePreview Component', () => {
test('renders error message when questions contain invalid syntax', () => {

View file

@ -1,7 +1,7 @@
// TextType.test.ts
import { TextFormat } from "gift-pegjs";
import textType from "../../../components/GiftTemplate/templates/TextType";
import textType from "src/components/GiftTemplate/templates/TextType";
describe('TextType', () => {
it('should format text with basic characters correctly', () => {
@ -32,7 +32,7 @@ describe('TextType', () => {
// Hint -- if the output changes because of a change in the code or library, you can update
// by running the test and copying the "Received string:" in jest output
// when it fails (assuming the output is correct)
const expectedOutput = '<span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>';
const expectedOutput = '<span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(textType({ text: input })).toContain(expectedOutput);
});
@ -42,19 +42,17 @@ describe('TextType', () => {
format: 'plain'
};
// hint: katex-display is the class that indicates a separate equation
const expectedOutput = '<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding=\"application/x-tex\">a + b = c</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6667em;vertical-align:-0.0833em;\"></span><span class=\"mord mathnormal\">a</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">+</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6944em;\"></span><span class=\"mord mathnormal\">b</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.4306em;\"></span><span class=\"mord mathnormal\">c</span></span></span></span> ? <span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>';
const expectedOutput = '<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding="application/x-tex">a + b = c</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6667em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">a</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">b</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">c</span></span></span></span> ? <span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">E=mc^2</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.05764em;">E</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.8641em;"></span><span class="mord mathnormal">m</span><span class="mord"><span class="mord mathnormal">c</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8641em;"><span style="top:-3.113em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(textType({ text: input })).toContain(expectedOutput);
});
it('should format text with a katex matrix correctly', () => {
const input: TextFormat = {
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}
a&b \\\\
c&d
\\end\{pmatrix\}`,
// eslint-disable-next-line no-useless-escape
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}\n a&b \\\\\n c&d\n\\end\{pmatrix\}`,
format: 'plain'
};
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}';
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);
});

View file

@ -1,5 +1,5 @@
//color.test.tsx
import { colors } from "../../../../components/GiftTemplate/constants";
import { colors } from "src/components/GiftTemplate/constants";
describe('Colors object', () => {
test('All colors are defined', () => {

View file

@ -1,8 +1,9 @@
//styles.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ParagraphStyle } from '../../../../components/GiftTemplate/constants';
import { ParagraphStyle } from 'src/components/GiftTemplate/constants';
describe('ParagraphStyle', () => {
test('applies styles correctly', () => {
@ -27,6 +28,7 @@ function convertStylesToObject(styles: string): React.CSSProperties {
styles.split(';').forEach((style) => {
const [property, value] = style.split(':');
if (property && value) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(styleObject as any)[property.trim()] = value.trim();
}
});

View file

@ -1,6 +1,6 @@
import '@testing-library/jest-dom';
import { theme } from '../../../../components/GiftTemplate/constants/theme';
import { colors } from '../../../../components/GiftTemplate/constants/colors';
import { theme } from 'src/components/GiftTemplate/constants/theme';
import { colors } from 'src/components/GiftTemplate/constants/colors';
describe('Theme', () => {
test('returns correct light color', () => {

View file

@ -1,10 +1,12 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import AnswerIcon from '../../../../components/GiftTemplate/templates/AnswerIcon';
import AnswerIcon from 'src/components/GiftTemplate/templates/AnswerIcon';
import DOMPurify from 'dompurify';
describe('AnswerIcon', () => {
test('renders correct icon when correct is true', () => {
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: true }) }} />);
const { container } = render(<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(AnswerIcon({ correct: true })) }} />);
const svgElement = container.querySelector('svg');
expect(svgElement).toBeInTheDocument();
@ -19,7 +21,7 @@ describe('AnswerIcon', () => {
});
test('renders incorrect icon when correct is false', () => {
const { container } = render(<div dangerouslySetInnerHTML={{ __html: AnswerIcon({ correct: false }) }} />);
const { container } = render(<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(AnswerIcon({ correct: false })) }} />);
const svgElement = container.querySelector('svg');
expect(svgElement).toBeInTheDocument();

View file

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

View file

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

View file

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

View file

@ -0,0 +1,74 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import TrueFalse from 'src/components/GiftTemplate/templates';
import { TemplateOptions, TrueFalse as TrueFalseType } from 'src/components/GiftTemplate/templates/types';
// Mock the nanoid function
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => 'mocked-id')
}));
const plainTextMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample True/False Title',
stem: { format: 'plain', text: 'Sample Stem' },
isTrue: true,
trueFeedback: { format: 'plain', text: 'Correct!' },
falseFeedback: { format: 'plain', text: 'Incorrect!' },
globalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
};
const katexMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample True/False Title',
stem: { format: 'html', text: '$$\\frac{zzz}{yyy}$$' },
isTrue: true,
trueFeedback: { format: 'moodle', text: 'Correct!' },
falseFeedback: { format: 'html', text: 'Incorrect!' },
globalFeedback: { format: 'markdown', text: 'Sample Global Feedback' }
};
const moodleMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample True/False Title',
stem: { format: 'moodle', text: 'Sample Stem' },
isTrue: true,
trueFeedback: { format: 'moodle', text: 'Correct!' },
falseFeedback: { format: 'moodle', text: 'Incorrect!' },
globalFeedback: { format: 'moodle', text: 'Sample Global Feedback' }
};
const imageMock: TemplateOptions & TrueFalseType = {
type: 'TF',
hasEmbeddedAnswers: false,
title: 'Sample Short Answer Title with Image',
stem: { format: 'plain', text: 'Sample Stem with Image' },
trueFeedback: { format: 'moodle', text: 'Correct!' },
isTrue: true,
falseFeedback: { format: 'moodle', text: 'Incorrect!' },
globalFeedback: { format: 'plain', text: '<img src="https://via.placeholder.com/150" alt="Sample Image" />' }
};
test('TrueFalse snapshot test with plain text', () => {
const { asFragment } = render(<TrueFalse {...plainTextMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('TrueFalse snapshot test with katex', () => {
const { asFragment } = render(<TrueFalse {...katexMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('TrueFalse snapshot test with moodle', () => {
const { asFragment } = render(<TrueFalse {...moodleMock} />);
expect(asFragment()).toMatchSnapshot();
});
test('TrueFalse snapshot test with image', () => {
const { asFragment } = render(<TrueFalse {...imageMock} />);
expect(asFragment()).toMatchSnapshot();
});

View file

@ -0,0 +1,301 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Numerical snapshot test with html 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Numerical Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&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%;
" placeholder="42, 43"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`Numerical snapshot test with image 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Numerical Title with Image&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
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="42, 43, 44"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback with Image&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`Numerical snapshot test with moodle 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Numerical Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
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="42, 43"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`Numerical snapshot test with plain text 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Numerical Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Numérique&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
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="42, 43"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;

View file

@ -0,0 +1,304 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShortAnswer snapshot test with image 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Short Answer Title with Image&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image
&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
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="Answer 1
, Answer 2
, &lt;img src="https://via.placeholder.com/150" alt="Sample Image" /&gt;"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback with Image&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`ShortAnswer snapshot test with katex 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Short Answer Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&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%;
" placeholder="Answer 1, Answer 2"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`ShortAnswer snapshot test with moodle 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Short Answer Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
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="Answer 1, Answer 2"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`ShortAnswer snapshot test with plain text 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Short Answer Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Réponse courte&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;
&lt;div&gt;
&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Réponse: &lt;/span&gt;&lt;input class="gift-input" type="text" style="
display: inline-block;
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="Answer 1, Answer 2"&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;

View file

@ -0,0 +1,438 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TrueFalse snapshot test with image 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample Short Answer Title with Image&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem with Image&lt;/p&gt;&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
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 class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Faux
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;&lt;img src="https://via.placeholder.com/150" alt="Sample Image" /&gt;&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`TrueFalse snapshot test with katex 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample True/False Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;&lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;math xmlns="http://www.w3.org/1998/Math/MathML" display="block"&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mfrac&gt;&lt;mrow&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;mi&gt;z&lt;/mi&gt;&lt;/mrow&gt;&lt;mrow&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;mi&gt;y&lt;/mi&gt;&lt;/mrow&gt;&lt;/mfrac&gt;&lt;/mrow&gt;&lt;annotation encoding="application/x-tex"&gt;\\frac{zzz}{yyy}&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/span&gt;&lt;span class="katex-html" aria-hidden="true"&gt;&lt;span class="base"&gt;&lt;span class="strut" style="height:1.988em;vertical-align:-0.8804em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:1.1076em;"&gt;&lt;span style="top:-2.314em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.03588em;"&gt;yyy&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.23em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="frac-line" style="border-bottom-width:0.04em;"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="top:-3.677em;"&gt;&lt;span class="pstrut" style="height:3em;"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal" style="margin-right:0.04398em;"&gt;zzz&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist" style="height:0.8804em;"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
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 class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Faux
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback
&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`TrueFalse snapshot test with moodle 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample True/False Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
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 class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Faux
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;
exports[`TrueFalse snapshot test with plain text 1`] = `
<DocumentFragment>
&lt;section style="
flex-wrap: wrap;
position: relative;
padding: 1rem 1rem;
margin-bottom: 0.5rem;
background-color: hsl(0, 0%, 100%);
border: solid hsl(0, 0%, 100%) 2px;
border-radius: 6px;
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
"&gt;
&lt;div style="
display: flex;
font-weight: bold;
"&gt;
&lt;span&gt;
&lt;span style="
color: #5271FF;
"&gt;Sample True/False Title&lt;/span&gt;
&lt;/span&gt;
&lt;span style="
margin-left: auto;
padding-left: 0.75rem;
flex: none;
margin-bottom: 10px;"&gt;
&lt;span style="
box-shadow: 0px 1px 3px hsl(0, 0%, 74%);
padding-left: 0.7rem;
padding-right: 0.7rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
border-radius: 4px;
background-color: hsl(0, 0%, 100%);
color: hsl(180, 15%, 41%);
"&gt;Vrai/Faux&lt;/span&gt;
&lt;/span&gt;
&lt;/div&gt;
&lt;p style="
color: hsl(0, 0%, 0%);
"&gt;Sample Stem&lt;/p&gt;&lt;span style="
color: hsl(0, 0%, 0%);
"&gt;Choisir une réponse:&lt;/span&gt;
&lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
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 class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;label style="
display: inline-block;
padding: 0.2em 0 0.2em 0;
color: hsl(0, 0%, 0%);
" for="idmocked-id"&gt;
Faux
&lt;/label&gt;
&lt;svg style="
vertical-align: text-bottom;
display: inline-block;
margin-left: 0.1rem;
margin-right: 0.2rem;
width: 0.75em;
color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span style="
color: hsl(180, 15%, 41%);
"&gt;Incorrect!&lt;/span&gt;
&lt;/input&gt;
&lt;/div&gt;
&lt;div style="
position: relative;
margin-top: 1rem;
padding: 0 1rem;
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%);
"&gt;
&lt;p&gt;Sample Global Feedback&lt;/p&gt;
&lt;/div&gt;&lt;/section&gt;
</DocumentFragment>
`;

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import DragAndDrop from '../../../components/ImportModal/ImportModal';
import DragAndDrop from 'src/components/ImportModal/ImportModal';
describe('DragAndDrop Component', () => {
@ -69,4 +70,4 @@ describe('DragAndDrop Component', () => {
target: { files: [file] },
});
});
});
});

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import LaunchQuizDialog from '../../../components/LaunchQuizDialog/LaunchQuizDialog';
import LaunchQuizDialog from 'src/components/LaunchQuizDialog/LaunchQuizDialog';
// Mock the functions passed as props
const mockHandleOnClose = jest.fn();

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
describe('LoadingCircle', () => {
it('displays the provided text correctly', () => {

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import MultipleChoiceQuestion from '../../../../components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
import MultipleChoiceQuestion from 'src/components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
import { act } from 'react';
import { MemoryRouter } from 'react-router-dom';

View file

@ -1,7 +1,8 @@
// NumericalQuestion.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import NumericalQuestion from '../../../../components/Questions/NumericalQuestion/NumericalQuestion';
import NumericalQuestion from 'src/components/Questions/NumericalQuestion/NumericalQuestion';
describe('NumericalQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn();

View file

@ -1,7 +1,8 @@
// Question.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Questions from '../../../components/Questions/Question';
import Questions from 'src/components/Questions/Question';
import { GIFTQuestion } from 'gift-pegjs';
//

View file

@ -1,7 +1,8 @@
// ShortAnswerQuestion.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import ShortAnswerQuestion from '../../../../components/Questions/ShortAnswerQuestion/ShortAnswerQuestion';
import ShortAnswerQuestion from 'src/components/Questions/ShortAnswerQuestion/ShortAnswerQuestion';
describe('ShortAnswerQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn();
@ -11,6 +12,7 @@ describe('ShortAnswerQuestion Component', () => {
questionTitle: 'Sample Question',
choices: [
{
id: '1',
feedback: {
format: 'text',
text: 'Correct answer feedback'
@ -22,6 +24,7 @@ describe('ShortAnswerQuestion Component', () => {
}
},
{
id: '2',
feedback: null,
isCorrect: false,
text: {
@ -58,7 +61,7 @@ describe('ShortAnswerQuestion Component', () => {
expect(submitButton).toBeDisabled();
});
it('not submited answer if nothing is entered', () => {
it('not submitted answer if nothing is entered', () => {
const submitButton = screen.getByText('Répondre');
fireEvent.click(submitButton);

View file

@ -1,7 +1,8 @@
// TrueFalseQuestion.test.tsx
import React from 'react';
import { render, fireEvent, screen, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import TrueFalseQuestion from '../../../../components/Questions/TrueFalseQuestion/TrueFalseQuestion';
import TrueFalseQuestion from 'src/components/Questions/TrueFalseQuestion/TrueFalseQuestion';
import { MemoryRouter } from 'react-router-dom';
describe('TrueFalseQuestion Component', () => {

View file

@ -1,6 +1,7 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
import { useNavigate } from 'react-router-dom';
jest.mock('react-router-dom', () => ({

View file

@ -1,7 +1,8 @@
// Importez le type UserType s'il n'est pas déjà importé
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage';
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
import { StudentType, Answer } from '../../../Types/StudentType';
describe('StudentWaitPage Component', () => {

View file

@ -1,3 +1,4 @@
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { BrowserRouter } from 'react-router-dom';
@ -32,4 +33,4 @@ describe('Home', () => {
fireEvent.click(teacherButton);
expect(window.location.pathname).toBe('/teacher/dashboard');
});
});
});

View file

@ -1,9 +1,10 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { parse } from 'gift-pegjs';
import { MemoryRouter } from 'react-router-dom';
import { QuestionType } from '../../../../Types/QuestionType';
import StudentModeQuiz from '../../../../components/StudentModeQuiz/StudentModeQuiz';
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
@ -76,21 +77,5 @@ describe('StudentModeQuiz', () => {
});
test('navigates to the previous question', async () => {
act(() => {
fireEvent.click(screen.getByText('Option A'));
});
act(() => {
fireEvent.click(screen.getByText('Répondre'));
});
act(() => {
fireEvent.click(screen.getByText('Question précédente'));
});
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
expect(screen.getByText('Option B')).toBeInTheDocument();
});
});

View file

@ -1,10 +1,11 @@
//TeacherModeQuiz.test.tsx
import React from 'react';
import { render, fireEvent, act } from '@testing-library/react';
import { screen } from '@testing-library/dom';
import '@testing-library/jest-dom';
import { parse } from 'gift-pegjs';
import TeacherModeQuiz from '../../../../components/TeacherModeQuiz/TeacherModeQuiz';
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
import { MemoryRouter } from 'react-router-dom';
// import { mock } from 'node:test';

View file

@ -1,3 +1,4 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MemoryRouter } from 'react-router-dom';

View file

@ -1,3 +1,4 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { MemoryRouter } from 'react-router-dom';

View file

@ -31,8 +31,8 @@ Object.defineProperty(window, 'localStorage', {
// NOTE: this suite seems to be designed around local storage of quizzes (older version, before a database)
describe.skip('QuizService', () => {
const mockQuizzes: QuizType[] = [
{ folderId: 'test', userId: 'user', _id: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
{ folderId: 'test', userId: 'user', _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
{ folderId: 'test', folderName: 'test', userId: 'user', _id: 'quiz1', title: 'Quiz One', content: ['Q1', 'Q2'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
{ folderId: 'test', folderName: 'test', userId: 'user', _id: 'quiz2', title: 'Quiz Two', content: ['Q3', 'Q4'], created_at: new Date('2024-09-15'), updated_at: new Date('2024-09-15') },
];
beforeEach(() => {

View file

@ -1,16 +1,10 @@
//WebsocketService.test.tsx
import WebsocketService from '../../services/WebsocketService';
import { io, Socket } from 'socket.io-client';
import { ENV_VARIABLES } from '../../constants';
import { ENV_VARIABLES } from 'src/constants';
jest.mock('socket.io-client');
jest.mock('../../constants', () => ({
ENV_VARIABLES: {
VITE_BACKEND_URL: 'https://ets-glitch-backend.glitch.me/'
}
}));
describe('WebSocketService', () => {
let mockSocket: Partial<Socket>;
@ -29,13 +23,13 @@ describe('WebSocketService', () => {
});
test('connect should initialize socket connection', () => {
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
expect(io).toHaveBeenCalled();
expect(WebsocketService['socket']).toBe(mockSocket);
});
test('disconnect should terminate socket connection', () => {
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
expect(WebsocketService['socket']).toBeTruthy();
WebsocketService.disconnect();
expect(mockSocket.disconnect).toHaveBeenCalled();
@ -43,7 +37,7 @@ describe('WebSocketService', () => {
});
test('createRoom should emit create-room event', () => {
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.createRoom();
expect(mockSocket.emit).toHaveBeenCalledWith('create-room');
});
@ -52,7 +46,7 @@ describe('WebSocketService', () => {
const roomName = 'testRoom';
const question = { id: 1, text: 'Sample Question' };
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.nextQuestion(roomName, question);
expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question });
});
@ -61,7 +55,7 @@ describe('WebSocketService', () => {
const roomName = 'testRoom';
const questions = [{ id: 1, text: 'Sample Question' }];
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.launchStudentModeQuiz(roomName, questions);
expect(mockSocket.emit).toHaveBeenCalledWith('launch-student-mode', {
roomName,
@ -72,7 +66,7 @@ describe('WebSocketService', () => {
test('endQuiz should emit end-quiz event with correct parameters', () => {
const roomName = 'testRoom';
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.endQuiz(roomName);
expect(mockSocket.emit).toHaveBeenCalledWith('end-quiz', { roomName });
});
@ -81,7 +75,7 @@ describe('WebSocketService', () => {
const enteredRoomName = 'testRoom';
const username = 'testUser';
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
WebsocketService.joinRoom(enteredRoomName, username);
expect(mockSocket.emit).toHaveBeenCalledWith('join-room', { enteredRoomName, username });
});

View file

@ -1,4 +1,5 @@
// GoBackButton.tsx
import React from 'react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
@ -33,7 +34,7 @@ const DisconnectButton: React.FC<Props> = ({
};
const handleOnReturn = () => {
if (!!onReturn) {
if (onReturn) {
onReturn();
} else {
navigate(-1);

View file

@ -1,20 +1,16 @@
import * as React from 'react';
import './footer.css';
interface FooterProps {
type FooterProps = object; //empty object
}
const Footer: React.FC<FooterProps> = ({ }) => {
const Footer: React.FC<FooterProps> = () => {
return (
<div className="footer">
<div className="footer-content">
Réalisé avec à Montréal par des finissantes de l'ETS
Réalisé avec à Montréal par des finissantes de l&apos;ETS
</div>
<div className="footer-links">
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Frontend/tree/main">Frontend GitHub</a>
<span className="divider">|</span>
<a href="https://github.com/louis-antoine-etsmtl/ETS-PFE042-EvalueTonSavoir-Backend">Backend GitHub</a>
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/">GitHub</a>
<span className="divider">|</span>
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki">Wiki GitHub</a>
</div>

View file

@ -21,16 +21,16 @@ const GiftCheatSheet: React.FC = () => {
};
const QuestionVraiFaux = "2+2 \\= 4 ? {T\n}// Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}";
const QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\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 le signe ~ pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse.";
const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n// Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte.";
const QuestionNum ="Question {#=Nombre\n} //OU \nQuestion {#=Nombre:Tolérance\n} // OU \nQuestion {#=PetitNombre..GrandNombre\n}\n// La tolérance est un pourcentage.\n// La réponse doit être comprise entre PetitNombre et GrandNombre";
const QuestionVraiFaux = "2+2 \\= 4 ? {T}\n// Utilisez les valeurs {T}, {F}, {TRUE} \net {FALSE}.";
const QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}\n// La bonne réponse est Ottawa";
const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n}\n/ Utilisez tilde (signe de vague) pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse.";
const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n/ Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte.";
const QuestionNum ="// Question de plage mathématique. \n Quel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
return (
<div className="gift-cheat-sheet">
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
<h2 className="subtitle">Informations pratiques sur l&apos;éditeur</h2>
<span>
L'éditeur utilise le format GIFT (General Import Format Template) créé pour la
L&apos;éditeur utilise le format GIFT (General Import Format Template) créé pour la
plateforme Moodle afin de générer les mini-tests. Ci-dessous vous pouvez retrouver la
syntaxe pour chaque type de question&nbsp;:
</span>
@ -126,7 +126,7 @@ const GiftCheatSheet: React.FC = () => {
<h4> 7. Paramètres optionnels </h4>
<p>
Si vous souhaitez utiliser certains caractères spéciaux dans vos énoncés,
réponses ou feedback, vous devez 'échapper' ces derniers en ajoutant un \
réponses ou feedback, vous devez «échapper» ces derniers en ajoutant un \
devant:
</p>
<pre>
@ -140,9 +140,9 @@ const GiftCheatSheet: React.FC = () => {
<h4> 8. LaTeX et Markdown</h4>
<p>
Les formats LaTeX et Markdown sont supportés dans cette application. Vous devez cependant penser
à 'échapper' les caractères spéciaux mentionnés plus haut.
à «échapper» les caractères spéciaux mentionnés plus haut.
</p>
<p>Exemple d'équation:</p>
<p>Exemple d&apos;équation:</p>
<pre>
<code className="question-code-block selectable-text">{'$$x\\= \\frac\\{y^2\\}\\{4\\}$$'}</code>
<code className="question-code-block selectable-text">{'\n$x\\= \\frac\\{y^2\\}\\{4\\}$'}</code>
@ -167,16 +167,16 @@ const GiftCheatSheet: React.FC = () => {
{'")'}
</code>
</pre>
<p>Exemple d'une question Vrai/Faux avec l'image d'un chat:</p>
<p>Exemple d&apos;une question Vrai/Faux avec l&apos;image d&apos;un chat:</p>
<pre>
<code className="question-code-block">
{'[markdown]Ceci est un chat: \n![Image de chat](https\\://www.example.com\\:8000/chat.jpg "Chat mignon")\n{T}'}
</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'URL de l'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' }}>
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'est plus
Attention: l&apos;ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n&apos;est plus
supportée.
</p>
</div>
@ -184,7 +184,7 @@ const GiftCheatSheet: React.FC = () => {
<div className="question-type">
<h4> 10. Informations supplémentaires </h4>
<p>
GIFT supporte d'autres formats de questions que nous ne gérons pas sur cette
GIFT supporte d&apos;autres formats de questions que nous ne gérons pas sur cette
application.
</p>
<p>Vous pouvez retrouver la Documentation de GIFT (en anglais):</p>

View file

@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
import Template, { ErrorTemplate } from './templates';
import { parse } from 'gift-pegjs';
import './styles.css';
import DOMPurify from 'dompurify';
interface GIFTTemplatePreviewProps {
questions: string[];
@ -73,7 +74,7 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
<div className="error">{error}</div>
) : isPreviewReady ? (
<div data-testid="preview-container">
<div dangerouslySetInnerHTML={{ __html: items }}></div>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(items) }}></div>
</div>
) : (
<div className="loading">Chargement de la prévisualisation...</div>

View file

@ -30,7 +30,7 @@ export function formatLatex(text: string): string {
*/
export default function textType({ text }: TextTypeOptions) {
const formatText = formatLatex(text.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped
let parsedText = '';
switch (text.format) {
case 'moodle':
case 'plain':
@ -40,7 +40,7 @@ export default function textType({ text }: TextTypeOptions) {
// Strip outer paragraph tags (not a great approach with regex)
return formatText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
case 'markdown':
const parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219
parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219
return parsedText.replace(/(^<p>)(.*?)(<\/p>)$/gm, '$2');
default:
throw new Error(`Unsupported text format: ${text.format}`);

View file

@ -168,7 +168,7 @@ const DragAndDrop: React.FC<Props> = ({ handleOnClose, handleOnImport, open, sel
<DialogContentText sx={{ textAlign: 'center' }}>
Déposer des fichiers ici ou
<br />
cliquez pour ouvrir l'explorateur des fichiers
cliquez pour ouvrir l&apos;explorateur des fichiers
</DialogContentText>
</div>
<Download color="primary" />

View file

@ -1,3 +1,4 @@
import React from 'react';
import {
Button,
Dialog,

View file

@ -300,7 +300,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuesti
<TableHead>
<TableRow>
<TableCell className="sticky-column">
<div className="text-base text-bold">Nom d'utilisateur</div>
<div className="text-base text-bold">Nom d&apos;utilisateur</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell

View file

@ -1,3 +1,4 @@
import React from 'react';
import { IconButton } from '@mui/material';
import { ChevronLeft, ChevronRight } from '@mui/icons-material';

View file

@ -4,6 +4,7 @@ import '../questionStyle.css';
import { Button } from '@mui/material';
import textType, { formatLatex } from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
// import Latex from 'react-latex';
type Choices = {
@ -22,6 +23,7 @@ interface Props {
}
const MultipleChoiceQuestion: React.FC<Props> = (props) => {
const { questionStem: questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props;
const [answer, setAnswer] = useState<string>();
@ -39,7 +41,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
return (
<div className="question-container">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} />
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div>
<div className="choices-wrapper mb-1">
{choices.map((choice, i) => {
@ -56,7 +58,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
(choice.isCorrect ? '✅' : '❌')}
<div className={`circle ${selected}`}>{alphabet[i]}</div>
<div className={`answer-text ${selected}`}>
{formatLatex(choice.text.text)}
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(formatLatex(choice.text.text)) }} />
</div>
</Button>
{choice.feedback && showAnswer && (
@ -72,7 +74,9 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
{globalFeedback && showAnswer && (
<div className="global-feedback mb-2">{globalFeedback}</div>
)}
{!showAnswer && handleOnSubmitAnswer && (
<Button
variant="contained"
onClick={() =>
@ -81,6 +85,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
disabled={answer === undefined}
>
Répondre
</Button>
)}
</div>

View file

@ -4,6 +4,7 @@ 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;
@ -34,7 +35,7 @@ const NumericalQuestion: React.FC<Props> = (props) => {
return (
<div className="question-wrapper">
<div>
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} />
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div>
{showAnswer ? (
<>

View file

@ -42,7 +42,7 @@ const Question: React.FC<QuestionProps> = ({
questionTypeComponent = (
<MultipleChoiceQuestion
questionStem={question.stem}
choices={question.choices}
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text}
@ -78,7 +78,7 @@ const Question: React.FC<QuestionProps> = ({
questionTypeComponent = (
<ShortAnswerQuestion
questionContent={question.stem}
choices={question.choices}
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer}
globalFeedback={question.globalFeedback?.text}

View file

@ -4,12 +4,14 @@ import '../questionStyle.css';
import { Button, TextField } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
type Choices = {
feedback: { format: string; text: string } | null;
isCorrect: boolean;
text: { format: string; text: string };
weigth?: number;
id: string;
};
interface Props {
@ -27,13 +29,15 @@ const ShortAnswerQuestion: React.FC<Props> = (props) => {
return (
<div className="question-wrapper">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} />
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div>
{showAnswer ? (
<>
<div className="correct-answer-text mb-1">
{choices.map((choice) => (
<div className="mb-1">{choice.text.text}</div>
<div key={choice.id} className="mb-1">
{choice.text.text}
</div>
))}
</div>
{globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}

View file

@ -4,6 +4,7 @@ import '../questionStyle.css';
import { Button } from '@mui/material';
import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify';
interface Props {
questionContent: TextFormat;
@ -27,7 +28,7 @@ const TrueFalseQuestion: React.FC<Props> = (props) => {
return (
<div className="question-container">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: textType({ text: questionContent }) }} />
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({ text: questionContent })) }} />
</div>
<div className="choices-wrapper mb-1">
<Button

View file

@ -1,4 +1,5 @@
// GoBackButton.tsx
import React from 'react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
@ -33,7 +34,7 @@ const ReturnButton: React.FC<Props> = ({
};
const handleOnReturn = () => {
if (!!onReturn) {
if (onReturn) {
onReturn();
} else {
navigate(-1);

View file

@ -1,14 +1,13 @@
// StudentModeQuiz.tsx
import React, { useEffect, useState } from 'react';
import QuestionComponent from '../Questions/Question';
import '../../pages/Student/JoinRoom/joinRoom.css';
import { QuestionType } from '../../Types/QuestionType';
// import { QuestionService } from '../../services/QuestionService';
import { Button } from '@mui/material';
import QuestionNavigation from '../QuestionNavigation/QuestionNavigation';
import { ChevronLeft, ChevronRight } from '@mui/icons-material';
import DisconnectButton from '../../components/DisconnectButton/DisconnectButton';
//import QuestionNavigation from '../QuestionNavigation/QuestionNavigation';
//import { ChevronLeft, ChevronRight } from '@mui/icons-material';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
interface StudentModeQuizProps {
questions: QuestionType[];
@ -25,10 +24,10 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
// const [imageUrl, setImageUrl] = useState('');
const previousQuestion = () => {
setQuestion(questions[Number(questionInfos.question?.id) - 2]);
setIsAnswerSubmitted(false);
};
// const previousQuestion = () => {
// setQuestion(questions[Number(questionInfos.question?.id) - 2]);
// setIsAnswerSubmitted(false);
// };
useEffect(() => {}, [questionInfos]);
@ -55,12 +54,12 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
<div className="overflow-auto">
<div className="question-component-container">
<div className="mb-5">
<QuestionNavigation
{/* <QuestionNavigation
currentQuestionId={Number(questionInfos.question.id)}
questionsLength={questions.length}
previousQuestion={previousQuestion}
nextQuestion={nextQuestion}
/>
/> */}
</div>
<QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer}
@ -69,7 +68,7 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
/>
<div className="center-h-align mt-1/2">
<div className="w-12">
<Button
{/* <Button
variant="outlined"
onClick={previousQuestion}
fullWidth
@ -77,14 +76,14 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
disabled={Number(questionInfos.question.id) <= 1}
>
Question précédente
</Button>
</Button> */}
</div>
<div className="w-12">
<Button
<Button style={{ display: isAnswerSubmitted ? 'block' : 'none' }}
variant="outlined"
onClick={nextQuestion}
fullWidth
endIcon={<ChevronRight />}
//endIcon={<ChevronRight />}
disabled={Number(questionInfos.question.id) >= questions.length}
>
Question suivante
@ -93,7 +92,7 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
</div>
</div>
</div>
</div>
</div>
);
};

View file

@ -1,3 +1,4 @@
import React from 'react';
import { Box, Button, Chip } from '@mui/material';
import { StudentType } from '../../Types/StudentType';
import { PlayArrow } from '@mui/icons-material';

View file

@ -6,7 +6,7 @@ import QuestionComponent from '../Questions/Question';
import '../../pages/Student/JoinRoom/joinRoom.css';
import { QuestionType } from '../../Types/QuestionType';
// import { QuestionService } from '../../services/QuestionService';
import DisconnectButton from '../../components/DisconnectButton/DisconnectButton';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
interface TeacherModeQuizProps {

View file

@ -1,7 +1,11 @@
// constants.tsx
const ENV_VARIABLES = {
MODE: 'production',
VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || ""
VITE_BACKEND_URL: import.meta.env.VITE_BACKEND_URL || "",
VITE_BACKEND_SOCKET_URL: import.meta.env.VITE_BACKEND_SOCKET_URL || "",
};
console.log(`ENV_VARIABLES.VITE_BACKEND_URL=${ENV_VARIABLES.VITE_BACKEND_URL}`);
console.log(`ENV_VARIABLES.VITE_BACKEND_SOCKET_URL=${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
export { ENV_VARIABLES };

View file

@ -1,3 +1,4 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
@ -27,10 +28,16 @@ const theme = createTheme({
}
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</BrowserRouter>
);
const rootElement = document.getElementById('root');
if (rootElement) {
ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</BrowserRouter>
);
} else {
console.error('Root element not found');
}

View file

@ -1,19 +1,19 @@
import React, { useEffect, useState } from 'react';
import { Socket } from 'socket.io-client';
import { ENV_VARIABLES } from '../../../constants';
import { ENV_VARIABLES } from 'src/constants';
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz';
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz';
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import './joinRoom.css';
import { QuestionType } from '../../../Types/QuestionType';
import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../components/LoginContainer/LoginContainer'
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
const JoinRoom: React.FC = () => {
const [roomName, setRoomName] = useState('');
@ -34,7 +34,8 @@ const JoinRoom: React.FC = () => {
}, []);
const handleCreateSocket = () => {
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
socket.on('join-success', () => {
setIsWaitingForTeacher(true);
@ -63,10 +64,10 @@ const JoinRoom: React.FC = () => {
socket.on('connect_error', (error) => {
switch (error.message) {
case 'timeout':
setConnectionError("Le serveur n'est pas disponible");
setConnectionError("JoinRoom: timeout: Le serveur n'est pas disponible");
break;
case 'websocket error':
setConnectionError("Le serveur n'est pas disponible");
setConnectionError("JoinRoom: websocket error: Le serveur n'est pas disponible");
break;
}
setIsConnecting(false);

View file

@ -3,14 +3,14 @@ import { useNavigate } from 'react-router-dom';
import React, { useState, useEffect, useMemo } from 'react';
import { parse } from 'gift-pegjs';
import Template from '../../../components/GiftTemplate/templates';
import Template from 'src/components/GiftTemplate/templates';
import { QuizType } from '../../../Types/QuizType';
import { FolderType } from '../../../Types/FolderType';
// import { QuestionService } from '../../../services/QuestionService';
import ApiService from '../../../services/ApiService';
import './dashboard.css';
import ImportModal from '../../../components/ImportModal/ImportModal';
import ImportModal from 'src/components/ImportModal/ImportModal';
//import axios from 'axios';
import {
@ -18,8 +18,11 @@ import {
IconButton,
InputAdornment,
Button,
Card,
Tooltip,
NativeSelect
NativeSelect,
CardContent,
styled,
} from '@mui/material';
import {
Search,
@ -27,19 +30,50 @@ import {
FileDownload,
Add,
Upload,
FolderCopy,
ContentCopy,
Edit,
Share,
// DriveFileMove
} from '@mui/icons-material';
// Create a custom-styled Card component
const CustomCard = styled(Card)({
overflow: 'visible', // Override the overflow property
position: 'relative',
margin: '40px 0 20px 0', // Add top margin to make space for the tab
borderRadius: '8px',
paddingTop: '20px', // Ensure content inside the card doesn't overlap with the tab
});
const Dashboard: React.FC = () => {
const navigate = useNavigate();
const [quizzes, setQuizzes] = useState<QuizType[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [showImportModal, setShowImportModal] = useState<boolean>(false);
const [folders, setFolders] = useState<FolderType[]>([]);
const [selectedFolder, setSelectedFolder] = useState<string>(''); // Selected folder
const [selectedFolderId, setSelectedFolderId] = useState<string>(''); // Selected folder
// Filter quizzes based on search term
// const filteredQuizzes = quizzes.filter(quiz =>
// quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
// );
const filteredQuizzes = useMemo(() => {
return quizzes.filter(
(quiz) =>
quiz && quiz.title && quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [quizzes, searchTerm]);
// Group quizzes by folder
const quizzesByFolder = filteredQuizzes.reduce((acc, quiz) => {
if (!acc[quiz.folderName]) {
acc[quiz.folderName] = [];
}
acc[quiz.folderName].push(quiz);
return acc;
}, {} as Record<string, QuizType[]>);
useEffect(() => {
const fetchData = async () => {
@ -48,7 +82,7 @@ const Dashboard: React.FC = () => {
return;
}
else {
let userFolders = await ApiService.getUserFolders();
const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
}
@ -58,40 +92,23 @@ const Dashboard: React.FC = () => {
fetchData();
}, []);
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedFolder(event.target.value);
setSelectedFolderId(event.target.value);
};
useEffect(() => {
const fetchQuizzesForFolder = async () => {
if (selectedFolder == '') {
if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes")
var quizzes: QuizType[] = [];
let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);
console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
// add the folder.title to the QuizType if the folderQuizzes is an array
addFolderTitleToQuizzes(folderQuizzes, folder.title);
quizzes = quizzes.concat(folderQuizzes as QuizType[])
}
@ -99,17 +116,19 @@ const Dashboard: React.FC = () => {
}
else {
console.log("show some quizzes")
const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
console.log("folderQuizzes: ", folderQuizzes);
// get the folder title from its id
const folderTitle = folders.find((folder) => folder._id === selectedFolderId)?.title || '';
addFolderTitleToQuizzes(folderQuizzes, folderTitle);
setQuizzes(folderQuizzes as QuizType[]);
}
};
fetchQuizzesForFolder();
}, [selectedFolder]);
}, [selectedFolderId]);
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -134,22 +153,24 @@ const Dashboard: React.FC = () => {
const handleDuplicateQuiz = async (quiz: QuizType) => {
try {
await ApiService.duplicateQuiz(quiz._id);
if (selectedFolder == '') {
if (selectedFolderId == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes")
var quizzes: QuizType[] = [];
console.log("show all quizzes")
let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);
console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
quizzes = quizzes.concat(folderQuizzes as QuizType[])
addFolderTitleToQuizzes(folderQuizzes, folder.title);
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
}
setQuizzes(quizzes as QuizType[]);
}
else {
console.log("show some quizzes")
const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
addFolderTitleToQuizzes(folderQuizzes, selectedFolderId);
setQuizzes(folderQuizzes as QuizType[]);
}
@ -158,13 +179,6 @@ const Dashboard: React.FC = () => {
}
};
const filteredQuizzes = useMemo(() => {
return quizzes.filter(
(quiz) =>
quiz && quiz.title && quiz.title.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [quizzes, searchTerm]);
const handleOnImport = () => {
setShowImportModal(true);
@ -182,6 +196,7 @@ const Dashboard: React.FC = () => {
// questions[i] = QuestionService.ignoreImgTags(questions[i]);
const parsedItem = parse(questions[i]);
Template(parsedItem[0]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
return false;
}
@ -190,30 +205,6 @@ const Dashboard: React.FC = () => {
return true;
};
// const handleMoveQuiz = async (quiz: QuizType, newFolderId: string) => {
// try {
// await ApiService.moveQuiz(quiz._id, newFolderId);
// if (selectedFolder == '') {
// const folders = await ApiService.getUserFolders();
// var quizzes: QuizType[] = [];
// for (const folder of folders as FolderType[]) {
// const folderQuizzes = await ApiService.getFolderContent(folder._id);
// quizzes = quizzes.concat(folderQuizzes as QuizType[])
// }
// setQuizzes(quizzes as QuizType[]);
// }
// else {
// const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
// setQuizzes(folderQuizzes as QuizType[]);
// }
// } catch (error) {
// console.error('Error moving quiz:', error);
// }
// };
const downloadTxtFile = async (quiz: QuizType) => {
try {
@ -226,7 +217,7 @@ const Dashboard: React.FC = () => {
//const { title, content } = selectedQuiz;
let quizContent = "";
let title = selectedQuiz.title;
const title = selectedQuiz.title;
console.log(selectedQuiz.content);
selectedQuiz.content.forEach((question, qIndex) => {
const formattedQuestion = question.trim();
@ -263,7 +254,7 @@ const Dashboard: React.FC = () => {
const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
setSelectedFolder(newlyCreatedFolder._id);
setSelectedFolderId(newlyCreatedFolder._id);
}
} catch (error) {
@ -273,18 +264,17 @@ const Dashboard: React.FC = () => {
const handleDeleteFolder = async () => {
try {
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
if (confirmed) {
await ApiService.deleteFolder(selectedFolder);
await ApiService.deleteFolder(selectedFolderId);
const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
}
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes")
var quizzes: QuizType[] = [];
console.log("show all quizzes")
let quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);
@ -293,19 +283,20 @@ const Dashboard: React.FC = () => {
}
setQuizzes(quizzes as QuizType[]);
setSelectedFolder('');
setSelectedFolderId('');
} catch (error) {
console.error('Error deleting folder:', error);
}
};
const handleRenameFolder = async () => {
try {
// folderId: string GET THIS FROM CURRENT FOLDER
// currentTitle: string GET THIS FROM CURRENT FOLDER
const newTitle = prompt('Entrée le nouveau nom du fichier', "Nouveau nom de dossier");
if (newTitle) {
await ApiService.renameFolder(selectedFolder, newTitle);
await ApiService.renameFolder(selectedFolderId, newTitle);
const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
@ -314,15 +305,16 @@ const Dashboard: React.FC = () => {
console.error('Error renaming folder:', error);
}
};
const handleDuplicateFolder = async () => {
try {
// folderId: string GET THIS FROM CURRENT FOLDER
await ApiService.duplicateFolder(selectedFolder);
await ApiService.duplicateFolder(selectedFolderId);
// TODO set the selected folder to be the duplicated folder
const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
setSelectedFolder(newlyCreatedFolder._id);
setSelectedFolderId(newlyCreatedFolder._id);
} catch (error) {
console.error('Error duplicating folder:', error);
}
@ -392,7 +384,7 @@ const Dashboard: React.FC = () => {
<NativeSelect
id="select-folder"
color="primary"
value={selectedFolder}
value={selectedFolderId}
onChange={handleSelectFolder}
>
<option value=""> Tous les dossiers... </option>
@ -415,7 +407,7 @@ const Dashboard: React.FC = () => {
<IconButton
color="primary"
onClick={handleRenameFolder}
disabled={selectedFolder == ''} // cannot action on all
disabled={selectedFolderId == ''} // cannot action on all
> <Edit /> </IconButton>
</Tooltip>
@ -423,8 +415,8 @@ const Dashboard: React.FC = () => {
<IconButton
color="primary"
onClick={handleDuplicateFolder}
disabled={selectedFolder == ''} // cannot action on all
> <ContentCopy /> </IconButton>
disabled={selectedFolderId == ''} // cannot action on all
> <FolderCopy /> </IconButton>
</Tooltip>
<Tooltip title="Supprimer dossier" placement="top">
@ -432,7 +424,7 @@ const Dashboard: React.FC = () => {
aria-label="delete"
color="primary"
onClick={handleDeleteFolder}
disabled={selectedFolder == ''} // cannot action on all
disabled={selectedFolderId == ''} // cannot action on all
> <DeleteOutline /> </IconButton>
</Tooltip>
</div>
@ -460,74 +452,72 @@ const Dashboard: React.FC = () => {
</div>
<div className='list'>
{Object.keys(quizzesByFolder).map(folderName => (
<CustomCard key={folderName} className='folder-card'>
<div className='folder-tab'>{folderName}</div>
<CardContent>
{quizzesByFolder[folderName].map((quiz: QuizType) => (
<div className='quiz' key={quiz._id}>
<div className='title'>
<Tooltip title="Lancer quiz" placement="top">
<Button
variant="outlined"
onClick={() => handleLancerQuiz(quiz)}
disabled={!validateQuiz(quiz.content)}
>
{`${quiz.title} (${quiz.content.length} question${quiz.content.length > 1 ? 's' : ''})`}
</Button>
</Tooltip>
</div>
{filteredQuizzes.map((quiz: QuizType) => (
<div className='quiz'>
<div className='title'>
<Tooltip title="Lancer quiz" placement="top">
<Button
variant="outlined"
onClick={() => handleLancerQuiz(quiz)}
disabled={!validateQuiz(quiz.content)}
>
{quiz.title}
</Button>
</Tooltip>
</div>
<div className='actions'>
<Tooltip title="Télécharger quiz" placement="top">
<IconButton
color="primary"
onClick={() => downloadTxtFile(quiz)}
> <FileDownload /> </IconButton>
</Tooltip>
<div className='actions'>
<Tooltip title="Télécharger quiz" placement="top">
<IconButton
color="primary"
onClick={() => downloadTxtFile(quiz)}
> <FileDownload /> </IconButton>
</Tooltip>
<Tooltip title="Modifier quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleEditQuiz(quiz)}
> <Edit /> </IconButton>
</Tooltip>
<Tooltip title="Modifier quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleEditQuiz(quiz)}
> <Edit /> </IconButton>
</Tooltip>
<Tooltip title="Dupliquer quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleDuplicateQuiz(quiz)}
> <ContentCopy /> </IconButton>
</Tooltip>
{/* <Tooltip title="Bouger quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleMoveQuiz(quiz)}
> <DriveFileMove /> </IconButton>
</Tooltip> */}
<Tooltip title="Supprimer quiz" placement="top">
<IconButton
aria-label="delete"
color="primary"
onClick={() => handleRemoveQuiz(quiz)}
> <DeleteOutline /> </IconButton>
</Tooltip>
<Tooltip title="Dupliquer quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleDuplicateQuiz(quiz)}
> <ContentCopy /> </IconButton>
</Tooltip>
<Tooltip title="Supprimer quiz" placement="top">
<IconButton
aria-label="delete"
color="primary"
onClick={() => handleRemoveQuiz(quiz)}
> <DeleteOutline /> </IconButton>
</Tooltip>
<Tooltip title="Partager quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleShareQuiz(quiz)}
> <Share /> </IconButton>
</Tooltip>
</div>
</div>
<Tooltip title="Partager quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleShareQuiz(quiz)}
> <Share /> </IconButton>
</Tooltip>
</div>
</div>
))}
</CardContent>
</CustomCard>
))}
</div>
<ImportModal
open={showImportModal}
handleOnClose={() => setShowImportModal(false)}
handleOnImport={handleOnImport}
selectedFolder={selectedFolder}
selectedFolder={selectedFolderId}
/>
</div>
@ -535,3 +525,11 @@ const Dashboard: React.FC = () => {
};
export default Dashboard;
function addFolderTitleToQuizzes(folderQuizzes: string | QuizType[], folderName: string) {
if (Array.isArray(folderQuizzes))
folderQuizzes.forEach((quiz) => {
quiz.folderName = folderName;
console.log(`quiz: ${quiz.title} folder: ${quiz.folderName}`);
});
}

View file

@ -77,4 +77,43 @@ div:has(> #select-folder) {
display: flex;
flex-direction: row;
align-items: center;
}
}
.dashboard .list .quiz .actions {
flex-shrink: 0;
display: flex;
flex-direction: row;
align-items: center;
}
.folder-card {
position: relative;
/* margin: 40px 0 20px 0; /* Add top margin to make space for the tab */
border-radius: 8px;
color: #f9f9f9;
--outline-color: #e1e1e1;
border: 2px solid var(--outline-color);
}
.folder-tab {
position: absolute;
top: -33px;
left: 9px;
padding: 5px 10px;
border-radius: 8px 8px 0 0;
font-weight: bold;
white-space: nowrap; /* Prevent text from wrapping */
display: inline-block; /* Ensure the tab width is based on content */
border: 2px solid var(--outline-color);
border-bottom-style: none;
background-color: white; /* Optional: background color to match the card */
color: #3f51b5; /* Text color to match the outline */
}
/* .folder-card:nth-child(odd) {
background-color: #f9f9f9;
}
.folder-card:nth-child(even) {
background-color: #e0e0e0;
} */

View file

@ -1,18 +1,18 @@
// EditorQuiz.tsx
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, CSSProperties } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { FolderType } from '../../../Types/FolderType';
import Editor from '../../../components/Editor/Editor';
import GiftCheatSheet from '../../../components/GIFTCheatSheet/GiftCheatSheet';
import GIFTTemplatePreview from '../../../components/GiftTemplate/GIFTTemplatePreview';
import Editor from 'src/components/Editor/Editor';
import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet';
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
import { QuizType } from '../../../Types/QuizType';
import './editorQuiz.css';
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material';
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
import ApiService from '../../../services/ApiService';
import { escapeForGIFT } from '../../../utils/giftUtils';
@ -40,6 +40,26 @@ const QuizForm: React.FC = () => {
};
const fileInputRef = useRef<HTMLInputElement>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [showScrollButton, setShowScrollButton] = useState(false);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 300) {
setShowScrollButton(true);
} else {
setShowScrollButton(false);
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
useEffect(() => {
const fetchData = async () => {
@ -162,8 +182,10 @@ const QuizForm: React.FC = () => {
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
}
};
@ -245,7 +267,7 @@ const QuizForm: React.FC = () => {
onClose={() => setDialogOpen(false)} >
<DialogTitle>Erreur</DialogTitle>
<DialogContent>
Veuillez d'abord choisir une image à téléverser.
Veuillez d&apos;abord choisir une image à téléverser.
</DialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)} color="primary">
@ -290,8 +312,32 @@ const QuizForm: React.FC = () => {
</div>
{showScrollButton && (
<Button
onClick={scrollToTop}
variant="contained"
color="primary"
style={scrollToTopButtonStyle}
title="Scroll to top"
>
</Button>
)}
</div>
);
};
const scrollToTopButtonStyle: CSSProperties = {
position: 'fixed',
bottom: '40px',
right: '50px',
padding: '10px',
fontSize: '16px',
color: 'white',
backgroundColor: '#5271ff',
border: 'none',
cursor: 'pointer',
zIndex: 1000,
};
export default QuizForm;

View file

@ -7,7 +7,7 @@ import './Login.css';
import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../components/LoginContainer/LoginContainer'
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService';
const Login: React.FC = () => {
@ -28,7 +28,7 @@ const Login: React.FC = () => {
const login = async () => {
const result = await ApiService.login(email, password);
if (result != true) {
if (typeof result === "string") {
setConnectionError(result);
return;
}
@ -49,7 +49,7 @@ const Login: React.FC = () => {
variant="outlined"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Nom d'utilisateur"
placeholder="Adresse courriel"
sx={{ marginBottom: '1rem' }}
fullWidth
/>
@ -60,7 +60,7 @@ const Login: React.FC = () => {
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Nom de la salle"
placeholder="Mot de passe"
sx={{ marginBottom: '1rem' }}
fullWidth
/>

View file

@ -4,21 +4,21 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client';
import { GIFTQuestion, parse } from 'gift-pegjs';
import { QuestionType } from '../../../Types/QuestionType';
import LiveResultsComponent from '../../../components/LiveResults/LiveResults';
import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService';
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
import { QuizType } from '../../../Types/QuizType';
import './manageRoom.css';
import { ENV_VARIABLES } from '../../../constants';
import { ENV_VARIABLES } from 'src/constants';
import { StudentType, Answer } from '../../../Types/StudentType';
import { Button } from '@mui/material';
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
import { Refresh, Error } from '@mui/icons-material';
import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton';
import QuestionNavigation from '../../../components/QuestionNavigation/QuestionNavigation';
import Question from '../../../components/Questions/Question';
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
//import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation';
import Question from 'src/components/Questions/Question';
import ApiService from '../../../services/ApiService';
const ManageRoom: React.FC = () => {
@ -49,6 +49,7 @@ const ManageRoom: React.FC = () => {
setQuiz(quiz as QuizType);
if (!socket) {
console.log(`no socket in ManageRoom, creating one.`);
createWebSocketRoom();
}
@ -80,15 +81,16 @@ const ManageRoom: React.FC = () => {
};
const createWebSocketRoom = () => {
console.log('Creating WebSocket room...');
setConnectingError('');
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL);
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
socket.on('connect', () => {
webSocketService.createRoom();
});
socket.on('connect_error', (error) => {
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
console.error('WebSocket connection error:', error);
console.error('ManageRoom: WebSocket connection error:', error);
});
socket.on('create-success', (roomName: string) => {
setRoomName(roomName);
@ -122,8 +124,8 @@ const ManageRoom: React.FC = () => {
// This is here to make sure the correct value is sent when user join
if (socket) {
console.log(`Listening for user-joined in room ${roomName}`);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
socket.on('user-joined', (_student: StudentType) => {
if (quizMode === 'teacher') {
webSocketService.nextQuestion(roomName, currentQuestion);
} else if (quizMode === 'student') {
@ -265,7 +267,6 @@ const ManageRoom: React.FC = () => {
const prevQuestionIndex = Number(currentQuestion?.question.id) - 2; // -2 because question.id starts at index 1
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
setCurrentQuestion(quizQuestions[prevQuestionIndex]);
webSocketService.nextQuestion(roomName, quizQuestions[prevQuestionIndex]);
};
@ -459,12 +460,12 @@ const ManageRoom: React.FC = () => {
{quizMode === 'teacher' && (
<div className="mb-1">
<QuestionNavigation
{/* <QuestionNavigation
currentQuestionId={Number(currentQuestion?.question.id)}
questionsLength={quizQuestions?.length}
previousQuestion={previousQuestion}
nextQuestion={nextQuestion}
/>
/> */}
</div>
)}
@ -491,12 +492,23 @@ const ManageRoom: React.FC = () => {
</div>
{quizMode === 'teacher' && (
<div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}>
<div className="previousQuestionButton">
<Button onClick={previousQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) <= 1}>
Question précédente
</Button>
</div>
<div className="nextQuestionButton">
<Button onClick={nextQuestion} variant="contained">
<Button onClick={nextQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) >=quizQuestions.length}
>
Prochaine question
</Button>
</div>
)}
</div> )}
</div>

View file

@ -7,7 +7,7 @@ import React, { useEffect, useState } from 'react';
import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../components/LoginContainer/LoginContainer'
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService';
const Register: React.FC = () => {
@ -28,7 +28,7 @@ const Register: React.FC = () => {
const register = async () => {
const result = await ApiService.register(email, password);
if (result != true) {
if (typeof result === 'string') {
setConnectionError(result);
return;
}
@ -70,7 +70,7 @@ const Register: React.FC = () => {
sx={{ marginBottom: `${connectionError && '2rem'}` }}
disabled={!email || !password}
>
S'inscrire
S&apos;inscrire
</LoadingButton>
</LoginContainer>

View file

@ -7,7 +7,7 @@ import React, { useEffect, useState } from 'react';
import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../../components/LoginContainer/LoginContainer'
import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService';
const ResetPassword: React.FC = () => {
@ -27,7 +27,7 @@ const ResetPassword: React.FC = () => {
const reset = async () => {
const result = await ApiService.resetPassword(email);
if (result != true) {
if (typeof result === 'string') {
setConnectionError(result);
return;
}

View file

@ -7,7 +7,7 @@ import { FolderType } from '../../../Types/FolderType';
import './share.css';
import { Button, NativeSelect } from '@mui/material';
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
import ApiService from '../../../services/ApiService';

View file

@ -1,8 +1,10 @@
import axios, { AxiosError, AxiosResponse } from 'axios';
import { ENV_VARIABLES } from '../constants';
import { QuizType } from '../Types/QuizType';
import { FolderType } from '../Types/FolderType';
import { FolderType } from 'src/Types/FolderType';
import { QuizType } from 'src/Types/QuizType';
import { ENV_VARIABLES } from 'src/constants';
type ApiResponse = boolean | string;
class ApiService {
private BASE_URL: string;
@ -17,7 +19,7 @@ class ApiService {
return `${this.BASE_URL}/api${endpoint}`;
}
private constructRequestHeaders(): any {
private constructRequestHeaders() {
if (this.isLoggedIn()) {
return {
Authorization: `Bearer ${this.getToken()}`,
@ -86,7 +88,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async register(email: string, password: string): Promise<any> {
public async register(email: string, password: string): Promise<ApiResponse> {
try {
if (!email || !password) {
@ -122,7 +124,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async login(email: string, password: string): Promise<any> {
public async login(email: string, password: string): Promise<ApiResponse> {
try {
if (!email || !password) {
@ -146,8 +148,13 @@ class ApiService {
} catch (error) {
console.log("Error details: ", error);
console.log("axios.isAxiosError(error): ", axios.isAxiosError(error));
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
if (err.status === 401) {
return 'Email ou mot de passe incorrect.';
}
const data = err.response?.data as { error: string } | undefined;
return data?.error || 'Erreur serveur inconnue lors de la requête.';
}
@ -157,10 +164,10 @@ class ApiService {
}
/**
* @returns true if successful
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async resetPassword(email: string): Promise<any> {
public async resetPassword(email: string): Promise<ApiResponse> {
try {
if (!email) {
@ -196,7 +203,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async changePassword(email: string, oldPassword: string, newPassword: string): Promise<any> {
public async changePassword(email: string, oldPassword: string, newPassword: string): Promise<ApiResponse> {
try {
if (!email || !oldPassword || !newPassword) {
@ -232,7 +239,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async deleteUser(email: string, password: string): Promise<any> {
public async deleteUser(email: string, password: string): Promise<ApiResponse> {
try {
if (!email || !password) {
@ -270,7 +277,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async createFolder(title: string): Promise<any> {
public async createFolder(title: string): Promise<ApiResponse> {
try {
if (!title) {
@ -375,7 +382,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async deleteFolder(folderId: string): Promise<any> {
public async deleteFolder(folderId: string): Promise<ApiResponse> {
try {
if (!folderId) {
@ -410,7 +417,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async renameFolder(folderId: string, newTitle: string): Promise<any> {
public async renameFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
try {
if (!folderId || !newTitle) {
@ -441,7 +448,7 @@ class ApiService {
}
}
public async duplicateFolder(folderId: string): Promise<any> {
public async duplicateFolder(folderId: string): Promise<ApiResponse> {
try {
if (!folderId) {
throw new Error(`Le folderId et le nouveau titre sont requis.`);
@ -473,7 +480,7 @@ class ApiService {
}
}
public async copyFolder(folderId: string, newTitle: string): Promise<any> {
public async copyFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
try {
if (!folderId || !newTitle) {
throw new Error(`Le folderId et le nouveau titre sont requis.`);
@ -510,7 +517,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async createQuiz(title: string, content: string[], folderId: string): Promise<any> {
public async createQuiz(title: string, content: string[], folderId: string): Promise<ApiResponse> {
try {
if (!title || !content || !folderId) {
@ -581,7 +588,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async deleteQuiz(quizId: string): Promise<any> {
public async deleteQuiz(quizId: string): Promise<ApiResponse> {
try {
if (!quizId) {
@ -616,7 +623,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async updateQuiz(quizId: string, newTitle: string, newContent: string[]): Promise<any> {
public async updateQuiz(quizId: string, newTitle: string, newContent: string[]): Promise<ApiResponse> {
try {
if (!quizId || !newTitle || !newContent) {
@ -652,7 +659,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async moveQuiz(quizId: string, newFolderId: string): Promise<any> {
public async moveQuiz(quizId: string, newFolderId: string): Promise<ApiResponse> {
try {
if (!quizId || !newFolderId) {
@ -689,7 +696,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async duplicateQuiz(quizId: string): Promise<any> {
public async duplicateQuiz(quizId: string): Promise<ApiResponse> {
const url: string = this.constructRequestUrl(`/quiz/duplicate`);
@ -703,7 +710,7 @@ class ApiService {
throw new Error(`La duplication du quiz a échoué. Status: ${result.status}`);
}
return result;
return result.status === 200;
} catch (error) {
console.error("Error details: ", error);
@ -723,9 +730,9 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async copyQuiz(quizId: string, newTitle: string, folderId: string): Promise<any> {
public async copyQuiz(quizId: string, newTitle: string, folderId: string): Promise<ApiResponse> {
try {
console.log(quizId, newTitle), folderId;
console.log(quizId, newTitle, folderId);
return "Route not implemented yet!";
} catch (error) {
@ -741,7 +748,7 @@ class ApiService {
}
}
async ShareQuiz(quizId: string, email: string): Promise<any> {
async ShareQuiz(quizId: string, email: string): Promise<ApiResponse> {
try {
if (!quizId || !email) {
throw new Error(`quizId and email are required.`);
@ -800,7 +807,7 @@ class ApiService {
}
}
async receiveSharedQuiz(quizId: string, folderId: string): Promise<any> {
async receiveSharedQuiz(quizId: string, folderId: string): Promise<ApiResponse> {
try {
if (!quizId || !folderId) {
throw new Error(`quizId and folderId are required.`);
@ -869,7 +876,8 @@ class ApiService {
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
const data = err.response?.data as { error: string } | undefined;
return `ERROR : ${data?.error}` || 'ERROR : Erreur serveur inconnue lors de la requête.';
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
return `ERROR : ${msg}`;
}
return `ERROR : Une erreur inattendue s'est produite.`

View file

@ -21,14 +21,23 @@ class WebSocketService {
private socket: Socket | null = null;
connect(backendUrl: string): Socket {
// console.log(backendUrl);
this.socket = io(`${backendUrl}`, {
console.log(`WebSocketService.connect('${backendUrl}')`);
// // Ensure the URL uses wss: if the URL starts with https:
// const protocol = backendUrl.startsWith('https:') ? 'wss:' : 'ws:';
// console.log(`WebSocketService.connect: protocol=${protocol}`);
// const url = backendUrl.replace(/^http(s):/, protocol);
// console.log(`WebSocketService.connect: changed url=${url}`);
const url = backendUrl || window.location.host;
this.socket = io(url, {
transports: ['websocket'],
reconnectionAttempts: 1
});
return this.socket;
}
disconnect() {
if (this.socket) {
@ -74,15 +83,15 @@ class WebSocketService {
// idQuestion: string
) {
if (this.socket) {
this.socket?.emit('submit-answer',
// {
// answer: answer,
// roomName: roomName,
// username: username,
// idQuestion: idQuestion
// }
answerData
);
this.socket?.emit('submit-answer',
// {
// answer: answer,
// roomName: roomName,
// username: username,
// idQuestion: idQuestion
// }
answerData
);
}
}
}

View file

@ -1,4 +1,4 @@
export function escapeForGIFT(link: string): string {
const specialChars = /[{}#~=<>\:]/g;
const specialChars = /[{}#~=<>\\:]/g;
return link.replace(specialChars, (match) => `\\${match}`);
}

View file

@ -1,9 +1,13 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"src/*": ["src/*"]
},
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ES2020",
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
@ -12,7 +16,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"jsx": "react",
/* Linting */
"strict": true,

View file

@ -3,22 +3,44 @@ import react from '@vitejs/plugin-react-swc';
import pluginChecker from 'vite-plugin-checker';
import EnvironmentPlugin from 'vite-plugin-environment';
// Filter out environment variables with invalid identifiers
const filteredEnv = Object.keys(process.env).reduce((acc, key) => {
// Only include environment variables with valid JavaScript identifiers
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
acc[key] = process.env[key];
}
return acc;
}, {});
// https://vitejs.dev/config/
export default defineConfig({
base: "/",
plugins: [
react(),
pluginChecker({ typescript: true }),
EnvironmentPlugin('all'),
EnvironmentPlugin(filteredEnv),
],
resolve: {
alias: {
'src': '/src'
}
},
preview: {
port: 5173,
strictPort: true
},
server: {
port: 5173,
strictPort: true,
host: true,
origin: "http://0.0.0.0:5173",
port: 5173,
strictPort: true,
host: true,
origin: "http://0.0.0.0:5173",
},
build: {
sourcemap: true, // Enable source maps
rollupOptions: {
output: {
sourcemapExcludeSources: true, // Exclude sources from source maps
},
},
},
});

View file

@ -1,10 +1,13 @@
version: '3'
services:
services:
frontend:
image: fuhrmanator/evaluetonsavoir-frontend:latest
container_name: frontend
environment:
# Define empty VITE_BACKEND_URL because it's production
- VITE_BACKEND_URL=
# Define empty VITE_BACKEND_SOCKET_URL so it will default to window.location.host
- VITE_BACKEND_SOCKET_URL=
ports:
- "5173:5173"
restart: always
@ -49,7 +52,7 @@ services:
- mongodb_data:/data/db
restart: always
# Ce conteneur assure que l'application est à jour en allant chercher s'il y a des mises à jours à chaque heure
# Ce conteneur cherche des mises à jour à 5h du matin
watchtower:
image: containrrr/watchtower
container_name: watchtower
@ -63,6 +66,19 @@ services:
- WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday
restart: always
watchtower-once:
image: containrrr/watchtower
container_name: watchtower-once
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --run-once
environment:
- TZ=America/Montreal
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_DEBUG=true
- WATCHTOWER_INCLUDE_RESTARTING=true
restart: "no"
volumes:
mongodb_data:
external: false

10
export-collections.bash Normal file
View file

@ -0,0 +1,10 @@
#!/bin/bash
DB_NAME="evaluetonsavoir"
OUTPUT_DIR="/data/db"
collections=$(mongosh $DB_NAME --quiet --eval "db.getCollectionNames().join(' ')")
for collection in $collections; do
mongoexport --db=$DB_NAME --collection=$collection --out=$OUTPUT_DIR/$collection.json --jsonArray
done

28
package-lock.json generated Normal file
View file

@ -0,0 +1,28 @@
{
"name": "EvalueTonSavoir",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"dompurify": "^3.2.3"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/dompurify": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
"integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
}
}
}

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"dependencies": {
"dompurify": "^3.2.3"
}
}

View file

@ -1,4 +1,3 @@
const { create } = require('../middleware/jwtToken');
const Folders = require('../models/folders');
const ObjectId = require('mongodb').ObjectId;
const Quizzes = require('../models/quiz');

View file

@ -1,3 +1,5 @@
/* eslint-disable */
// const request = require('supertest');
// const app = require('../app.js');
// // const app = require('../routers/images.js');

View file

@ -2,7 +2,6 @@ const Users = require('../models/users');
const bcrypt = require('bcrypt');
const Quizzes = require('../models/quiz');
const Folders = require('../models/folders');
const AppError = require('../middleware/AppError');
const { ObjectId } = require('mongodb');
jest.mock('bcrypt');

View file

@ -43,6 +43,7 @@ const imagesRouter = require('./routers/images.js');
// Setup environment
dotenv.config();
const isDev = process.env.NODE_ENV === 'development';
const errorHandler = require("./middleware/errorHandler.js");
// Start app
@ -51,6 +52,7 @@ const cors = require("cors");
const bodyParser = require('body-parser');
const configureServer = (httpServer, isDev) => {
console.log(`Configuring server with isDev: ${isDev}`);
return new Server(httpServer, {
path: "/socket.io",
cors: {
@ -63,14 +65,16 @@ const configureServer = (httpServer, isDev) => {
};
// Start sockets (depending on the dev or prod environment)
let server = http.createServer(app);
let isDev = process.env.NODE_ENV === 'development';
const server = http.createServer(app);
console.log(`Environnement: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`);
const io = configureServer(server);
const io = configureServer(server, isDev);
console.log(`Server configured with cors.origin: ${io.opts.cors.origin} and secure: ${io.opts.secure}`);
setupWebsocket(io);
console.log(`Websocket setup with on() listeners.`);
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

View file

@ -18,7 +18,7 @@ exports.USER_ALREADY_EXISTS = {
}
exports.LOGIN_CREDENTIALS_ERROR = {
message: 'L\'email et le mot de passe ne correspondent pas.',
code: 400
code: 401
}
exports.GENERATE_PASSWORD_ERROR = {
message: 'Une erreur s\'est produite lors de la création d\'un nouveau mot de passe.',
@ -130,4 +130,4 @@ exports.NOT_IMPLEMENTED = {
// static badRequest(res, message) {400
// static unauthorized(res, message) {401
// static notFound(res, message) {404
// static serverError(res, message) {505
// static serverError(res, message) {505

View file

@ -1,6 +1,6 @@
//controller
const AppError = require('../middleware/AppError.js');
const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, FOLDER_NOT_FOUND, FOLDER_ALREADY_EXISTS, GETTING_FOLDER_ERROR, DELETE_FOLDER_ERROR, UPDATE_FOLDER_ERROR, MOVING_FOLDER_ERROR, DUPLICATE_FOLDER_ERROR, COPY_FOLDER_ERROR } = require('../constants/errorCodes');
const { MISSING_REQUIRED_PARAMETER, FOLDER_NOT_FOUND, FOLDER_ALREADY_EXISTS, GETTING_FOLDER_ERROR, DELETE_FOLDER_ERROR, UPDATE_FOLDER_ERROR, DUPLICATE_FOLDER_ERROR, COPY_FOLDER_ERROR } = require('../constants/errorCodes');
// controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express
class FoldersController {

View file

@ -1,13 +1,13 @@
const emailer = require('../config/email.js');
const AppError = require('../middleware/AppError.js');
const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, QUIZ_NOT_FOUND, FOLDER_NOT_FOUND, QUIZ_ALREADY_EXISTS, GETTING_QUIZ_ERROR, DELETE_QUIZ_ERROR, UPDATE_QUIZ_ERROR, MOVING_QUIZ_ERROR, DUPLICATE_QUIZ_ERROR, COPY_QUIZ_ERROR } = require('../constants/errorCodes');
const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, QUIZ_NOT_FOUND, FOLDER_NOT_FOUND, QUIZ_ALREADY_EXISTS, GETTING_QUIZ_ERROR, DELETE_QUIZ_ERROR, UPDATE_QUIZ_ERROR, MOVING_QUIZ_ERROR } = require('../constants/errorCodes');
class QuizController {
constructor(quizModel, foldersModel) {
this.folders = foldersModel;
this.quizzes = quizModel;
this.folders = foldersModel;
}
create = async (req, res, next) => {
@ -165,7 +165,7 @@ class QuizController {
}
};
copy = async (req, res, next) => {
copy = async (req, _res, _next) => {
const { quizId, newTitle, folderId } = req.body;
if (!quizId || !newTitle || !folderId) {
@ -207,7 +207,7 @@ class QuizController {
}
// Call the method from the Quiz model to delete quizzes by folder ID
await Quiz.deleteQuizzesByFolderId(folderId);
await this.quizzes.deleteQuizzesByFolderId(folderId);
return res.status(200).json({
message: 'Quizzes deleted successfully.'
@ -232,7 +232,7 @@ class QuizController {
try {
const existingFile = await this.quizzes.quizExists(title, userId);
return existingFile !== null;
} catch (error) {
} catch (_error) {
throw new AppError(GETTING_QUIZ_ERROR);
}
};

32
server/eslint.config.mjs Normal file
View file

@ -0,0 +1,32 @@
import globals from "globals";
import pluginJs from "@eslint/js";
/** @type {import('eslint').Linter.Config[]} */
export default [
{
files: ["**/*.js"],
languageOptions: {
sourceType: "commonjs",
globals: {
...globals.node,
...globals.jest, // Add Jest globals
},
},
rules: {
"no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrors": "all", // Ignore all catch clause parameters
"caughtErrorsIgnorePattern": "^_" // Ignore catch clause parameters that start with _
}],
},
},
{
languageOptions: {
globals: {
...globals.browser,
},
},
},
pluginJs.configs.recommended,
];

View file

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

View file

@ -1,4 +1,3 @@
//const db = require('../config/db.js')
const { ObjectId } = require('mongodb');
class Images {
@ -8,8 +7,8 @@ class Images {
}
async upload(file, userId) {
await db.connect()
const conn = db.getConnection();
await this.db.connect()
const conn = this.db.getConnection();
const imagesCollection = conn.collection('images');
@ -27,8 +26,8 @@ class Images {
}
async get(id) {
await db.connect()
const conn = db.getConnection();
await this.db.connect()
const conn = this.db.getConnection();
const imagesCollection = conn.collection('images');

871
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,10 @@
"socket.io-client": "^4.7.2"
},
"devDependencies": {
"@eslint/js": "^9.18.0",
"cross-env": "^7.0.3",
"eslint": "^9.18.0",
"globals": "^15.14.0",
"jest": "^29.7.0",
"jest-mock": "^29.7.0",
"nodemon": "^3.0.1",

Some files were not shown because too many files have changed in this diff Show more