Compare commits

..

77 commits

Author SHA1 Message Date
27efcda1d8 Adds azure diagram 2024-12-10 15:42:42 -05:00
d91cb0e245 set theme to red for ETS 2024-12-10 15:27:11 -05:00
MathieuSevignyLavallee
04ccca91d6 ajustement 2024-12-10 13:32:18 -05:00
dd0f5f9534 fix dns issue stress-test 2024-12-09 22:17:27 -05:00
a2d83f4f77 Merge branch 'dev-it3-PFEA2024' into dev-it3-it4-PFEA2024 2024-12-09 21:37:29 -05:00
b97454d9a9 Adds logs back in docker 2024-12-09 21:34:23 -05:00
fserres
75e669b8b4 Add deployment with opentofu 2024-12-09 14:58:48 -05:00
fserres
3ef37f6dc4 Ajout de la doc opentofu 2024-12-09 12:55:13 -05:00
MathieuSevignyLavallee
b60961acea ajout documentation test de charge 2024-12-08 22:24:15 -05:00
MathieuSevignyLavallee
e7eede36be Merge branch 'stress-test-socket' into dev-it3-it4-PFEA2024 2024-12-08 21:45:41 -05:00
MathieuSevignyLavallee
81186b6a35 move test_metric to class and rename file 2024-12-08 15:00:55 -05:00
MathieuSevignyLavallee
fff5830afd Add README, implementation de l'api Docker sur le quizroom, Fonctionnel
remove docker group

graph generator remake

Docker API implementation
2024-12-07 22:01:55 -05:00
63cdd03c14 Ajout de la configuration nginx + quizroom + healtchecks 2024-12-07 17:02:34 -05:00
dabdfafd35 fix quizroom docker checks 2024-12-07 16:40:53 -05:00
32a41d93aa adds branch creation script 2024-12-07 16:00:14 -05:00
MathieuSevignyLavallee
bb4ef54db9 new approach using cgroup 2024-12-07 15:41:21 -05:00
b3d65e0a1e force lf ending of script 2024-12-07 15:05:24 -05:00
567a765f94 fixed nginx 2024-12-07 14:58:15 -05:00
5c75347887 Adds basic health checks 2024-12-06 21:01:23 -05:00
8eab2d3a05 Adds image validation + download 2024-12-06 19:40:45 -05:00
MathieuSevignyLavallee
1a7be0ad79 ajout de metrique de sommaire 2024-12-06 19:31:48 -05:00
MathieuSevignyLavallee
1e67762b5e cleanup 2024-12-06 19:12:29 -05:00
d3199b9f3f adds env for network name 2024-12-06 18:35:32 -05:00
d9e5d6a91f Merge branch 'stress-test-socket' of github.com:ets-cfuhrman-pfe/EvalueTonSavoir into stress-test-socket 2024-12-06 18:19:25 -05:00
d4e13b8c36 template to add env variables 2024-12-06 18:19:23 -05:00
MathieuSevignyLavallee
18d3ded4fa Test de charge et graph
This reverts commit 6d988c347f.

typo gitignore

Create test.txt

gitignore

pas terminer a besoin de pofinage
2024-12-06 14:23:19 -05:00
MathieuSevignyLavallee
3744bf4347 typo gitignore 2024-12-06 11:26:35 -05:00
MathieuSevignyLavallee
ec0cc48ae7 Create test.txt 2024-12-06 11:25:15 -05:00
MathieuSevignyLavallee
20fe4e673a Revert "gitignore"
This reverts commit 6d988c347f.
2024-12-06 11:24:57 -05:00
MathieuSevignyLavallee
6d988c347f gitignore 2024-12-06 11:21:10 -05:00
MathieuSevignyLavallee
80610c3a6e pas terminer a besoin de pofinage 2024-12-05 20:24:56 -05:00
Jerry Kwok
58a55ed176 ansible documentation 2024-11-30 16:36:56 -05:00
MathieuSevignyLavallee
5a3f965c58 Update main.js 2024-11-28 17:56:56 -05:00
MathieuSevignyLavallee
ec15909d55 working 2024-11-28 15:09:22 -05:00
71353669ca adds env to main 2024-11-27 21:09:24 -05:00
15144244ad Adds docker tests 2024-11-27 21:07:21 -05:00
MathieuSevignyLavallee
0af9b099fd nginx conf 2024-11-27 21:00:52 -05:00
4f72dc7b9b clean diagram 2024-11-27 19:24:41 -05:00
MathieuSevignyLavallee
49fbdb1ffd refactor 2024-11-27 18:36:59 -05:00
fa95b9003f fixes plantuml url + adds back deployment diagram 2024-11-27 15:52:32 -05:00
2176edf7d0 Adds documentation to IT3 - less branches 2024-11-27 14:47:54 -05:00
6883774ed4
Merge pull request #168 from ets-cfuhrman-pfe/dev-it3-cache
Adds cache to nginx
2024-11-26 22:52:10 -05:00
MathieuSevignyLavallee
11222c70bd not finished 2024-11-26 17:04:22 -05:00
c45a674c72 Adds cache to nginx 2024-11-25 20:40:48 -05:00
MathieuSevignyLavallee
5c24ae56a9 write to file base 2024-11-15 20:09:56 -05:00
MathieuSevignyLavallee
f835c733a1 metrique de base 2024-11-15 19:57:55 -05:00
MathieuSevignyLavallee
5c21b6a15f optimize and cleanup 2024-11-15 19:35:41 -05:00
MathieuSevignyLavallee
b608793ac3 base
no library for socket.io
2024-11-15 17:46:01 -05:00
878fd302a4
Merge pull request #167 from ets-cfuhrman-pfe/ansible-it3-PFEA2024
Ansible it3 pfea2024
2024-11-14 14:22:21 -05:00
MathieuSevignyLavallee
0b2552bdff DNS name for proxying 2024-11-12 12:06:26 -05:00
706308d54f
Merge pull request #164 from ets-cfuhrman-pfe/socket-image
QuizRoom separation and duplication
2024-11-12 12:01:58 -05:00
MathieuSevignyLavallee
977d1c9700 added jwt token to room routes 2024-11-12 11:44:15 -05:00
35d6724d87 Removes needs of port forwarding + francisation 2024-11-12 02:33:19 -05:00
806935e48c Adds container cleaning - doesn't clean populated container on reboot 2024-11-12 01:22:54 -05:00
3c2bcb4ed4 Adds healthcheck to quiz + adds image to build 2024-11-11 23:00:38 -05:00
MathieuSevignyLavallee
2c7fd9c828 Delete room on end-quiz 2024-11-11 17:45:30 -05:00
MathieuSevignyLavallee
db6fa947d7 Working
Quand le professeur créer le room il y a un delai avec aucune info puis ca afficher la classe donc petit bug
2024-11-11 15:46:02 -05:00
MathieuSevignyLavallee
d37e6c540a Redirecting not working 2024-11-11 15:16:59 -05:00
MathieuSevignyLavallee
b744284472 basic functions for room Creation using docker 2024-11-11 11:32:46 -05:00
MathieuSevignyLavallee
cca9a2c99a provider create docker quizRoom 2024-11-10 23:41:03 -05:00
2df750b6f7
Merge pull request #163 from ets-cfuhrman-pfe/nginx-port-forward
Nginx port forward
2024-11-10 22:54:20 -05:00
d563459aa6 Was able to show a VERY guided demo 2024-11-10 22:52:04 -05:00
678d1c2250 trying to setup dynamic nginx 2024-11-10 20:42:02 -05:00
80115f050c setup routing 2024-11-10 16:33:45 -05:00
c26708a609 semi-stable-state - non-working
Co-authored-by: MathieuSevignyLavallee <MathieuSevignyLavallee@users.noreply.github.com>
2024-11-10 15:42:46 -05:00
MathieuSevignyLavallee
bbc0359ead add to the compose file 2024-11-07 12:39:36 -05:00
Jerry Kwok
e08d1477ec documentation for ansible deployment 2024-11-06 09:07:15 -05:00
MathieuSevignyLavallee
85bd93792c base image for quizRoom socket 2024-11-05 16:37:07 -05:00
Jerry Kwok
3fd562c144 fix deploy.yml 2024-11-05 15:58:26 -05:00
f2597f5491
Update deploy.yml
Adds dependencies validation
2024-11-05 15:12:39 -05:00
4cca066751 adds valkey default config 2024-11-03 16:36:21 -05:00
Jerry Kwok
cc420b3a9c fix deploy.yml 2024-10-31 15:33:16 -04:00
Jerry Kwok
ff7f0da964 update deploy.yml to run docker-compose 2024-10-31 15:31:12 -04:00
Jerry Kwok
900ccd847f update deploy.yml file to test deployment with WSL 2024-10-31 10:49:06 -04:00
Jerry Kwok
3db53c5cc4 docker file for ansible 2024-10-30 10:45:11 -04:00
Jerry Kwok
93e16f8d0b testing ansible files 2024-10-30 10:42:21 -04:00
32bcb67f33 Adds base for multi-room
Co-authored-by: roesnerb <roesnerb@users.noreply.github.com>
Co-authored-by: MathieuSevignyLavallee <MathieuSevignyLavallee@users.noreply.github.com>
2024-10-29 16:47:10 -04:00
191 changed files with 13541 additions and 9315 deletions

View file

@ -104,3 +104,34 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-quizroom:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Quizroom Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}-quizroom
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
- name: Build and push Quizroom Docker image
uses: docker/build-push-action@v5
with:
context: ./quizRoom
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

33
.github/workflows/create-docs.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Creates docs and deploy to gh-pages
on:
workflow_call:
workflow_dispatch:
push:
branches: [ main ]
jobs:
build:
name: Deploy docs
runs-on: ubuntu-latest
env:
PUMLURL: "https://www.plantuml.com/plantuml/"
steps:
- name: Checkout main
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v5
- name: Install dependencies
working-directory: ./documentation
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build docs
working-directory: ./documentation
run: mkdocs build --verbose --clean
- name: Push docs to gh-pages
working-directory: ./documentation
run: python deploy.py

View file

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

13
.gitignore vendored
View file

@ -129,3 +129,16 @@ dist
.yarn/install-state.gz
.pnp.*
db-backup/
**/.env
.venv
deployments
/test/stressTest/output
# Opentofu state
opentofu/*/.terraform
opentofu/*/.terraform.lock*
opentofu/*/terraform.tfstate*
opentofu/*/terraform.tfvars
# Opentofu auth config
opentofu/auth_config.json

39
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,39 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug backend",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/server/app.js",
"cwd":"${workspaceFolder}/server/"
},
{
"type": "msedge",
"request": "launch",
"name": "Debug frontend",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/client/"
},
{
"name": "Docker: Attach to Node",
"type": "node",
"request": "attach",
"restart": true,
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app",
"protocol": "inspector",
"skipFiles": [
"<node_internals>/**"
]
}
]
}

View file

@ -1,7 +1,6 @@
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

10
ansible/Dockerfile Normal file
View file

@ -0,0 +1,10 @@
FROM python:3.9-slim
# Installer Ansible
RUN pip install ansible
# Définir le répertoire de travail
WORKDIR /ansible
# Copier les fichiers nécessaires
COPY inventory.ini deploy.yml ./

40
ansible/README.md Normal file
View file

@ -0,0 +1,40 @@
# Déploiement de Services avec Ansible et Docker Compose
Ce guide explique comment utiliser Ansible pour configurer et déployer des services Docker avec `docker-compose`.
## Prérequis
1. **Ansible** : Assurez-vous qu'Ansible est installé sur votre système.
- [Guide d'installation d'Ansible](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html)
2. **Docker et Docker Compose** : Docker doit être installé et configuré pour fonctionner avec Ansible.
- Installez Docker : [Documentation Docker](https://docs.docker.com/get-docker/)
- Docker Compose est inclus comme plugin Docker dans les versions récentes de Docker.
3. **WSL (pour Windows)** : Si vous utilisez Windows, assurez-vous d'avoir configuré WSL et un environnement Ubuntu.
## Structure du projet
Le fichier `deploy.yml` contient les tâches Ansible nécessaires pour télécharger, configurer, et démarrer les services Docker en utilisant Docker Compose.
## Installation et de déploiement
### Lancer le déploiement avec Ansible
Pour exécuter le playbook Ansible `deploy.yml`, utilisez la commande suivante depuis le répertoire racine du projet :
`ansible-playbook -i inventory.ini deploy.yml`
### Vérification du déploiement
Une fois le playbook exécuté, Ansible télécharge Docker et Docker Compose, télécharge le fichier `docker-compose.yaml`, démarre Docker et lance les conteneurs spécifiés.
### Configuration et contenu du Playbook (deploy.yml)
Le playbook deploy.yml exécute les étapes suivantes :
1. Télécharge Docker Compose si ce dernier n'est pas encore présent.
2. Vérifie l'installation de Docker Compose pour s'assurer qu'il est opérationnel.
3. Démarre le service Docker si ce n'est pas déjà le cas.
4. Télécharge le fichier docker-compose.yaml depuis le dépôt Git spécifié.
5. Lance Docker Compose pour déployer les conteneurs définis dans docker-compose.yaml.
6. Vérifie l'état des conteneurs et affiche les conteneurs en cours d'exécution.

38
ansible/deploy.yml Normal file
View file

@ -0,0 +1,38 @@
---
- name: Déployer des services avec Docker Compose
hosts: local
tasks:
- name: Télécharger Docker
ansible.builtin.package:
name: docker-compose
state: present
- name: Vérifier l'installation de Docker Compose plugin
ansible.builtin.command:
cmd: docker compose version
- name: Commencer le service docker
ansible.builtin.service:
name: docker
state: started
enabled: yes
- name: Telecharger le fichier docker-compose
ansible.builtin.get_url:
url: https://raw.githubusercontent.com/ets-cfuhrman-pfe/EvalueTonSavoir/refs/heads/main/docker-compose.yaml
dest: ./docker-compose.yaml
- name: Lancer Docker Compose
ansible.builtin.shell:
docker-compose up -d
become: true
- name: Vérification des services Docker
ansible.builtin.command:
cmd: docker ps
register: docker_ps_output
- name: Afficher l'état des conteneurs Docker
ansible.builtin.debug:
msg: "{{ docker_ps_output.stdout }}"

View file

@ -0,0 +1,70 @@
services:
frontend:
image: fuhrmanator/evaluetonsavoir-frontend:latest
container_name: frontend
ports:
- "5173:5173"
environment:
VITE_BACKEND_URL: "http://localhost:4400"
# don't define VITE_BACKEND_SOCKET_URL so it will default to window.location.host
# VITE_BACKEND_SOCKET_URL: ""
restart: always
backend:
image: fuhrmanator/evaluetonsavoir-backend:latest
container_name: backend
ports:
- "3000:3000"
environment:
PORT: 3000
MONGO_URI: "mongodb://mongo:27017/evaluetonsavoir"
MONGO_DATABASE: evaluetonsavoir
EMAIL_SERVICE: gmail
SENDER_EMAIL: infoevaluetonsavoir@gmail.com
EMAIL_PSW: 'vvml wmfr dkzb vjzb'
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
FRONTEND_URL: "http://localhost:5173"
depends_on:
- mongo
restart: always
# Ce conteneur sert de routeur pour assurer le bon fonctionnement de l'application
nginx:
image: fuhrmanator/evaluetonsavoir-routeur:latest
container_name: nginx
ports:
- "80:80"
depends_on:
- backend
- frontend
restart: always
# Ce conteneur est la base de données principale pour l'application
mongo:
image: mongo
container_name: mongo
ports:
- "27017:27017"
tty: true
volumes:
- 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
watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- TZ=America/Montreal
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_DEBUG=true
- WATCHTOWER_INCLUDE_RESTARTING=true
- WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday
restart: always
volumes:
mongodb_data:
external: false

9
ansible/inventory.ini Normal file
View file

@ -0,0 +1,9 @@
# Spécifier les serveurs où vous souhaitez déployer votre application.
# Remplacez votre_ip_serveur par ladresse IP de votre serveur, et votre_utilisateur_ssh par le nom dutilisateur SSH.
# Pour les serveurs
# [app_servers]
# votre_ip_serveur ansible_user=votre_utilisateur_ssh
[local]
localhost ansible_connection=local ansible_python_interpreter=/usr/bin/python3

View file

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

View file

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

View file

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

View file

@ -12,6 +12,10 @@ RUN npm install
RUN npm run build
EXPOSE 5173
ENV PORT=5173
EXPOSE ${PORT}
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:${PORT} || exit 1
CMD [ "npm", "run", "preview" ]

View file

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

View file

@ -1,29 +0,0 @@
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,4 +1,3 @@
/* eslint-disable no-undef */
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
@ -12,11 +11,7 @@ module.exports = {
//moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFiles: ['./jest.setup.cjs'],
moduleNameMapper: {
'\\.(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',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
},
transformIgnorePatterns: ['node_modules/(?!nanoid/)'],
};

View file

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

7451
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,7 @@
"@mui/material": "^6.1.0",
"@types/uuid": "^9.0.7",
"axios": "^1.6.7",
"dompurify": "^3.2.3",
"dockerode": "^4.0.2",
"esbuild": "^0.23.1",
"gift-pegjs": "^1.0.2",
"jest-environment-jsdom": "^29.7.0",
@ -44,7 +44,6 @@
"@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",
@ -56,16 +55,13 @@
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"eslint": "^9.18.0",
"eslint-plugin-react": "^7.37.3",
"eslint": "^9.10.0",
"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,4 +1,3 @@
import React from 'react';
// App.tsx
import { Routes, Route } from 'react-router-dom';

View file

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

View file

@ -1,13 +0,0 @@
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,7 +9,6 @@ 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'),
@ -25,7 +24,6 @@ describe('isQuizValid function', () => {
const invalidQuiz: QuizType = {
_id: '2',
folderId: 'test',
folderName: 'test',
userId: 'user',
title: '',
created_at: new Date('2021-10-01'),
@ -41,7 +39,6 @@ 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,8 +1,7 @@
// Modal.test.tsx
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import ConfirmDialog from 'src/components/ConfirmDialog/ConfirmDialog';
import ConfirmDialog from '../../../components/ConfirmDialog/ConfirmDialog';
describe('ConfirmDialog Component', () => {
const mockOnConfirm = jest.fn();

View file

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

View file

@ -1,7 +1,6 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
import GIFTTemplatePreview from '../../../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 "src/components/GiftTemplate/templates/TextType";
import textType from "../../../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,17 +42,19 @@ 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 = {
// 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\}`,
text: `Donnez le déterminant de la matrice suivante.$$\\begin\{pmatrix\}
a&b \\\\
c&d
\\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 "src/components/GiftTemplate/constants";
import { colors } from "../../../../components/GiftTemplate/constants";
describe('Colors object', () => {
test('All colors are defined', () => {

View file

@ -1,9 +1,8 @@
//styles.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ParagraphStyle } from 'src/components/GiftTemplate/constants';
import { ParagraphStyle } from '../../../../components/GiftTemplate/constants';
describe('ParagraphStyle', () => {
test('applies styles correctly', () => {
@ -28,7 +27,6 @@ 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 'src/components/GiftTemplate/constants/theme';
import { colors } from 'src/components/GiftTemplate/constants/colors';
import { theme } from '../../../../components/GiftTemplate/constants/theme';
import { colors } from '../../../../components/GiftTemplate/constants/colors';
describe('Theme', () => {
test('returns correct light color', () => {

View file

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

View file

@ -1,133 +0,0 @@
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

@ -1,79 +0,0 @@
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

@ -1,80 +0,0 @@
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

@ -1,74 +0,0 @@
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

@ -1,301 +0,0 @@
// 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

@ -1,304 +0,0 @@
// 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

@ -1,438 +0,0 @@
// 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,7 +1,6 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import DragAndDrop from 'src/components/ImportModal/ImportModal';
import DragAndDrop from '../../../components/ImportModal/ImportModal';
describe('DragAndDrop Component', () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,7 @@
// 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 'src/components/StudentWaitPage/StudentWaitPage';
import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage';
import { StudentType, Answer } from '../../../Types/StudentType';
describe('StudentWaitPage Component', () => {

View file

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

View file

@ -1,10 +1,9 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { parse } from 'gift-pegjs';
import { MemoryRouter } from 'react-router-dom';
import { QuestionType } from '../../../../Types/QuestionType';
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
import StudentModeQuiz from '../../../../components/StudentModeQuiz/StudentModeQuiz';
const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
@ -77,5 +76,21 @@ 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,11 +1,10 @@
//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 'src/components/TeacherModeQuiz/TeacherModeQuiz';
import TeacherModeQuiz from '../../../../components/TeacherModeQuiz/TeacherModeQuiz';
import { MemoryRouter } from 'react-router-dom';
// import { mock } from 'node:test';

View file

@ -1,4 +1,3 @@
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,4 +1,3 @@
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', 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') },
{ 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') },
];
beforeEach(() => {

View file

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

View file

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

View file

@ -1,16 +1,20 @@
import * as React from 'react';
import './footer.css';
type FooterProps = object; //empty object
interface FooterProps {
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&apos;ETS
Réalisé avec à Montréal par des finissantes de l'ETS
</div>
<div className="footer-links">
<a href="https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/">GitHub</a>
<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>
<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} \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}";
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";
return (
<div className="gift-cheat-sheet">
<h2 className="subtitle">Informations pratiques sur l&apos;éditeur</h2>
<h2 className="subtitle">Informations pratiques sur l'éditeur</h2>
<span>
L&apos;éditeur utilise le format GIFT (General Import Format Template) créé pour la
L'é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&apos;équation:</p>
<p>Exemple d'é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&apos;une question Vrai/Faux avec l&apos;image d&apos;un chat:</p>
<p>Exemple d'une question Vrai/Faux avec l'image d'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&apos;URL de l&apos;image.</p>
<p>Note&nbsp;: les images étant spécifiées avec la syntaxe Markdown dans GIFT, on doit échapper les caractères spéciales (:) dans l'URL de l'image.</p>
<p>Note&nbsp;: On ne peut utiliser les images dans les messages de rétroaction (GIFT), car les rétroactions ne supportent pas le texte avec formatage (Markdown).</p>
<p style={{ color: 'red' }}>
Attention: l&apos;ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n&apos;est plus
Attention: l'ancienne fonctionnalité avec les balises <code>{'<img>'}</code> n'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&apos;autres formats de questions que nous ne gérons pas sur cette
GIFT supporte d'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,7 +3,6 @@ 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[];
@ -74,7 +73,7 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
<div className="error">{error}</div>
) : isPreviewReady ? (
<div data-testid="preview-container">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(items) }}></div>
<div dangerouslySetInnerHTML={{ __html: 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':
parsedText = marked.parse(formatText, { breaks: true }) as string; // https://github.com/markedjs/marked/discussions/3219
const 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&apos;explorateur des fichiers
cliquez pour ouvrir l'explorateur des fichiers
</DialogContentText>
</div>
<Download color="primary" />

View file

@ -1,4 +1,3 @@
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&apos;utilisateur</div>
<div className="text-base text-bold">Nom d'utilisateur</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell

View file

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

View file

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

View file

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

View file

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

View file

@ -4,14 +4,12 @@ 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 {
@ -29,15 +27,13 @@ const ShortAnswerQuestion: React.FC<Props> = (props) => {
return (
<div className="question-wrapper">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
<div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} />
</div>
{showAnswer ? (
<>
<div className="correct-answer-text mb-1">
{choices.map((choice) => (
<div key={choice.id} className="mb-1">
{choice.text.text}
</div>
<div className="mb-1">{choice.text.text}</div>
))}
</div>
{globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}

View file

@ -4,7 +4,6 @@ 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;
@ -28,7 +27,7 @@ const TrueFalseQuestion: React.FC<Props> = (props) => {
return (
<div className="question-container">
<div className="question content">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({ text: questionContent })) }} />
<div dangerouslySetInnerHTML={{ __html: textType({ text: questionContent }) }} />
</div>
<div className="choices-wrapper mb-1">
<Button

View file

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

View file

@ -1,13 +1,14 @@
// 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 'src/components/DisconnectButton/DisconnectButton';
import QuestionNavigation from '../QuestionNavigation/QuestionNavigation';
import { ChevronLeft, ChevronRight } from '@mui/icons-material';
import DisconnectButton from '../../components/DisconnectButton/DisconnectButton';
interface StudentModeQuizProps {
questions: QuestionType[];
@ -24,10 +25,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]);
@ -54,12 +55,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}
@ -68,7 +69,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
@ -76,14 +77,14 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
disabled={Number(questionInfos.question.id) <= 1}
>
Question précédente
</Button> */}
</Button>
</div>
<div className="w-12">
<Button style={{ display: isAnswerSubmitted ? 'block' : 'none' }}
<Button
variant="outlined"
onClick={nextQuestion}
fullWidth
//endIcon={<ChevronRight />}
endIcon={<ChevronRight />}
disabled={Number(questionInfos.question.id) >= questions.length}
>
Question suivante
@ -92,7 +93,7 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
</div>
</div>
</div>
</div>
</div>
);
};

View file

@ -1,4 +1,3 @@
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 'src/components/DisconnectButton/DisconnectButton';
import DisconnectButton from '../../components/DisconnectButton/DisconnectButton';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material';
interface TeacherModeQuizProps {

View file

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

View file

@ -1,4 +1,3 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
@ -28,16 +27,10 @@ const theme = createTheme({
}
});
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');
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</BrowserRouter>
);

View file

@ -1,19 +1,19 @@
import React, { useEffect, useState } from 'react';
import { Socket } from 'socket.io-client';
import { ENV_VARIABLES } from 'src/constants';
//import { ENV_VARIABLES } from '../../../constants';
import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz';
import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz';
import StudentModeQuiz from '../../../components/StudentModeQuiz/StudentModeQuiz';
import TeacherModeQuiz from '../../../components/TeacherModeQuiz/TeacherModeQuiz';
import webSocketService, { AnswerSubmissionToBackendType } from '../../../services/WebsocketService';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import DisconnectButton from '../../../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 'src/components/LoginContainer/LoginContainer'
import LoginContainer from '../../../components/LoginContainer/LoginContainer'
const JoinRoom: React.FC = () => {
const [roomName, setRoomName] = useState('');
@ -27,15 +27,14 @@ const JoinRoom: React.FC = () => {
const [isConnecting, setIsConnecting] = useState<boolean>(false);
useEffect(() => {
handleCreateSocket();
//handleCreateSocket();
return () => {
disconnect();
};
}, []);
const handleCreateSocket = () => {
console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_SOCKET_URL}`);
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
const socket = webSocketService.connect(`/api/room/${roomName}/socket`);
socket.on('join-success', () => {
setIsWaitingForTeacher(true);
@ -64,10 +63,10 @@ const JoinRoom: React.FC = () => {
socket.on('connect_error', (error) => {
switch (error.message) {
case 'timeout':
setConnectionError("JoinRoom: timeout: Le serveur n'est pas disponible");
setConnectionError("Le serveur n'est pas disponible");
break;
case 'websocket error':
setConnectionError("JoinRoom: websocket error: Le serveur n'est pas disponible");
setConnectionError("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 'src/components/GiftTemplate/templates';
import Template from '../../../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 'src/components/ImportModal/ImportModal';
import ImportModal from '../../../components/ImportModal/ImportModal';
//import axios from 'axios';
import {
@ -18,11 +18,8 @@ import {
IconButton,
InputAdornment,
Button,
Card,
Tooltip,
NativeSelect,
CardContent,
styled,
NativeSelect
} from '@mui/material';
import {
Search,
@ -30,50 +27,19 @@ 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 [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[]>);
const [selectedFolder, setSelectedFolder] = useState<string>(''); // Selected folder
useEffect(() => {
const fetchData = async () => {
@ -82,7 +48,7 @@ const Dashboard: React.FC = () => {
return;
}
else {
const userFolders = await ApiService.getUserFolders();
let userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
}
@ -92,23 +58,40 @@ const Dashboard: React.FC = () => {
fetchData();
}, []);
const handleSelectFolder = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedFolderId(event.target.value);
setSelectedFolder(event.target.value);
};
useEffect(() => {
const fetchQuizzesForFolder = async () => {
if (selectedFolderId == '') {
if (selectedFolder == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizes")
let quizzes: QuizType[] = [];
var 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[])
}
@ -116,19 +99,17 @@ const Dashboard: React.FC = () => {
}
else {
console.log("show some quizzes")
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
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();
}, [selectedFolderId]);
}, [selectedFolder]);
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -153,24 +134,22 @@ const Dashboard: React.FC = () => {
const handleDuplicateQuiz = async (quiz: QuizType) => {
try {
await ApiService.duplicateQuiz(quiz._id);
if (selectedFolderId == '') {
if (selectedFolder == '') {
const folders = await ApiService.getUserFolders(); // HACK force user folders to load on first load
console.log("show all quizzes")
let quizzes: QuizType[] = [];
console.log("show all quizes")
var quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);
console.log("folder: ", folder.title, " quiz: ", folderQuizzes);
addFolderTitleToQuizzes(folderQuizzes, folder.title);
quizzes = quizzes.concat(folderQuizzes as QuizType[]);
quizzes = quizzes.concat(folderQuizzes as QuizType[])
}
setQuizzes(quizzes as QuizType[]);
}
else {
console.log("show some quizzes")
const folderQuizzes = await ApiService.getFolderContent(selectedFolderId);
addFolderTitleToQuizzes(folderQuizzes, selectedFolderId);
const folderQuizzes = await ApiService.getFolderContent(selectedFolder);
setQuizzes(folderQuizzes as QuizType[]);
}
@ -179,6 +158,13 @@ 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);
@ -196,7 +182,6 @@ 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;
}
@ -205,6 +190,30 @@ 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 {
@ -217,7 +226,7 @@ const Dashboard: React.FC = () => {
//const { title, content } = selectedQuiz;
let quizContent = "";
const title = selectedQuiz.title;
let title = selectedQuiz.title;
console.log(selectedQuiz.content);
selectedQuiz.content.forEach((question, qIndex) => {
const formattedQuestion = question.trim();
@ -254,7 +263,7 @@ const Dashboard: React.FC = () => {
const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
const newlyCreatedFolder = userFolders[userFolders.length - 1] as FolderType;
setSelectedFolderId(newlyCreatedFolder._id);
setSelectedFolder(newlyCreatedFolder._id);
}
} catch (error) {
@ -264,17 +273,18 @@ const Dashboard: React.FC = () => {
const handleDeleteFolder = async () => {
try {
const confirmed = window.confirm('Voulez-vous vraiment supprimer ce dossier?');
if (confirmed) {
await ApiService.deleteFolder(selectedFolderId);
await ApiService.deleteFolder(selectedFolder);
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 quizzes")
let quizzes: QuizType[] = [];
console.log("show all quizes")
var quizzes: QuizType[] = [];
for (const folder of folders as FolderType[]) {
const folderQuizzes = await ApiService.getFolderContent(folder._id);
@ -283,20 +293,19 @@ const Dashboard: React.FC = () => {
}
setQuizzes(quizzes as QuizType[]);
setSelectedFolderId('');
setSelectedFolder('');
} 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(selectedFolderId, newTitle);
await ApiService.renameFolder(selectedFolder, newTitle);
const userFolders = await ApiService.getUserFolders();
setFolders(userFolders as FolderType[]);
@ -305,16 +314,15 @@ const Dashboard: React.FC = () => {
console.error('Error renaming folder:', error);
}
};
const handleDuplicateFolder = async () => {
try {
// folderId: string GET THIS FROM CURRENT FOLDER
await ApiService.duplicateFolder(selectedFolderId);
await ApiService.duplicateFolder(selectedFolder);
// 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;
setSelectedFolderId(newlyCreatedFolder._id);
setSelectedFolder(newlyCreatedFolder._id);
} catch (error) {
console.error('Error duplicating folder:', error);
}
@ -384,7 +392,7 @@ const Dashboard: React.FC = () => {
<NativeSelect
id="select-folder"
color="primary"
value={selectedFolderId}
value={selectedFolder}
onChange={handleSelectFolder}
>
<option value=""> Tous les dossiers... </option>
@ -407,7 +415,7 @@ const Dashboard: React.FC = () => {
<IconButton
color="primary"
onClick={handleRenameFolder}
disabled={selectedFolderId == ''} // cannot action on all
disabled={selectedFolder == ''} // cannot action on all
> <Edit /> </IconButton>
</Tooltip>
@ -415,8 +423,8 @@ const Dashboard: React.FC = () => {
<IconButton
color="primary"
onClick={handleDuplicateFolder}
disabled={selectedFolderId == ''} // cannot action on all
> <FolderCopy /> </IconButton>
disabled={selectedFolder == ''} // cannot action on all
> <ContentCopy /> </IconButton>
</Tooltip>
<Tooltip title="Supprimer dossier" placement="top">
@ -424,7 +432,7 @@ const Dashboard: React.FC = () => {
aria-label="delete"
color="primary"
onClick={handleDeleteFolder}
disabled={selectedFolderId == ''} // cannot action on all
disabled={selectedFolder == ''} // cannot action on all
> <DeleteOutline /> </IconButton>
</Tooltip>
</div>
@ -452,72 +460,74 @@ 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>
<div className='actions'>
<Tooltip title="Télécharger quiz" placement="top">
<IconButton
color="primary"
onClick={() => downloadTxtFile(quiz)}
> <FileDownload /> </IconButton>
</Tooltip>
{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>
<Tooltip title="Modifier quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleEditQuiz(quiz)}
> <Edit /> </IconButton>
</Tooltip>
<div className='actions'>
<Tooltip title="Télécharger quiz" placement="top">
<IconButton
color="primary"
onClick={() => downloadTxtFile(quiz)}
> <FileDownload /> </IconButton>
</Tooltip>
<Tooltip title="Dupliquer quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleDuplicateQuiz(quiz)}
> <ContentCopy /> </IconButton>
</Tooltip>
<Tooltip title="Modifier quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleEditQuiz(quiz)}
> <Edit /> </IconButton>
</Tooltip>
<Tooltip title="Supprimer quiz" placement="top">
<IconButton
aria-label="delete"
color="primary"
onClick={() => handleRemoveQuiz(quiz)}
> <DeleteOutline /> </IconButton>
</Tooltip>
{/* <Tooltip title="Bouger quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleMoveQuiz(quiz)}
> <DriveFileMove /> </IconButton>
</Tooltip> */}
<Tooltip title="Partager quiz" placement="top">
<IconButton
color="primary"
onClick={() => handleShareQuiz(quiz)}
> <Share /> </IconButton>
</Tooltip>
</div>
</div>
))}
</CardContent>
</CustomCard>
<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>
))}
</div>
<ImportModal
open={showImportModal}
handleOnClose={() => setShowImportModal(false)}
handleOnImport={handleOnImport}
selectedFolder={selectedFolderId}
selectedFolder={selectedFolder}
/>
</div>
@ -525,11 +535,3 @@ 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

@ -78,42 +78,3 @@ div:has(> #select-folder) {
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, CSSProperties } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { FolderType } from '../../../Types/FolderType';
import Editor from 'src/components/Editor/Editor';
import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet';
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
import Editor from '../../../components/Editor/Editor';
import GiftCheatSheet from '../../../components/GIFTCheatSheet/GiftCheatSheet';
import GIFTTemplatePreview from '../../../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 'src/components/ReturnButton/ReturnButton';
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
import ApiService from '../../../services/ApiService';
import { escapeForGIFT } from '../../../utils/giftUtils';
@ -40,26 +40,6 @@ 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 () => {
@ -182,10 +162,8 @@ 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`)
}
};
@ -267,7 +245,7 @@ const QuizForm: React.FC = () => {
onClose={() => setDialogOpen(false)} >
<DialogTitle>Erreur</DialogTitle>
<DialogContent>
Veuillez d&apos;abord choisir une image à téléverser.
Veuillez d'abord choisir une image à téléverser.
</DialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)} color="primary">
@ -312,32 +290,8 @@ 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 'src/components/LoginContainer/LoginContainer'
import LoginContainer from '../../../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 (typeof result === "string") {
if (result != true) {
setConnectionError(result);
return;
}
@ -49,7 +49,7 @@ const Login: React.FC = () => {
variant="outlined"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Adresse courriel"
placeholder="Nom d'utilisateur"
sx={{ marginBottom: '1rem' }}
fullWidth
/>
@ -60,7 +60,7 @@ const Login: React.FC = () => {
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Mot de passe"
placeholder="Nom de la salle"
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 'src/components/LiveResults/LiveResults';
import LiveResultsComponent from '../../../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 'src/constants';
//import { ENV_VARIABLES } from '../../../constants';
import { StudentType, Answer } from '../../../Types/StudentType';
import { Button } from '@mui/material';
import LoadingCircle from 'src/components/LoadingCircle/LoadingCircle';
import LoadingCircle from '../../../components/LoadingCircle/LoadingCircle';
import { Refresh, Error } from '@mui/icons-material';
import StudentWaitPage from 'src/components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
//import QuestionNavigation from 'src/components/QuestionNavigation/QuestionNavigation';
import Question from 'src/components/Questions/Question';
import StudentWaitPage from '../../../components/StudentWaitPage/StudentWaitPage';
import DisconnectButton from '../../../components/DisconnectButton/DisconnectButton';
import QuestionNavigation from '../../../components/QuestionNavigation/QuestionNavigation';
import Question from '../../../components/Questions/Question';
import ApiService from '../../../services/ApiService';
const ManageRoom: React.FC = () => {
@ -49,7 +49,6 @@ const ManageRoom: React.FC = () => {
setQuiz(quiz as QuizType);
if (!socket) {
console.log(`no socket in ManageRoom, creating one.`);
createWebSocketRoom();
}
@ -80,17 +79,22 @@ const ManageRoom: React.FC = () => {
}
};
const createWebSocketRoom = () => {
console.log('Creating WebSocket room...');
const createWebSocketRoom = async () => {
setConnectingError('');
const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_SOCKET_URL);
const room = await ApiService.createRoom();
const socket = webSocketService.connect(`/api/room/${room.id}/socket`);
socket.on('connect', () => {
webSocketService.createRoom();
webSocketService.createRoom(room.id);
});
socket.on("error", (error) => {
console.error("WebSocket server error:", error);
});
socket.on('connect_error', (error) => {
setConnectingError('Erreur lors de la connexion... Veuillez réessayer');
console.error('ManageRoom: WebSocket connection error:', error);
console.error('WebSocket connection error:', error);
});
socket.on('create-success', (roomName: string) => {
setRoomName(roomName);
@ -124,8 +128,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') {
@ -172,7 +176,7 @@ const ManageRoom: React.FC = () => {
updatedAnswers = [...student.answers, newAnswer];
}
return { ...student, answers: updatedAnswers };
}
}
return student;
});
if (!foundStudent) {
@ -267,6 +271,7 @@ 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]);
};
@ -460,12 +465,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>
)}
@ -492,23 +497,12 @@ 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"
disabled={Number(currentQuestion?.question.id) >=quizQuestions.length}
>
<Button onClick={nextQuestion} variant="contained">
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 'src/components/LoginContainer/LoginContainer'
import LoginContainer from '../../../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 (typeof result === 'string') {
if (result != true) {
setConnectionError(result);
return;
}
@ -70,7 +70,7 @@ const Register: React.FC = () => {
sx={{ marginBottom: `${connectionError && '2rem'}` }}
disabled={!email || !password}
>
S&apos;inscrire
S'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 'src/components/LoginContainer/LoginContainer'
import LoginContainer from '../../../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 (typeof result === 'string') {
if (result != true) {
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 'src/components/ReturnButton/ReturnButton';
import ReturnButton from '../../../components/ReturnButton/ReturnButton';
import ApiService from '../../../services/ApiService';

View file

@ -1,10 +1,8 @@
import axios, { AxiosError, AxiosResponse } from 'axios';
import { ENV_VARIABLES } from '../constants';
import { FolderType } from 'src/Types/FolderType';
import { QuizType } from 'src/Types/QuizType';
import { ENV_VARIABLES } from 'src/constants';
type ApiResponse = boolean | string;
import { QuizType } from '../Types/QuizType';
import { FolderType } from '../Types/FolderType';
class ApiService {
private BASE_URL: string;
@ -19,7 +17,7 @@ class ApiService {
return `${this.BASE_URL}/api${endpoint}`;
}
private constructRequestHeaders() {
private constructRequestHeaders(): any {
if (this.isLoggedIn()) {
return {
Authorization: `Bearer ${this.getToken()}`,
@ -82,13 +80,85 @@ class ApiService {
return localStorage.removeItem("jwt");
}
//Socket Route
/**
* Creates a new room.
* @returns The room object if successful
* @returns An error string if unsuccessful
*/
public async createRoom(): Promise<any> {
try {
const url: string = this.constructRequestUrl(`/room`);
const headers = this.constructRequestHeaders();
const response = await fetch(url, {
method: 'POST',
headers: headers,
});
if (!response.ok) {
throw new Error(`La création de la salle a échoué. Status: ${response.status}`);
}
const room = await response.json();
return room;
} catch (error) {
console.log("Error details: ", error);
if (error instanceof Error) {
return error.message || 'Erreur serveur inconnue lors de la requête.';
}
return `Une erreur inattendue s'est produite.`;
}
}
/**
* Deletes a room by its name.
* @param roomName - The name of the room to delete.
* @returns true if successful
* @returns An error string if unsuccessful
*/
public async deleteRoom(roomName: string): Promise<any> {
try {
if (!roomName) {
throw new Error(`Le nom de la salle est requis.`);
}
const url = this.constructRequestUrl(`/room/${roomName}`);
const headers = this.constructRequestHeaders();
fetch(url, {
method: 'DELETE',
headers: headers,
});
return true;
} catch (error) {
console.log("Error details: ", error);
if (error instanceof Error) {
return error.message || 'Erreur serveur inconnue lors de la requête.';
}
return `Une erreur inattendue s'est produite.`;
}
}
// User Routes
/**
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async register(email: string, password: string): Promise<ApiResponse> {
public async register(email: string, password: string): Promise<any> {
try {
if (!email || !password) {
@ -124,7 +194,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async login(email: string, password: string): Promise<ApiResponse> {
public async login(email: string, password: string): Promise<any> {
try {
if (!email || !password) {
@ -148,13 +218,8 @@ 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.';
}
@ -164,10 +229,10 @@ class ApiService {
}
/**
* @returns true if successful
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async resetPassword(email: string): Promise<ApiResponse> {
public async resetPassword(email: string): Promise<any> {
try {
if (!email) {
@ -203,7 +268,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async changePassword(email: string, oldPassword: string, newPassword: string): Promise<ApiResponse> {
public async changePassword(email: string, oldPassword: string, newPassword: string): Promise<any> {
try {
if (!email || !oldPassword || !newPassword) {
@ -239,7 +304,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async deleteUser(email: string, password: string): Promise<ApiResponse> {
public async deleteUser(email: string, password: string): Promise<any> {
try {
if (!email || !password) {
@ -277,7 +342,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async createFolder(title: string): Promise<ApiResponse> {
public async createFolder(title: string): Promise<any> {
try {
if (!title) {
@ -309,6 +374,7 @@ class ApiService {
}
}
/**
* @returns folder array if successful
* @returns A error string if unsuccessful,
@ -382,7 +448,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async deleteFolder(folderId: string): Promise<ApiResponse> {
public async deleteFolder(folderId: string): Promise<any> {
try {
if (!folderId) {
@ -417,7 +483,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async renameFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
public async renameFolder(folderId: string, newTitle: string): Promise<any> {
try {
if (!folderId || !newTitle) {
@ -448,7 +514,7 @@ class ApiService {
}
}
public async duplicateFolder(folderId: string): Promise<ApiResponse> {
public async duplicateFolder(folderId: string): Promise<any> {
try {
if (!folderId) {
throw new Error(`Le folderId et le nouveau titre sont requis.`);
@ -480,7 +546,7 @@ class ApiService {
}
}
public async copyFolder(folderId: string, newTitle: string): Promise<ApiResponse> {
public async copyFolder(folderId: string, newTitle: string): Promise<any> {
try {
if (!folderId || !newTitle) {
throw new Error(`Le folderId et le nouveau titre sont requis.`);
@ -517,7 +583,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async createQuiz(title: string, content: string[], folderId: string): Promise<ApiResponse> {
public async createQuiz(title: string, content: string[], folderId: string): Promise<any> {
try {
if (!title || !content || !folderId) {
@ -588,7 +654,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async deleteQuiz(quizId: string): Promise<ApiResponse> {
public async deleteQuiz(quizId: string): Promise<any> {
try {
if (!quizId) {
@ -623,7 +689,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async updateQuiz(quizId: string, newTitle: string, newContent: string[]): Promise<ApiResponse> {
public async updateQuiz(quizId: string, newTitle: string, newContent: string[]): Promise<any> {
try {
if (!quizId || !newTitle || !newContent) {
@ -659,7 +725,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async moveQuiz(quizId: string, newFolderId: string): Promise<ApiResponse> {
public async moveQuiz(quizId: string, newFolderId: string): Promise<any> {
try {
if (!quizId || !newFolderId) {
@ -696,7 +762,7 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async duplicateQuiz(quizId: string): Promise<ApiResponse> {
public async duplicateQuiz(quizId: string): Promise<any> {
const url: string = this.constructRequestUrl(`/quiz/duplicate`);
@ -710,7 +776,7 @@ class ApiService {
throw new Error(`La duplication du quiz a échoué. Status: ${result.status}`);
}
return result.status === 200;
return result;
} catch (error) {
console.error("Error details: ", error);
@ -730,9 +796,9 @@ class ApiService {
* @returns true if successful
* @returns A error string if unsuccessful,
*/
public async copyQuiz(quizId: string, newTitle: string, folderId: string): Promise<ApiResponse> {
public async copyQuiz(quizId: string, newTitle: string, folderId: string): Promise<any> {
try {
console.log(quizId, newTitle, folderId);
console.log(quizId, newTitle), folderId;
return "Route not implemented yet!";
} catch (error) {
@ -748,7 +814,7 @@ class ApiService {
}
}
async ShareQuiz(quizId: string, email: string): Promise<ApiResponse> {
async ShareQuiz(quizId: string, email: string): Promise<any> {
try {
if (!quizId || !email) {
throw new Error(`quizId and email are required.`);
@ -807,7 +873,7 @@ class ApiService {
}
}
async receiveSharedQuiz(quizId: string, folderId: string): Promise<ApiResponse> {
async receiveSharedQuiz(quizId: string, folderId: string): Promise<any> {
try {
if (!quizId || !folderId) {
throw new Error(`quizId and folderId are required.`);
@ -876,8 +942,7 @@ class ApiService {
if (axios.isAxiosError(error)) {
const err = error as AxiosError;
const data = err.response?.data as { error: string } | undefined;
const msg = data?.error || 'Erreur serveur inconnue lors de la requête.';
return `ERROR : ${msg}`;
return `ERROR : ${data?.error}` || 'ERROR : Erreur serveur inconnue lors de la requête.';
}
return `ERROR : Une erreur inattendue s'est produite.`

View file

@ -1,5 +1,6 @@
// WebSocketService.tsx
import { io, Socket } from 'socket.io-client';
import apiService from './ApiService';
// Must (manually) sync these types to server/socket/socket.js
@ -21,20 +22,15 @@ class WebSocketService {
private socket: Socket | null = null;
connect(backendUrl: string): Socket {
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, {
this.socket = io( '/',{
path: backendUrl,
transports: ['websocket'],
reconnectionAttempts: 1
autoConnect: true,
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 10000,
timeout: 20000,
});
return this.socket;
}
@ -46,9 +42,9 @@ class WebSocketService {
}
}
createRoom() {
createRoom(roomName: string) {
if (this.socket) {
this.socket.emit('create-room');
this.socket.emit('create-room', roomName || undefined);
}
}
@ -67,6 +63,8 @@ class WebSocketService {
endQuiz(roomName: string) {
if (this.socket) {
this.socket.emit('end-quiz', { roomName });
//Delete room in mongoDb, roomContainer will be deleted in cleanup
apiService.deleteRoom(roomName);
}
}
@ -84,14 +82,14 @@ class WebSocketService {
) {
if (this.socket) {
this.socket?.emit('submit-answer',
// {
// answer: answer,
// roomName: roomName,
// username: username,
// idQuestion: idQuestion
// }
answerData
);
// {
// 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,13 +1,9 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"src/*": ["src/*"]
},
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"module": "ES2020",
"skipLibCheck": true,
/* Bundler mode */
@ -16,7 +12,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"jsx": "react-jsx",
/* Linting */
"strict": true,

View file

@ -3,44 +3,22 @@ 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(filteredEnv),
EnvironmentPlugin('all'),
],
resolve: {
alias: {
'src': '/src'
}
},
preview: {
port: 5173,
strictPort: true
},
server: {
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
},
},
port: 5173,
strictPort: true,
host: true,
origin: "http://0.0.0.0:5173",
},
});

74
create-branch-image.bat Normal file
View file

@ -0,0 +1,74 @@
@echo off
setlocal EnableDelayedExpansion
:: Check if gh is installed
where gh >nul 2>&1
if %errorlevel% neq 0 (
echo GitHub CLI not found. Installing...
winget install --id GitHub.cli
if %errorlevel% neq 0 (
echo Failed to install GitHub CLI. Exiting...
exit /b 1
)
echo GitHub CLI installed successfully.
)
:: Check if user is authenticated
gh auth status >nul 2>&1
if %errorlevel% neq 0 (
echo GitHub CLI not authenticated. Please authenticate...
gh auth login
if %errorlevel% neq 0 (
echo Failed to authenticate. Exiting...
exit /b 1
)
echo Authentication successful.
)
:: Get the current branch name
for /f "tokens=*" %%i in ('git rev-parse --abbrev-ref HEAD') do set BRANCH_NAME=%%i
:: Run the GitHub workflow with the current branch name
echo Running GitHub workflow with branch %BRANCH_NAME%...
gh workflow run 119194149 --ref %BRANCH_NAME%
:: Wait and validate workflow launch
set /a attempts=0
set /a max_attempts=12
echo Waiting for workflow to start...
:wait_for_workflow
timeout /t 15 >nul
set /a attempts+=1
:: Get recent workflow run matching our criteria with in_progress status
for /f "tokens=*" %%i in ('gh run list --branch %BRANCH_NAME% --status in_progress --limit 1 --json databaseId --jq ".[0].databaseId"') do set WORKFLOW_RUN_ID=%%i
if "%WORKFLOW_RUN_ID%"=="" (
if !attempts! lss !max_attempts! (
echo Attempt !attempts! of !max_attempts!: No running workflow found yet...
goto wait_for_workflow
) else (
echo Timeout waiting for workflow to start running.
exit /b 1
)
)
echo Found running workflow ID: %WORKFLOW_RUN_ID%
:monitor_progress
cls
echo Workflow Progress:
echo ----------------
gh run view %WORKFLOW_RUN_ID% --json jobs --jq ".jobs[] | \"Job: \" + .name + \" - Status: \" + .status + if .conclusion != null then \" (\" + .conclusion + \")\" else \"\" end"
echo.
:: Check if workflow is still running
for /f "tokens=*" %%i in ('gh run view %WORKFLOW_RUN_ID% --json status --jq ".status"') do set CURRENT_STATUS=%%i
if "%CURRENT_STATUS%" == "completed" (
echo Workflow completed.
exit /b 0
)
timeout /t 5 >nul
goto monitor_progress

137
docker-compose.local.yaml Normal file
View file

@ -0,0 +1,137 @@
version: '3'
services:
frontend:
container_name: frontend
build:
context: ./client
dockerfile: Dockerfile
ports:
- "5173:5173"
networks:
- quiz_network
restart: always
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:$${PORT} || exit 1"]
interval: 5s
timeout: 10s
start_period: 5s
retries: 6
backend:
build:
context: ./server
dockerfile: Dockerfile
container_name: backend
networks:
- quiz_network
ports:
- "3000:3000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
PORT: 3000
MONGO_URI: "mongodb://mongo:27017/evaluetonsavoir"
MONGO_DATABASE: evaluetonsavoir
EMAIL_SERVICE: gmail
SENDER_EMAIL: infoevaluetonsavoir@gmail.com
EMAIL_PSW: 'vvml wmfr dkzb vjzb'
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
depends_on:
mongo:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:$${PORT}/health || exit 1"]
interval: 5s
timeout: 10s
start_period: 5s
retries: 6
quizroom: # Forces image to update
build:
context: ./quizRoom
dockerfile: Dockerfile
container_name: quizroom
ports:
- "4500:4500"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- quiz_network
restart: always
healthcheck:
test: ["CMD", "/usr/src/app/healthcheck.sh"]
interval: 5s
timeout: 10s
start_period: 5s
retries: 6
nginx:
build:
context: ./nginx
dockerfile: Dockerfile
container_name: nginx
ports:
- "80:80"
depends_on:
frontend:
condition: service_healthy
backend:
condition: service_healthy
networks:
- quiz_network
restart: always
#environment:
# - PORT=8000
# - FRONTEND_HOST=frontend
# - FRONTEND_PORT=5173
# - BACKEND_HOST=backend
# - BACKEND_PORT=3000
healthcheck:
test: ["CMD-SHELL", "wget --spider http://0.0.0.0:$${PORT}/health || exit 1"]
interval: 5s
timeout: 10s
start_period: 5s
retries: 6
mongo:
image: mongo
container_name: mongo
ports:
- "27017:27017"
tty: true
volumes:
- mongodb_data:/data/db
networks:
- quiz_network
restart: always
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 3
start_period: 20s
watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- TZ=America/Montreal
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_DEBUG=true
- WATCHTOWER_INCLUDE_RESTARTING=true
- WATCHTOWER_SCHEDULE=0 0 5 * * * # At 5 am everyday
networks:
- quiz_network
restart: always
networks:
quiz_network:
driver: bridge
volumes:
mongodb_data:
external: false

View file

@ -1,13 +1,10 @@
version: '3'
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
@ -30,6 +27,17 @@ services:
- mongo
restart: always
quizroom:
build:
context: ./quizRoom
dockerfile: Dockerfile
container_name: quizroom
ports:
- "4500:4500"
depends_on:
- backend
restart: always
# Ce conteneur sert de routeur pour assurer le bon fonctionnement de l'application
nginx:
image: fuhrmanator/evaluetonsavoir-routeur:latest
@ -52,7 +60,7 @@ services:
- mongodb_data:/data/db
restart: always
# Ce conteneur cherche des mises à jour à 5h du matin
# Ce conteneur assure que l'application est à jour en allant chercher s'il y a des mises à jours à chaque heure
watchtower:
image: containrrr/watchtower
container_name: watchtower
@ -66,19 +74,6 @@ 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

1
documentation/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
site

2
documentation/deploy.py Normal file
View file

@ -0,0 +1,2 @@
from ghp_import import ghp_import
ghp_import('site', push=True, force=True)

View file

@ -0,0 +1,12 @@
## À Propos
Ce projet utilise Node.js Express pour créer un backend simple pour l'application.
## Routes API
Vous pouvez consulter toutes les routes utilisables du backend ici
* User : https://documenter.getpostman.com/view/32663805/2sA2rCU28v#e942a4f4-321c-465b-bf88-e6c1f1d6f6c8
* Quiz : https://documenter.getpostman.com/view/32663805/2sA2rCU28v#732d980b-02fd-4807-b5bc-72725098b9b0
* Folders : https://documenter.getpostman.com/view/32663805/2sA2rCU28v#49ecd432-ccfc-4c8a-8390-b3962f0d5fd7
* Images : https://documenter.getpostman.com/view/32663805/2sA2rCU28v#58382180-d6f0-492d-80c3-e09de1c368b8

View file

@ -0,0 +1,384 @@
# Authentification
## Introduction
Le but du module d'authentification est de pouvoir facilement faire des blocs de code permettant une authentification
personalisée. Il est possible de le faire grâce à cette architecture. Pour la première version de cette fonctionalité,
l'introduction de OIDC et de OAuth sont priorisé ainsi que la migration du module d'authentification simple.
## Déconstruction simple de la structure
La structure est la suivante :
Le AuthManager s'occupe de centraliser les requêtes d'authentification. Ce dernier initialise les autres modules et est
la source de vérité dans les aspects liés à l'authentification. Les modules sont automatiquement chargés par
l'utilisation de variables d'environment.
Le module s'occupe de créer les routes nécessaires pour son fonctionnement et de créer les utilisateurs. Ces modules
vont appeller le AuthManager afin de confirmer leurs actions avec le login/register de celui-ci.
Dans le cas de modules plus complexe, tels que le module Passport, la chaine peut être prolongée afin de maintenir
les actions centralisée . Chaque connecteur de PassportJs est initialisé par le module de PassportJs.
## Besoins exprimés
Modularité et généricité :
- Le système d'authentification doit être adaptable à diverses configurations, notamment pour répondre aux exigences
spécifiques des différentes universités ou institutions.
Utilisation de différentes méthodes d'authentification :
- L'application doit permettre de gérer plusieurs fournisseurs d'authentification (SSO, LDAP, OAuth, etc.) de manière
centralisée et flexible.
Facilité de configuration :
- Le système doit permettre une configuration simple et flexible, adaptée à différents environnements (développement,
production, etc.).
Gestion des permissions :
- Il doit être possible de définir et de mapper facilement les permissions et les rôles des utilisateurs pour sécuriser
laccès aux différentes fonctionnalités de lapplication.
Maintien de la connexion :
- Le système doit garantir la persistance de la connexion pendant toute la durée de l'utilisation de l'application
(exemple : quiz), avec la possibilité de se reconnecter sans perte de données en cas de déconnexion temporaire.
## Recits utilisateurs pris en comptes
- En tant qu'utilisateur de projet FOSS, je veux que le module d'authentification soit modulaire et générique afin de
l'adapter à mes besoins.
- En tant qu'administrateur, je veux que les droits des utilisateurs soient inférés par l'authentificateur de l'établissement.
- En tant qu'administrateur, je veux que la configuration des authentificateurs soit simple
- En tant qu'administrateur, je veux configurer les connexions à partir de variables d'environnement ou fichier de config.
- En tant qu'utilisateur, je veux que ma connexion soit stable.
- En tant qu'utilisateur, je veux pouvoir me reconnecter à une salle s'il survient un problème de connexion.
## Diagrammes
### Structure
```plantuml
@startuml
package Backend {
class AuthManager{
+IAuthModule[] auths
#userInfos
-load()
-registerAuths()
+showAuths()
+authStatus()
+logIn(UserInfos)
+register(UserInfos)
+logOut()
}
interface IAuthModule{
+registerAuth()
+authenticate()
+register()
+showAuth()
}
class SimpleFormAuthModule{
}
class PassportAuthModule{
IPassportProviderDefinition[] providers
}
Interface IPassportProviderDefinition{
+name
+type
}
class OAuthPassportProvider{
+clientId
+clientSecret
+configUrl
+authorizeUrl
+tokenUrl
+userinfoUrl
+logoutUrl
+JWKSUrl
}
IAuthModule <|-- SimpleFormAuthModule
IAuthModule <|-- PassportAuthModule
IPassportProviderDefinition <|-- OAuthPassportProvider
AuthManager -> IAuthModule
PassportAuthModule -> IPassportProviderDefinition
}
package Frontend{
class AuthDrawer{
+IAuthVisual[] getAuthsVisual()
+drawAuths()
}
Interface IAuthVisual{
+draw()
}
class FormVisual{
+FormInput[] formInputs
}
interface FormInput{
+name
+label
+type
+value
}
AuthDrawer -> IAuthVisual
IAuthVisual <|-- FormVisual
FormVisual -> FormInput
}
@enduml
```
### Explication des communications : Passport Js
```plantuml
@startuml
box "Frontend"
participant User
Participant App
end box
box "Backend"
participant PassportAuthModule
participant Db
participant AuthManager
end box
box "Auth Server"
participant AuthServer
end box
User -> App : Get auth page
App -> User : auth page
User -> App : click OAuth button
App -> User : redirect to OAuth
User -> AuthServer: Login
AuthServer -> User: Redirect to Auth endpoint with token
User -> PassportAuthModule: Authenticate with token
PassportAuthModule -> AuthServer: get user info
AuthServer -> PassportAuthModule: userInfo
alt login
PassportAuthModule -> Db : fetch local userInfo
Db->PassportAuthModule: userInfo
PassportAuthModule -> PassportAuthModule: Merge userInfo definition
PassportAuthModule -> Db : update user profile
Db->PassportAuthModule: userInfo
end
alt register
PassportAuthModule -> Db : fetch local userInfo
Db->PassportAuthModule: null
PassportAuthModule -> Db : create user profile
Db->PassportAuthModule: userInfo
end
PassportAuthModule -> AuthManager : login(userInfos)
AuthManager -> User: Give refresh token + Redirect to page
User -> App: get /
App -> User: Show Authenticated /
@enduml
```
### Explication des communications : SimpleAuth
```plantuml
@startuml
box "Frontend"
participant User
Participant App
end box
box "Backend"
participant SimpleAuthModule
participant Db
participant AuthManager
end box
User -> App : Get auth page
App -> User : auth page
alt Login
User -> App : Send Login/Pass
App -> SimpleAuthModule: Send login/pass
SimpleAuthModule -> Db: get user info
Db->SimpleAuthModule: user info
SimpleAuthModule -> SimpleAuthModule: Validate Hash
end
alt register
User -> App : Send Username + Password + Email
App -> SimpleAuthModule: Send Username + Password + Email
SimpleAuthModule -> Db: get user info
Db -> SimpleAuthModule : null
SimpleAuthModule -> Db: put user info
end
SimpleAuthModule -> AuthManager: userInfo
AuthManager -> User: Give refresh token + Redirect to page
User -> App: get /
App -> User: Show Authenticated /
@enduml
```
### Comment les boutons sont affichés
```plantuml
@startuml
box "FrontEnd"
participant User
Participant FrontEnd
Participant AuthDrawer
end box
box "BackEnd"
participant API
participant AuthManager
participant Db
participant IAuthModule
end box
API -> API : load global configurations
create AuthManager
API -> AuthManager : instanciate with auth configurations
create IAuthModule
AuthManager -> IAuthModule : instanciate array
loop For each auth in auths
AuthManager -> IAuthModule : register
IAuthModule -> API : register routes
API -> IAuthModule : route registration confirmation
IAuthModule -> AuthManager : module registration confirmation
end
User -> FrontEnd : get login page
alt already logged in
FrontEnd -> User: redirected to authenticated page
end
FrontEnd -> AuthDrawer : get auth visual
AuthDrawer -> API : get auth form data
API -> AuthManager : get auth form data
loop For each auth in auths
AuthManager -> IAuthModule : get form data
IAuthModule -> AuthManager : form data
end
AuthManager -> API : auth fom data
API -> AuthDrawer : auth form data
AuthDrawer -> AuthDrawer : make auth html
AuthDrawer -> FrontEnd : auth HTML
FrontEnd -> User : show auth page
@enduml
```
### Comment les sessions sont conservées
```plantuml
@startuml
box "Frontend"
participant User
Participant App
end box
box "Backend"
participant AuthManager
participant IAuthModules
end box
App -> AuthManager : send refresh token
AuthManager -> IAuthModules: ForEach check if logged
IAuthModules -> AuthManager: is authenticated ?
alt one logged in
AuthManager -> App : send new token
end
alt all logged out
AuthManager -> App : send error
App -> App : destroy token
App -> User : redirect to login page
end
@enduml
```
## Configuration des variables d'environnement
Example de configuration du fichier : `server/auth_config.json` :
```json
{
"auth": {
"passportjs": // Module
[
{
"gmatte": { // Nom du sous-module Passport
"type": "oauth", // type
"OAUTH_AUTHORIZATION_URL": "https://auth.gmatte.xyz/application/o/authorize/",
"OAUTH_TOKEN_URL": "https://auth.gmatte.xyz/application/o/token/",
"OAUTH_USERINFO_URL": "https://auth.gmatte.xyz/application/o/userinfo/",
"OAUTH_CLIENT_ID": "--redacted--",
"OAUTH_CLIENT_SECRET": "--Redacted--",
"OAUTH_ADD_SCOPE": "groups", // scopes supplémentaire nécessaire pour le pivot
"OAUTH_ROLE_TEACHER_VALUE": "groups_evaluetonsavoir-prof", // valeur de pivot afin de définir un enseignant
"OAUTH_ROLE_STUDENT_VALUE": "groups_evaluetonsavoir" // valeur de pivot afin de définir un étudiant
}
},
{
"etsmtl":{
"type":"oidc",
"OIDC_CONFIG_URL":"https://login.microsoftonline.com/70aae3b7-9f3b-484d-8f95-49e8fbb783c0/v2.0/.well-known/openid-configuration",
"OIDC_CLIENT_ID": "--redacted--",
"OIDC_CLIENT_SECRET": "--redacted--",
"OIDC_ADD_SCOPE": "",
"OIDC_ROLE_TEACHER_VALUE": "groups_evaluetonsavoir-prof",
"OIDC_ROLE_STUDENT_VALUE": "groups_evaluetonsavoir"
}
}
],
"simpleauth":{}
}
}
```

View file

@ -0,0 +1,11 @@
# Type de base de données
La base de données est une MongoDB.
# Collections disponibles
* Files : Ceci est la collection qui contient les différents quiz et leurs questions.
* Folders : Ceci est la collection qui contient les dossiers qui servent à la gestion des différents quiz
* Images : C'est dans cette collection que sont stockées les images utilisées dans les quiz
* Users : Cette collection est utilisée pour la gestion des utilisateurs
# Information sur la création
Lors du démarrage du projet, la base de données est créée automatiquement.

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