From c6c830ef741e4d514d249286e89910b310e5a40d Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Sat, 22 Feb 2025 17:56:53 -0500 Subject: [PATCH 01/46] init gestion img --- server/__tests__/image.test.js | 83 ++++++++++++++++++++++++++++++++++ server/controllers/images.js | 17 +++++++ server/models/images.js | 22 +++++++++ 3 files changed, 122 insertions(+) diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js index 488d27b..b220e2c 100644 --- a/server/__tests__/image.test.js +++ b/server/__tests__/image.test.js @@ -7,6 +7,9 @@ // const BASE_URL = '/image' +const Images = require('../models/images'); +const ObjectId = require('mongodb').ObjectId; + describe.skip("POST /upload", () => { describe("when the jwt is not sent", () => { @@ -64,3 +67,83 @@ describe.skip("GET /get", () => { }) }) + +describe('Images', () => { + let db; + let images; + let dbConn; + let mockImagesCollection; + + beforeEach(() => { + mockImagesCollection = { + insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }), + findOne: jest.fn() + }; + + dbConn = { + collection: jest.fn().mockReturnValue(mockImagesCollection) + }; + + db = { + connect: jest.fn().mockResolvedValue(), + getConnection: jest.fn().mockReturnValue(dbConn) + }; + + images = new Images(db); + }); + + describe('upload', () => { + it('should upload an image and return the inserted ID', async () => { + const testFile = { originalname: 'test.png', buffer: Buffer.from('dummydata'), mimetype: 'image/png' }; + const userId = 'user123'; + + const result = await images.upload(testFile, userId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(dbConn.collection).toHaveBeenCalledWith('images'); + expect(mockImagesCollection.insertOne).toHaveBeenCalledWith({ + userId: userId, + file_name: 'test.png', + file_content: testFile.buffer.toString('base64'), + mime_type: 'image/png', + created_at: expect.any(Date) + }); + expect(result).toBe('image123'); + }); + }); + + describe('get', () => { + it('should retrieve the image if found', async () => { + const imageId = '65d9a8f9b5e8d1a5e6a8c9f0'; + const testImage = { + file_name: 'test.png', + file_content: Buffer.from('dummydata').toString('base64'), + mime_type: 'image/png' + }; + mockImagesCollection.findOne.mockResolvedValue(testImage); + + const result = await images.get(imageId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(dbConn.collection).toHaveBeenCalledWith('images'); + expect(mockImagesCollection.findOne).toHaveBeenCalledWith({ _id: ObjectId.createFromHexString(imageId) }); + expect(result).toEqual({ + file_name: 'test.png', + file_content: Buffer.from('dummydata'), + mime_type: 'image/png' + }); + }); + + it('should return null if image is not found', async () => { + const imageId = '65d9a8f9b5e8d1a5e6a8c9f0'; + mockImagesCollection.findOne.mockResolvedValue(null); + + const result = await images.get(imageId); + + expect(result).toBeNull(); + }); + }); +}); + diff --git a/server/controllers/images.js b/server/controllers/images.js index b77ed96..ff41d5c 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -50,6 +50,23 @@ class ImagesController { } }; + //TODO TEST + getAll = async (req, res, next) => { + try { + + const images = await this.images.getAll(); + + if (!images || images.length === 0) { + throw new AppError(IMAGE_NOT_FOUND); + } + + res.setHeader('Content-Type', 'application/json'); + return res.status(200).json(imagesName); + } catch (error) { + return next(error); + } + }; + } module.exports = ImagesController; diff --git a/server/models/images.js b/server/models/images.js index 67a6583..8713424 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -42,6 +42,28 @@ class Images { }; } + //TODO TEST + async getAll() { + await this.db.connect() + const conn = this.db.getConnection(); + + const imagesCollection = conn.collection('images'); + + const result = await imagesCollection.find({}); + + if (!result) return null; + + //TODO latency issues -> images > 20 + const imagesName = result.map(image => ({ + id: image.id, + file_name: image.file_name, + file_content: Buffer.from(image.file_content, 'base64'), + mime_type: image.mime_type + })); + + return imagesName; + } + } module.exports = Images; From ee5e09698471baee162530e2540ac60a80560f53 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Sat, 22 Feb 2025 18:08:38 -0500 Subject: [PATCH 02/46] added comments --- server/models/images.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/models/images.js b/server/models/images.js index 8713424..68c9380 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -54,6 +54,16 @@ class Images { if (!result) return null; //TODO latency issues -> images > 20 + // USE pagination + /* + app.get('/images', (req, res) => { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + + const images = getImagesFromDatabase(page, limit); + res.json(images); + }); + */ const imagesName = result.map(image => ({ id: image.id, file_name: image.file_name, From e7c3c84b8018622bc3ca40e5e7af2397050dbed1 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Mon, 24 Feb 2025 20:26:30 -0500 Subject: [PATCH 03/46] UC38 - gestion images backend fin --- server/__tests__/image.test.js | 36 ++++++++++++++++++++++++++++++++-- server/controllers/images.js | 11 ++++++----- server/models/images.js | 21 ++++---------------- server/routers/images.js | 1 + 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js index b220e2c..8230b61 100644 --- a/server/__tests__/image.test.js +++ b/server/__tests__/image.test.js @@ -77,7 +77,8 @@ describe('Images', () => { beforeEach(() => { mockImagesCollection = { insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }), - findOne: jest.fn() + findOne: jest.fn(), + find: jest.fn().mockReturnValue({ sort: jest.fn().mockReturnValue([]) }) }; dbConn = { @@ -145,5 +146,36 @@ describe('Images', () => { expect(result).toBeNull(); }); }); -}); + describe('getAll', () => { + it('should retrieve a paginated list of images', async () => { + const mockImages = [ + { id: '1', file_name: 'image1.png', file_content: Buffer.from('data1').toString('base64'), mime_type: 'image/png' }, + { id: '2', file_name: 'image2.png', file_content: Buffer.from('data2').toString('base64'), mime_type: 'image/png' } + ]; + mockImagesCollection.find.mockReturnValue({ sort: jest.fn().mockReturnValue(mockImages) }); + + const result = await images.getAll(1, 10); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(dbConn.collection).toHaveBeenCalledWith('images'); + expect(mockImagesCollection.find).toHaveBeenCalledWith({}); + expect(result.length).toEqual(mockImages.length); + expect(result).toEqual([ + { id: '1', file_name: 'image1.png', file_content: Buffer.from('data1'), mime_type: 'image/png' }, + { id: '2', file_name: 'image2.png', file_content: Buffer.from('data2'), mime_type: 'image/png' } + ]); + }); + + it('should return null if not images is not found', async () => { + mockImagesCollection.find.mockReturnValue({ sort: jest.fn().mockReturnValue(undefined) }); + const result = await images.getAll(1, 10); + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(dbConn.collection).toHaveBeenCalledWith('images'); + expect(mockImagesCollection.find).toHaveBeenCalledWith({}); + expect(result).toEqual(null); + }); + }); +}); \ No newline at end of file diff --git a/server/controllers/images.js b/server/controllers/images.js index ff41d5c..877eb7d 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -50,18 +50,19 @@ class ImagesController { } }; - //TODO TEST - getAll = async (req, res, next) => { + getImages = async (req, res, next) => { try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; - const images = await this.images.getAll(); + const imagesBit = await this.images.getImages(page, limit); - if (!images || images.length === 0) { + if (!imagesBit || imagesBit.length === 0) { throw new AppError(IMAGE_NOT_FOUND); } res.setHeader('Content-Type', 'application/json'); - return res.status(200).json(imagesName); + return res.status(200).json(imagesBit); } catch (error) { return next(error); } diff --git a/server/models/images.js b/server/models/images.js index 68c9380..66d6084 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -42,38 +42,25 @@ class Images { }; } - //TODO TEST - async getAll() { + async getImages(page, limit) { await this.db.connect() const conn = this.db.getConnection(); const imagesCollection = conn.collection('images'); - const result = await imagesCollection.find({}); + const result = await imagesCollection.find({}).sort({created_at: 1}); if (!result) return null; - //TODO latency issues -> images > 20 - // USE pagination - /* - app.get('/images', (req, res) => { - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - - const images = getImagesFromDatabase(page, limit); - res.json(images); - }); - */ - const imagesName = result.map(image => ({ + const objImages = result.slice((page - 1) * limit, page * limit).map(image => ({ id: image.id, file_name: image.file_name, file_content: Buffer.from(image.file_content, 'base64'), mime_type: image.mime_type })); - return imagesName; + return objImages; } - } module.exports = Images; diff --git a/server/routers/images.js b/server/routers/images.js index 06e2830..626e6e8 100644 --- a/server/routers/images.js +++ b/server/routers/images.js @@ -12,5 +12,6 @@ const upload = multer({ storage: storage }); router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(images.upload)); router.get("/get/:id", asyncHandler(images.get)); +router.get("/getImages", asyncHandler(images.getImages)); module.exports = router; From 16d594c61d913ad843b3e96077d3d47f41163f18 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Tue, 25 Feb 2025 17:38:23 -0500 Subject: [PATCH 04/46] FIX erreur encoding --- server/models/images.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/models/images.js b/server/models/images.js index 66d6084..a9e8306 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -48,14 +48,15 @@ class Images { const imagesCollection = conn.collection('images'); - const result = await imagesCollection.find({}).sort({created_at: 1}); + const result = await imagesCollection.find({}).sort({created_at: 1}).toArray(); if (!result) return null; const objImages = result.slice((page - 1) * limit, page * limit).map(image => ({ - id: image.id, + id: image._id, + user: image.userId, file_name: image.file_name, - file_content: Buffer.from(image.file_content, 'base64'), + file_content: image.file_content.toString('base64'), mime_type: image.mime_type })); From 9e5613a011af4d43ecda36b898cdbafc3b2b137a Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:16:06 -0500 Subject: [PATCH 05/46] ShareQuizModal and able to copy quiz URL --- .../ShareQuizModal/ShareQuizModal.tsx | 71 +++++++++++++++++++ .../src/pages/Teacher/Dashboard/Dashboard.tsx | 30 ++------ client/src/pages/Teacher/Share/Share.tsx | 65 ++++++++--------- client/src/pages/Teacher/Share/share.css | 45 +++++++++++- 4 files changed, 148 insertions(+), 63 deletions(-) create mode 100644 client/src/components/ShareQuizModal/ShareQuizModal.tsx diff --git a/client/src/components/ShareQuizModal/ShareQuizModal.tsx b/client/src/components/ShareQuizModal/ShareQuizModal.tsx new file mode 100644 index 0000000..9e25dca --- /dev/null +++ b/client/src/components/ShareQuizModal/ShareQuizModal.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; +import { Dialog, DialogTitle, DialogActions, Button, Tooltip, IconButton } from '@mui/material'; +import { Share } from '@mui/icons-material'; +import { QuizType } from '../../Types/QuizType'; +import ApiService from '../../services/ApiService'; + +interface ShareQuizModalProps { + quiz: QuizType; +} + +const ShareQuizModal: React.FC = ({ quiz }) => { + const [open, setOpen] = useState(false); + + const handleOpenModal = () => setOpen(true); + + const handleCloseModal = () => setOpen(false); + + const handleShareByEmail = async () => { + const email = prompt(`Veuillez saisir l'email de la personne avec qui vous souhaitez partager ce quiz`, ""); + + if (email) { + try { + const result = await ApiService.ShareQuiz(quiz._id, email); + + if (!result) { + window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`); + return; + } + + window.alert(`Quiz partagé avec succès!`); + } catch (error) { + console.error('Erreur lors du partage du quiz:', error); + } + } + + handleCloseModal(); + }; + + const handleShareByUrl = () => { + const quizUrl = `${window.location.origin}/teacher/share/${quiz._id}`; + navigator.clipboard.writeText(quizUrl) + .then(() => { + window.alert('URL copied to clipboard!'); + }) + .catch(() => { + window.alert('Failed to copy URL to clipboard.'); + }); + + handleCloseModal(); + }; + + return ( + <> + + + + + + + + Choisissez une méthode de partage + + + + + + + ); +}; + +export default ShareQuizModal; \ No newline at end of file diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx index f920d12..c8d03bf 100644 --- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx +++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx @@ -36,6 +36,7 @@ import { Share, // DriveFileMove } from '@mui/icons-material'; +import ShareQuizModal from 'src/components/ShareQuizModal/ShareQuizModal'; // Create a custom-styled Card component const CustomCard = styled(Card)({ @@ -342,26 +343,6 @@ const Dashboard: React.FC = () => { navigate(`/teacher/manage-room/${quiz._id}`); } - const handleShareQuiz = async (quiz: QuizType) => { - try { - const email = prompt(`Veuillez saisir l'email de la personne avec qui vous souhaitez partager ce quiz`, ""); - - if (email) { - const result = await ApiService.ShareQuiz(quiz._id, email); - - if (!result) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) - return; - } - - window.alert(`Quiz partagé avec succès!`) - } - - } catch (error) { - console.error('Erreur lors du partage du quiz:', error); - } - } - @@ -510,12 +491,9 @@ const Dashboard: React.FC = () => { > - - handleShareQuiz(quiz)} - > - +
+ +
))} diff --git a/client/src/pages/Teacher/Share/Share.tsx b/client/src/pages/Teacher/Share/Share.tsx index 31bb72c..34bd0a9 100644 --- a/client/src/pages/Teacher/Share/Share.tsx +++ b/client/src/pages/Teacher/Share/Share.tsx @@ -91,42 +91,39 @@ const Share: React.FC = () => { }; return ( -
- -
- - -
Importer quiz: {quizTitle}
- -
+
+
+ +
+
Importation du Quiz: {quizTitle}
+
+ Vous êtes sur le point d'importer le quiz {quizTitle}, choisissez un dossier dans lequel enregistrer ce nouveau quiz.
- -
- -
- - - - - {folders.map((folder: FolderType) => ( - - ))} - - - - -
- -
-
+
+
+ +
+
+ + + {folders.map((folder: FolderType) => ( + + ))} + + + +
+
+
); }; diff --git a/client/src/pages/Teacher/Share/share.css b/client/src/pages/Teacher/Share/share.css index 119b645..146f318 100644 --- a/client/src/pages/Teacher/Share/share.css +++ b/client/src/pages/Teacher/Share/share.css @@ -3,19 +3,58 @@ display: flex; flex-direction: row; justify-content: space-between; - align-content: stretch + align-content: stretch; } + .quizImport .importHeader .returnButton { flex-basis: 10%; - display: flex; justify-content: center; } -.quizImport .importHeader .title { +.quizImport .importHeader .titleContainer { flex-basis: auto; + text-align: center; /* Center the title and subtitle */ +} + +.quizImport .importHeader .mainTitle { + font-size: 44px; /* Larger font size for the main title */ + font-weight: bold; /* Remove bold */ + color: #333; /* Slightly paler color */ +} + +.quizImport .importHeader .subTitle { + font-size: 14px; /* Smaller font size for the subtitle */ + color: #666; /* Pale gray color */ + margin-top: 8px; /* Add some space between the title and subtitle */ } .quizImport .importHeader .dumb { flex-basis: 10%; +} + +.quizImport .editSection { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; +} + +.quizImport .editSection .formContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; /* Adds space between the select and button */ + width: 100%; + max-width: 400px; /* Limits the width of the form container */ +} + +.quizImport .editSection .folderSelect { + width: 100%; /* Ensures the select element takes the full width of its container */ +} + +.quizImport .editSection .saveButton { + width: 100%; /* Makes the button take the full width of its container */ + padding: 10px 20px; /* Increases the button's padding for a larger appearance */ + font-size: 16px; /* Increases the font size for better visibility */ } \ No newline at end of file From 5ba89072e291a94064c5a44a9085f59ea1e6ef23 Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Fri, 28 Feb 2025 11:03:47 -0500 Subject: [PATCH 06/46] modifications dashboard --- client/src/pages/Teacher/Dashboard/Dashboard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx index c8d03bf..01faaa8 100644 --- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx +++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx @@ -33,7 +33,6 @@ import { FolderCopy, ContentCopy, Edit, - Share, // DriveFileMove } from '@mui/icons-material'; import ShareQuizModal from 'src/components/ShareQuizModal/ShareQuizModal'; From 460b4f1648a6c06a440d2969643d57b3842e0432 Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Sun, 2 Mar 2025 20:13:44 -0500 Subject: [PATCH 07/46] ShareQuizModal tests --- client/package-lock.json | 208 +++++++----------- client/package.json | 2 +- .../ShareQuizModal/ShareQuizModal.test.tsx | 74 +++++++ .../ShareQuizModal/ShareQuizModal.tsx | 2 +- 4 files changed, 150 insertions(+), 136 deletions(-) create mode 100644 client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx diff --git a/client/package-lock.json b/client/package-lock.json index 3c24ffc..f930271 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -42,7 +42,7 @@ "@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/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@types/jest": "^29.5.13", "@types/node": "^22.5.5", @@ -2549,7 +2549,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -2568,7 +2568,7 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -2578,7 +2578,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.5", @@ -2593,7 +2593,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2604,7 +2604,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2617,7 +2617,7 @@ "version": "0.10.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -2630,7 +2630,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -2654,7 +2654,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2665,7 +2665,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -2678,7 +2678,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2691,7 +2691,7 @@ "version": "9.18.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2701,7 +2701,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2711,7 +2711,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.10.0", @@ -2818,7 +2818,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -2828,7 +2828,7 @@ "version": "0.16.6", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", @@ -2842,7 +2842,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -2856,7 +2856,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -3859,7 +3859,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3873,7 +3872,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3887,7 +3885,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3901,7 +3898,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3915,7 +3911,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3929,7 +3924,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3943,7 +3937,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3957,7 +3950,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3971,7 +3963,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3985,7 +3976,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3999,7 +3989,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4013,7 +4002,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4027,7 +4015,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4041,7 +4028,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4055,7 +4041,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4069,7 +4054,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4083,7 +4067,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4097,7 +4080,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4138,7 +4120,7 @@ "version": "1.7.40", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.40.tgz", "integrity": "sha512-0HIzM5vigVT5IvNum+pPuST9p8xFhN6mhdIKju7qYYeNuZG78lwms/2d8WgjTJJlzp6JlPguXGrMMNzjQw0qNg==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4180,7 +4162,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4197,7 +4178,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4214,7 +4194,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4231,7 +4210,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4248,7 +4226,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4265,7 +4242,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4282,7 +4258,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4299,7 +4274,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4316,7 +4290,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4333,7 +4306,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4347,14 +4319,14 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@swc/types": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz", "integrity": "sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -4381,11 +4353,10 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz", - "integrity": "sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, - "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", @@ -4548,7 +4519,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, "license": "MIT" }, "node_modules/@types/graceful-fs": { @@ -4648,7 +4618,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/katex": { @@ -5029,7 +4999,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -5063,7 +5033,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5138,7 +5108,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, + "devOptional": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -5952,7 +5922,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -6123,7 +6093,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/deepmerge": { @@ -6673,7 +6643,7 @@ "version": "9.18.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -6831,7 +6801,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -6848,7 +6818,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -6861,7 +6831,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -6875,7 +6845,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -6886,7 +6856,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6899,7 +6869,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6912,7 +6882,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", @@ -6930,7 +6900,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6956,7 +6926,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -6969,7 +6939,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -7062,7 +7032,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -7097,14 +7067,14 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fastq": { @@ -7130,7 +7100,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" @@ -7184,7 +7154,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -7201,7 +7171,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -7215,7 +7185,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/follow-redirects": { @@ -7480,7 +7450,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -7768,7 +7738,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -7814,7 +7784,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -8317,7 +8287,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -9355,7 +9325,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9425,7 +9395,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -9438,14 +9408,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json5": { @@ -9517,7 +9487,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -9547,7 +9517,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -9567,7 +9537,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -9604,7 +9574,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/longest-streak": { @@ -10381,7 +10351,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/node-int64": { @@ -10561,7 +10531,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -10597,7 +10567,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -10613,7 +10583,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -10681,7 +10651,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -10843,7 +10813,6 @@ "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -10872,7 +10841,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -10891,7 +10859,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -11380,7 +11348,6 @@ "version": "4.24.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.3.tgz", "integrity": "sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.6" @@ -11582,7 +11549,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -11595,7 +11562,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11741,7 +11708,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -11972,7 +11938,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -12240,7 +12206,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -12352,7 +12318,6 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12586,7 +12551,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -12675,7 +12640,6 @@ "version": "5.4.14", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", - "dev": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -12813,7 +12777,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12830,7 +12793,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12847,7 +12809,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12864,7 +12825,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12881,7 +12841,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12898,7 +12857,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12915,7 +12873,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12932,7 +12889,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12949,7 +12905,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12966,7 +12921,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12983,7 +12937,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13000,7 +12953,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13017,7 +12969,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13034,7 +12985,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13051,7 +13001,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13068,7 +13017,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13085,7 +13033,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13102,7 +13049,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13119,7 +13065,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13136,7 +13081,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13153,7 +13097,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13170,7 +13113,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13187,7 +13129,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13201,7 +13142,6 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -13411,7 +13351,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -13515,7 +13455,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13672,7 +13612,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/client/package.json b/client/package.json index 2a99173..51f6237 100644 --- a/client/package.json +++ b/client/package.json @@ -46,7 +46,7 @@ "@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/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@types/jest": "^29.5.13", "@types/node": "^22.5.5", diff --git a/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx b/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx new file mode 100644 index 0000000..7f7812c --- /dev/null +++ b/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ShareQuizModal from '../../../components/ShareQuizModal/ShareQuizModal'; +import { QuizType } from '../../../Types/QuizType'; +import ApiService from '../../../services/ApiService'; + +jest.mock('../../../services/ApiService'); + +Object.assign(navigator, { + clipboard: { + writeText: jest.fn().mockResolvedValue(undefined), + }, +}); + +window.alert = jest.fn(); + +const mockQuiz: QuizType = { + _id: '1', + title: 'Sample Quiz', + content: ['::Question 1:: What is 2+2? {=4 ~3 ~5}'], + folderId: 'folder1', + folderName: 'Sample Folder', + userId: 'user1', + created_at: new Date(), + updated_at: new Date(), +}; + +describe('ShareQuizModal', () => { + + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call ApiService.ShareQuiz when sharing by email', async () => { + render(); + + const shareButton = screen.getByRole('button', { name: /partager quiz/i }); + fireEvent.click(shareButton); + + const emailButton = screen.getByRole('button', { name: /partager par email/i }); + fireEvent.click(emailButton); + + const email = 'test@example.com'; + window.prompt = jest.fn().mockReturnValue(email); + + await fireEvent.click(emailButton); + + expect(ApiService.ShareQuiz).toHaveBeenCalledWith(mockQuiz._id, email); + }); + + it('copies the correct URL to the clipboard when sharing by URL', async () => { + render(); + + // Open the modal + const shareButton = screen.getByRole('button', { name: /partager quiz/i }); + fireEvent.click(shareButton); + + // Click the "Share by URL" button + const shareByUrlButton = screen.getByRole('button', { name: /partager par url/i }); + fireEvent.click(shareByUrlButton); + + // Check if the correct URL was copied + const expectedUrl = `${window.location.origin}/teacher/share/${mockQuiz._id}`; + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expectedUrl); + + // Check if the alert is shown + await waitFor(() => { + expect(window.alert).toHaveBeenCalledWith('URL copied to clipboard!'); + }); + }); + +}); \ No newline at end of file diff --git a/client/src/components/ShareQuizModal/ShareQuizModal.tsx b/client/src/components/ShareQuizModal/ShareQuizModal.tsx index 9e25dca..7bd5dd5 100644 --- a/client/src/components/ShareQuizModal/ShareQuizModal.tsx +++ b/client/src/components/ShareQuizModal/ShareQuizModal.tsx @@ -52,7 +52,7 @@ const ShareQuizModal: React.FC = ({ quiz }) => { return ( <> - + From c9d65c00820586a9480c0461108765a5d004742f Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Sun, 2 Mar 2025 20:52:08 -0500 Subject: [PATCH 08/46] ajout front images gallery --- client/src/Types/Images.tsx | 11 +++ .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 67 ++++++++++++++++++- client/src/services/ApiService.tsx | 47 ++++++++++++- server/controllers/users.js | 7 +- server/models/images.js | 9 ++- 5 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 client/src/Types/Images.tsx diff --git a/client/src/Types/Images.tsx b/client/src/Types/Images.tsx new file mode 100644 index 0000000..84e1e5a --- /dev/null +++ b/client/src/Types/Images.tsx @@ -0,0 +1,11 @@ +export interface Images { + id: string; + file_content: string; + file_name: string; + mime_type: string; +} + +export interface ImagesResponse { + images: Images[]; + total: number; +} diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index f2c2d69..51ad681 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -16,7 +16,8 @@ import ReturnButton from 'src/components/ReturnButton/ReturnButton'; import ApiService from '../../../services/ApiService'; import { escapeForGIFT } from '../../../utils/giftUtils'; -import { Upload } from '@mui/icons-material'; +import { Upload, ImageSearch } from '@mui/icons-material'; +import { Images } from '../../../Types/Images'; interface EditQuizParams { id: string; @@ -40,7 +41,12 @@ const QuizForm: React.FC = () => { }; const fileInputRef = useRef(null); const [dialogOpen, setDialogOpen] = useState(false); + const [galleryOpen, setGalleryOpen] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false); + const [images, setImages] = useState([]); + const [totalImg, setTotalImg] = useState(0); + const [imgPage, setImgPage] = useState(1); + const [imgLimit] = useState(5); const scrollToTop = () => { window.scrollTo({ top: 0, behavior: 'smooth' }); @@ -69,9 +75,19 @@ const QuizForm: React.FC = () => { } }; + const fetchImages = async (page: number , limit: number) => { + const data = await ApiService.getImages(page, limit); + const imgs = data.images; + const total = data.total; + + setImages(imgs as Images[]); + setTotalImg(total); + } + useEffect(() => { const fetchData = async () => { const userFolders = await ApiService.getUserFolders(); + fetchImages(1, imgLimit); setFolders(userFolders as FolderType[]); }; @@ -205,6 +221,13 @@ const QuizForm: React.FC = () => { navigator.clipboard.writeText(link); } + const handleMoreImages = async () => { + let page = imgPage; + page += 1; + setImgPage(page); + fetchImages(imgPage, imgLimit); + } + return (
@@ -290,6 +313,48 @@ const QuizForm: React.FC = () => {

Mes images :

+ + + + setDialogOpen(false)} > + Images disponibles + + +
+ {images.map((obj: Images, index) => ( +
+ {`Image + {`lien: ${obj.id}`} +
+ ))} +
+
+ + { + totalImg > 10 ? + + : + + + } + +
(Voir section
diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index ef124b4..22189e5 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -1,6 +1,7 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; import { FolderType } from 'src/Types/FolderType'; +import { ImagesResponse } from '../Types/Images'; import { QuizType } from 'src/Types/QuizType'; import { ENV_VARIABLES } from 'src/constants'; @@ -65,6 +66,16 @@ class ApiService { return object.token; } + private getUserID(): string | null { + const objStr = localStorage.getItem("uid"); + + if (!objStr) { + return null; + } + + return objStr; + } + public isLoggedIn(): boolean { const token = this.getToken() @@ -141,7 +152,8 @@ class ApiService { throw new Error(`La connexion a échoué. Status: ${result.status}`); } - this.saveToken(result.data.token); + this.saveToken(result.data.result.token); + localStorage.setItem("uid", JSON.stringify(result.data.result.userId)); return true; @@ -886,7 +898,38 @@ class ApiService { return `ERROR : Une erreur inattendue s'est produite.` } } - // NOTE : Get Image pas necessaire + + + public async getImages(page: number, limit: number): Promise { + try { + const url: string = this.constructRequestUrl(`/image/getImages`); + const headers = this.constructRequestHeaders(); + const params = { page: page, limit: limit}; + + const result: AxiosResponse = await axios.get(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`L'enregistrement a échoué. Status: ${result.status}`); + } + + console.log(result.data); + const images = result.data; + + return images; + + } catch (error) { + console.log("Error details: ", error); + + 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.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } } diff --git a/server/controllers/users.js b/server/controllers/users.js index c6b5dab..57ebeeb 100644 --- a/server/controllers/users.js +++ b/server/controllers/users.js @@ -54,7 +54,12 @@ class UsersController { const token = jwt.create(user.email, user._id); - return res.status(200).json({ token }); + let result = { + token: token, + userId: user._id + } + + return res.status(200).json({ result }); } catch (error) { next(error); } diff --git a/server/models/images.js b/server/models/images.js index a9e8306..b631e0e 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -52,6 +52,8 @@ class Images { if (!result) return null; + const total = result.length; + const objImages = result.slice((page - 1) * limit, page * limit).map(image => ({ id: image._id, user: image.userId, @@ -60,7 +62,12 @@ class Images { mime_type: image.mime_type })); - return objImages; + let respObj = { + images: objImages, + total: total + } + + return respObj; } } From 0f87bab5fff9863bb771402adf503acc54633dbe Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Tue, 4 Mar 2025 19:49:18 -0500 Subject: [PATCH 09/46] FIX dialog front et perf pagination --- .../components/ImageGallery/ImageGallery.tsx | 166 ++++++++++++++++++ .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 70 ++------ server/controllers/images.js | 2 +- server/models/images.js | 14 +- 4 files changed, 191 insertions(+), 61 deletions(-) create mode 100644 client/src/components/ImageGallery/ImageGallery.tsx diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx new file mode 100644 index 0000000..be4a67a --- /dev/null +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -0,0 +1,166 @@ +import React, { useState, useEffect } from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + IconButton, + Paper, + TextField, + Box +} from "@mui/material"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import CloseIcon from "@mui/icons-material/Close"; +import DeleteIcon from "@mui/icons-material/Delete"; +import EditIcon from "@mui/icons-material/Edit"; +import { Images } from "../../Types/Images"; +import ApiService from '../../services/ApiService'; + +type Props = { + galleryOpen: boolean; + admin: boolean; + setDialogOpen: React.Dispatch>; + setImageLinks: React.Dispatch>; +} + +const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setImageLinks }) => { + + const [copiedId, setCopiedId] = useState(null); + const [deleteId, setDeleteId] = useState(null); + /* const [editedFileNames, setEditedFileNames] = useState<{ [key: string]: string }>( + Object.fromEntries(images.map((img) => [img.id, img.file_name])) + ); + */ + const [editingId, setEditingId] = useState(null); + const [images, setImages] = useState([]); + const [totalImg, setTotalImg] = useState(0); + const [imgPage, setImgPage] = useState(1); + const [imgLimit] = useState(3); + + const fetchImages = async (page: number, limit: number) => { + const data = await ApiService.getImages(page, limit); + console.log(data); + setImages(data.images); + setTotalImg(data.total); + }; + + useEffect(() => { + fetchImages(imgPage, imgLimit); + }, [imgPage]); // Re-fetch images when page changes + + const handleDeleteClick = (id: string) => { + setDeleteId(id); + }; + + const handleEditClick = (id: string) => { + setEditingId(id === editingId ? null : id); + }; + + const handleFileNameChange = (id: string, newFileName: string) => { + //setEditedFileNames((prev) => ({ ...prev, [id]: newFileName })); + }; + + const onCopy = (id: string) => { + setCopiedId(id); + setImageLinks(prevLinks => [...prevLinks, id]); + }; + + const handleNextPage = () => { + if ((imgPage * imgLimit) < totalImg) { + setImgPage(prev => prev + 1); + } + }; + + const handlePrevPage = () => { + if (imgPage > 1) { + setImgPage(prev => prev - 1); + } + }; + + return ( + setDialogOpen(false)} + maxWidth="xl" // 'md' stands for medium size + > + + Images disponibles + setDialogOpen(false)} + style={{ position: "absolute", right: 8, top: 8 }} + > + + + + + + + + {images.map((obj: Images) => ( + + + {`Image + + + {admin && editingId === obj.id ? ( + handleFileNameChange(obj.id, e.target.value)} + variant="outlined" + size="small" + style={{ maxWidth: 150 }} + /> + ) : ( + obj.file_name + )} + + + {obj.id} + onCopy(obj.id)} size="small"> + + + {admin && ( + <> + handleEditClick(obj.id)} size="small" color="primary"> + + + handleDeleteClick(obj.id)} size="small" color="primary"> + + + + )} + {copiedId === obj.id && Copié!} + + + ))} + +
+
+
+ + + + + + +
+ ); +}; + +export default ImageDialog; diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index 51ad681..bafb1d9 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -13,11 +13,11 @@ 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 ImageGallery from 'src/components/ImageGallery/ImageGallery'; import ApiService from '../../../services/ApiService'; import { escapeForGIFT } from '../../../utils/giftUtils'; import { Upload, ImageSearch } from '@mui/icons-material'; -import { Images } from '../../../Types/Images'; interface EditQuizParams { id: string; @@ -43,10 +43,6 @@ const QuizForm: React.FC = () => { const [dialogOpen, setDialogOpen] = useState(false); const [galleryOpen, setGalleryOpen] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false); - const [images, setImages] = useState([]); - const [totalImg, setTotalImg] = useState(0); - const [imgPage, setImgPage] = useState(1); - const [imgLimit] = useState(5); const scrollToTop = () => { window.scrollTo({ top: 0, behavior: 'smooth' }); @@ -75,19 +71,9 @@ const QuizForm: React.FC = () => { } }; - const fetchImages = async (page: number , limit: number) => { - const data = await ApiService.getImages(page, limit); - const imgs = data.images; - const total = data.total; - - setImages(imgs as Images[]); - setTotalImg(total); - } - useEffect(() => { const fetchData = async () => { const userFolders = await ApiService.getUserFolders(); - fetchImages(1, imgLimit); setFolders(userFolders as FolderType[]); }; @@ -220,13 +206,13 @@ const QuizForm: React.FC = () => { const handleCopyToClipboard = async (link: string) => { navigator.clipboard.writeText(link); } + + const handleCopy = (imgId: string) => { + setImageLinks(prevLinks => [...prevLinks, imgId]); + console.log(imgId); + }; - const handleMoreImages = async () => { - let page = imgPage; - page += 1; - setImgPage(page); - fetchImages(imgPage, imgLimit); - } + return (
@@ -321,40 +307,14 @@ const QuizForm: React.FC = () => { Images - setDialogOpen(false)} > - Images disponibles - - -
- {images.map((obj: Images, index) => ( -
- {`Image - {`lien: ${obj.id}`} -
- ))} -
-
- - { - totalImg > 10 ? - - : - - - } - -
+ + +
(Voir section
diff --git a/server/controllers/images.js b/server/controllers/images.js index 877eb7d..c5675d4 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -53,7 +53,7 @@ class ImagesController { getImages = async (req, res, next) => { try { const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; + const limit = parseInt(req.query.limit) || 5; const imagesBit = await this.images.getImages(page, limit); diff --git a/server/models/images.js b/server/models/images.js index b631e0e..13d6ad0 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -48,13 +48,17 @@ class Images { const imagesCollection = conn.collection('images'); - const result = await imagesCollection.find({}).sort({created_at: 1}).toArray(); + + const total = await imagesCollection.countDocuments(); // Efficient total count + if (!total || total === 0) return { images: [], total }; - if (!result) return null; + const result = await imagesCollection.find({}) + .sort({ created_at: 1 }) // Ensure 'created_at' is indexed + .skip((page - 1) * limit) + .limit(limit) + .toArray(); - const total = result.length; - - const objImages = result.slice((page - 1) * limit, page * limit).map(image => ({ + const objImages = result.map(image => ({ id: image._id, user: image.userId, file_name: image.file_name, From f8357883e630104dd332d9914b9c5e3283bcc35b Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Tue, 4 Mar 2025 19:59:05 -0500 Subject: [PATCH 10/46] FIX image render --- client/src/components/ImageGallery/ImageGallery.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index be4a67a..6b4301b 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -67,8 +67,9 @@ const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setIm }; const onCopy = (id: string) => { + const escLink = 'http://localhost:4400/api/image/get/'+id; setCopiedId(id); - setImageLinks(prevLinks => [...prevLinks, id]); + setImageLinks(prevLinks => [...prevLinks, escLink]); }; const handleNextPage = () => { From f055a47305e38334ee76b035ae8b1dc33d81cb81 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 6 Mar 2025 20:33:57 -0500 Subject: [PATCH 11/46] ajout test imagegallery --- .../ImageGallery/ImageGallery.test.tsx | 130 ++++++++++++++++++ .../components/ImageGallery/ImageGallery.tsx | 23 +--- .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 7 - client/src/services/ApiService.tsx | 10 -- 4 files changed, 132 insertions(+), 38 deletions(-) create mode 100644 client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx diff --git a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx new file mode 100644 index 0000000..9604b35 --- /dev/null +++ b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx @@ -0,0 +1,130 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import ImageDialog from "../../../components/ImageGallery/ImageGallery"; +import ApiService from "../../../services/ApiService"; +import { Images } from "../../../Types/Images"; +import { act } from "react"; +import "@testing-library/jest-dom"; + +// Mock ApiService +jest.mock("../../../services/ApiService"); + +const mockImages: Images[] = [ + { id: "1", file_name: "image1.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content1" }, + { id: "2", file_name: "image2.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content2" }, + { id: "3", file_name: "image3.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content3" }, +]; + +describe("ImageDialog Component", () => { + let setDialogOpenMock: jest.Mock; + let setImageLinksMock: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + setDialogOpenMock = jest.fn(); + setImageLinksMock = jest.fn(); + jest.spyOn(ApiService, "getImages").mockResolvedValue({ images: mockImages, total: 6 }); + }); + + test("renders the dialog when open", async () => { + + await act(async () => { + render( + + ); + }); + + expect(screen.getByText("Images disponibles")).toBeInTheDocument(); + await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3)); + expect(screen.getAllByRole("img")).toHaveLength(mockImages.length); + }); + + test("closes the dialog when close button is clicked", async () => { + + await act(async () => { + render( + + ); + }); + + fireEvent.click(screen.getByLabelText("close")); + expect(setDialogOpenMock).toHaveBeenCalledWith(false); + }); + + test("copies the image link when copy button is clicked", async () => { + + //const setImageLinksMock = jest.fn(); + await act(async () => { + render( + + ); + }); + + await act(async () => { + await waitFor(() => expect(screen.getAllByRole("img")).toHaveLength(mockImages.length)); + }); + + // Click the copy button + fireEvent.click(screen.getByTestId("copy-button-1")); + // Check that "Copié!" appears + expect(screen.getByText("Copié!")).toBeInTheDocument(); + }); + + test("shows edit field when admin clicks edit button", async () => { + await act(async () => { + render( + + ); + }); + + await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled()); + + const editButton = screen.getByTestId("edit-button-1"); + fireEvent.click(editButton); + + expect(screen.getByDisplayValue("image1.jpg")).toBeInTheDocument(); + }); + + test("navigates to next and previous page", async () => { + await act(async () => { + render( + + ); + }); + + await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3)); + + fireEvent.click(screen.getByText("Suivant")); + + await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(2, 3)); + + fireEvent.click(screen.getByText("Précédent")); + + await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3)); + }); +}); \ No newline at end of file diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index 6b4301b..53b181b 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -17,7 +17,6 @@ import { } from "@mui/material"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import CloseIcon from "@mui/icons-material/Close"; -import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import { Images } from "../../Types/Images"; import ApiService from '../../services/ApiService'; @@ -32,11 +31,6 @@ type Props = { const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setImageLinks }) => { const [copiedId, setCopiedId] = useState(null); - const [deleteId, setDeleteId] = useState(null); - /* const [editedFileNames, setEditedFileNames] = useState<{ [key: string]: string }>( - Object.fromEntries(images.map((img) => [img.id, img.file_name])) - ); - */ const [editingId, setEditingId] = useState(null); const [images, setImages] = useState([]); const [totalImg, setTotalImg] = useState(0); @@ -45,7 +39,6 @@ const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setIm const fetchImages = async (page: number, limit: number) => { const data = await ApiService.getImages(page, limit); - console.log(data); setImages(data.images); setTotalImg(data.total); }; @@ -54,18 +47,10 @@ const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setIm fetchImages(imgPage, imgLimit); }, [imgPage]); // Re-fetch images when page changes - const handleDeleteClick = (id: string) => { - setDeleteId(id); - }; - const handleEditClick = (id: string) => { setEditingId(id === editingId ? null : id); }; - const handleFileNameChange = (id: string, newFileName: string) => { - //setEditedFileNames((prev) => ({ ...prev, [id]: newFileName })); - }; - const onCopy = (id: string) => { const escLink = 'http://localhost:4400/api/image/get/'+id; setCopiedId(id); @@ -118,7 +103,6 @@ const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setIm {admin && editingId === obj.id ? ( handleFileNameChange(obj.id, e.target.value)} variant="outlined" size="small" style={{ maxWidth: 150 }} @@ -129,17 +113,14 @@ const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setIm {obj.id} - onCopy(obj.id)} size="small"> + onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}> {admin && ( <> - handleEditClick(obj.id)} size="small" color="primary"> + handleEditClick(obj.id)} size="small" color="primary" data-testid={`edit-button-${obj.id}`}> - handleDeleteClick(obj.id)} size="small" color="primary"> - - )} {copiedId === obj.id && Copié!} diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index bafb1d9..6231892 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -206,13 +206,6 @@ const QuizForm: React.FC = () => { const handleCopyToClipboard = async (link: string) => { navigator.clipboard.writeText(link); } - - const handleCopy = (imgId: string) => { - setImageLinks(prevLinks => [...prevLinks, imgId]); - console.log(imgId); - }; - - return (
diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index 22189e5..116765f 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -66,16 +66,6 @@ class ApiService { return object.token; } - private getUserID(): string | null { - const objStr = localStorage.getItem("uid"); - - if (!objStr) { - return null; - } - - return objStr; - } - public isLoggedIn(): boolean { const token = this.getToken() From add7d9954b8498fe677459f878939e1e6f41dcf0 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Mon, 10 Mar 2025 17:52:51 -0400 Subject: [PATCH 12/46] update package,json --- client/package-lock.json | 214 ++++++++++++++++----------------------- client/package.json | 1 + 2 files changed, 86 insertions(+), 129 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 3c24ffc..0031e88 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -44,6 +44,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.6.1", "@types/jest": "^29.5.13", "@types/node": "^22.5.5", "@types/react": "^18.2.15", @@ -2549,7 +2550,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -2568,7 +2569,7 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -2578,7 +2579,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.5", @@ -2593,7 +2594,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2604,7 +2605,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2617,7 +2618,7 @@ "version": "0.10.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -2630,7 +2631,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -2654,7 +2655,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2665,7 +2666,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -2678,7 +2679,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2691,7 +2692,7 @@ "version": "9.18.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2701,7 +2702,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2711,7 +2712,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.10.0", @@ -2818,7 +2819,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -2828,7 +2829,7 @@ "version": "0.16.6", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", @@ -2842,7 +2843,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -2856,7 +2857,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -3859,7 +3860,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3873,7 +3873,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3887,7 +3886,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3901,7 +3899,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3915,7 +3912,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3929,7 +3925,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3943,7 +3938,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3957,7 +3951,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3971,7 +3964,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3985,7 +3977,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3999,7 +3990,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4013,7 +4003,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4027,7 +4016,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4041,7 +4029,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4055,7 +4042,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4069,7 +4055,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4083,7 +4068,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4097,7 +4081,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4138,7 +4121,7 @@ "version": "1.7.40", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.40.tgz", "integrity": "sha512-0HIzM5vigVT5IvNum+pPuST9p8xFhN6mhdIKju7qYYeNuZG78lwms/2d8WgjTJJlzp6JlPguXGrMMNzjQw0qNg==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4180,7 +4163,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4197,7 +4179,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4214,7 +4195,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4231,7 +4211,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4248,7 +4227,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4265,7 +4243,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4282,7 +4259,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4299,7 +4275,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4316,7 +4291,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4333,7 +4307,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4347,14 +4320,14 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@swc/types": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz", "integrity": "sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -4450,6 +4423,20 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4548,7 +4535,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, "license": "MIT" }, "node_modules/@types/graceful-fs": { @@ -4648,7 +4634,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/katex": { @@ -5029,7 +5015,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -5063,7 +5049,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5138,7 +5124,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, + "devOptional": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -5952,7 +5938,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -6123,7 +6109,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/deepmerge": { @@ -6673,7 +6659,7 @@ "version": "9.18.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -6831,7 +6817,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -6848,7 +6834,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -6861,7 +6847,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -6875,7 +6861,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -6886,7 +6872,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6899,7 +6885,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6912,7 +6898,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", @@ -6930,7 +6916,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6956,7 +6942,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -6969,7 +6955,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -7062,7 +7048,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -7097,14 +7083,14 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/fastq": { @@ -7130,7 +7116,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" @@ -7184,7 +7170,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -7201,7 +7187,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -7215,7 +7201,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/follow-redirects": { @@ -7480,7 +7466,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -7768,7 +7754,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -7814,7 +7800,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -8317,7 +8303,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -9355,7 +9341,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9425,7 +9411,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -9438,14 +9424,14 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/json5": { @@ -9517,7 +9503,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -9547,7 +9533,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -9567,7 +9553,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -9604,7 +9590,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/longest-streak": { @@ -10381,7 +10367,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/node-int64": { @@ -10561,7 +10547,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -10597,7 +10583,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -10613,7 +10599,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -10681,7 +10667,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -10843,7 +10829,6 @@ "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -10872,7 +10857,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -10891,7 +10875,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -11380,7 +11364,6 @@ "version": "4.24.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.3.tgz", "integrity": "sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.6" @@ -11582,7 +11565,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -11595,7 +11578,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11741,7 +11724,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -11972,7 +11954,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -12240,7 +12222,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -12352,7 +12334,6 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12586,7 +12567,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -12675,7 +12656,6 @@ "version": "5.4.14", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", - "dev": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -12813,7 +12793,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12830,7 +12809,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12847,7 +12825,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12864,7 +12841,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12881,7 +12857,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12898,7 +12873,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12915,7 +12889,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12932,7 +12905,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12949,7 +12921,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12966,7 +12937,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -12983,7 +12953,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13000,7 +12969,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13017,7 +12985,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13034,7 +13001,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13051,7 +13017,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13068,7 +13033,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13085,7 +13049,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13102,7 +13065,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13119,7 +13081,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13136,7 +13097,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13153,7 +13113,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13170,7 +13129,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13187,7 +13145,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13201,7 +13158,6 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -13411,7 +13367,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -13515,7 +13471,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13672,7 +13628,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/client/package.json b/client/package.json index 2a99173..8fccab2 100644 --- a/client/package.json +++ b/client/package.json @@ -48,6 +48,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.6.1", "@types/jest": "^29.5.13", "@types/node": "^22.5.5", "@types/react": "^18.2.15", From 1b321c74f907bc1696497ee45762ad195efade1c Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Mon, 10 Mar 2025 17:54:50 -0400 Subject: [PATCH 13/46] update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6e8de7b..e0551c4 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ build/Release # Dependency directories node_modules/ jspm_packages/ +mongo-backup/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ From 78f6993547b920a58e8091515480778498053b05 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Mon, 10 Mar 2025 18:49:29 -0400 Subject: [PATCH 14/46] remove user-id --- server/controllers/users.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/server/controllers/users.js b/server/controllers/users.js index 57ebeeb..c6b5dab 100644 --- a/server/controllers/users.js +++ b/server/controllers/users.js @@ -54,12 +54,7 @@ class UsersController { const token = jwt.create(user.email, user._id); - let result = { - token: token, - userId: user._id - } - - return res.status(200).json({ result }); + return res.status(200).json({ token }); } catch (error) { next(error); } From 1512f320c87c510085b6725479043b363650732a Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Mon, 10 Mar 2025 19:03:58 -0400 Subject: [PATCH 15/46] tests fix --- server/__tests__/image.test.js | 60 +++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js index 8230b61..1a56431 100644 --- a/server/__tests__/image.test.js +++ b/server/__tests__/image.test.js @@ -73,12 +73,22 @@ describe('Images', () => { let images; let dbConn; let mockImagesCollection; + let mockFindCursor; beforeEach(() => { + + mockFindCursor = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn(), + }; + mockImagesCollection = { insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }), findOne: jest.fn(), - find: jest.fn().mockReturnValue({ sort: jest.fn().mockReturnValue([]) }) + find: jest.fn().mockReturnValue(mockFindCursor), + countDocuments: jest.fn() }; dbConn = { @@ -147,35 +157,41 @@ describe('Images', () => { }); }); - describe('getAll', () => { + describe('getImages', () => { it('should retrieve a paginated list of images', async () => { const mockImages = [ - { id: '1', file_name: 'image1.png', file_content: Buffer.from('data1').toString('base64'), mime_type: 'image/png' }, - { id: '2', file_name: 'image2.png', file_content: Buffer.from('data2').toString('base64'), mime_type: 'image/png' } + { _id: '1', userId: 'user1', file_name: 'image1.png', file_content: Buffer.from('data1'), mime_type: 'image/png' }, + { _id: '2', userId: 'user2', file_name: 'image2.png', file_content: Buffer.from('data2'), mime_type: 'image/png' } ]; - mockImagesCollection.find.mockReturnValue({ sort: jest.fn().mockReturnValue(mockImages) }); - - const result = await images.getAll(1, 10); - + + mockImagesCollection.countDocuments.mockResolvedValue(2); + mockFindCursor.toArray.mockResolvedValue(mockImages); + + const result = await images.getImages(1, 10); + expect(db.connect).toHaveBeenCalled(); expect(db.getConnection).toHaveBeenCalled(); expect(dbConn.collection).toHaveBeenCalledWith('images'); expect(mockImagesCollection.find).toHaveBeenCalledWith({}); - expect(result.length).toEqual(mockImages.length); - expect(result).toEqual([ - { id: '1', file_name: 'image1.png', file_content: Buffer.from('data1'), mime_type: 'image/png' }, - { id: '2', file_name: 'image2.png', file_content: Buffer.from('data2'), mime_type: 'image/png' } - ]); + expect(mockFindCursor.sort).toHaveBeenCalledWith({ created_at: 1 }); + expect(mockFindCursor.skip).toHaveBeenCalledWith(0); + expect(mockFindCursor.limit).toHaveBeenCalledWith(10); + expect(result).toEqual({ + images: [ + { id: '1', user: 'user1', file_name: 'image1.png', file_content: 'ZGF0YTE=', mime_type: 'image/png' }, + { id: '2', user: 'user2', file_name: 'image2.png', file_content: 'ZGF0YTI=', mime_type: 'image/png' } + ], + total: 2, + }); }); - - it('should return null if not images is not found', async () => { - mockImagesCollection.find.mockReturnValue({ sort: jest.fn().mockReturnValue(undefined) }); - const result = await images.getAll(1, 10); - expect(db.connect).toHaveBeenCalled(); - expect(db.getConnection).toHaveBeenCalled(); - expect(dbConn.collection).toHaveBeenCalledWith('images'); - expect(mockImagesCollection.find).toHaveBeenCalledWith({}); - expect(result).toEqual(null); + + it('should return an empty array if no images are found', async () => { + mockImagesCollection.countDocuments.mockResolvedValue(0); + mockFindCursor.toArray.mockResolvedValue([]); + + const result = await images.getImages(1, 10); + + expect(result).toEqual({ images: [], total: 0 }); }); }); }); \ No newline at end of file From de5f8fad6cb58074a78b921b4196b3f3d5220eb1 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Tue, 11 Mar 2025 19:50:28 -0400 Subject: [PATCH 16/46] FIX - url DEV et PROD - images par prof --- client/src/Types/Images.tsx | 6 ++ .../components/ImageGallery/ImageGallery.tsx | 4 +- client/src/constants.tsx | 1 + client/src/services/ApiService.tsx | 56 +++++++++++++++++-- docker-compose.yaml | 1 + server/controllers/images.js | 25 +++++++-- server/models/images.js | 33 ++++++++++- server/routers/images.js | 1 + 8 files changed, 115 insertions(+), 12 deletions(-) diff --git a/client/src/Types/Images.tsx b/client/src/Types/Images.tsx index 84e1e5a..8cfe170 100644 --- a/client/src/Types/Images.tsx +++ b/client/src/Types/Images.tsx @@ -9,3 +9,9 @@ export interface ImagesResponse { images: Images[]; total: number; } + +export interface ImagesParams { + page: number; + limit: number; + uid?: string; +} \ No newline at end of file diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index 53b181b..7184cda 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -20,6 +20,7 @@ import CloseIcon from "@mui/icons-material/Close"; import EditIcon from "@mui/icons-material/Edit"; import { Images } from "../../Types/Images"; import ApiService from '../../services/ApiService'; +import { ENV_VARIABLES } from '../constants'; type Props = { galleryOpen: boolean; @@ -52,7 +53,8 @@ const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setIm }; const onCopy = (id: string) => { - const escLink = 'http://localhost:4400/api/image/get/'+id; + const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; + console.log(escLink); setCopiedId(id); setImageLinks(prevLinks => [...prevLinks, escLink]); }; diff --git a/client/src/constants.tsx b/client/src/constants.tsx index ad5b80b..fcdd278 100644 --- a/client/src/constants.tsx +++ b/client/src/constants.tsx @@ -2,6 +2,7 @@ const ENV_VARIABLES = { MODE: process.env.MODE || "production", VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", + IMG_URL: process.env.MODE == "development" ? process.env.VITE_BACKEND_URL : process.env.IMG_URL, BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '', FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : '' }; diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index fe573eb..0984652 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -3,7 +3,7 @@ import { jwtDecode } from 'jwt-decode'; import { ENV_VARIABLES } from '../constants'; import { FolderType } from 'src/Types/FolderType'; -import { ImagesResponse } from '../Types/Images'; +import { ImagesResponse, ImagesParams } from '../Types/Images'; import { QuizType } from 'src/Types/QuizType'; import { RoomType } from 'src/Types/RoomType'; @@ -143,6 +143,21 @@ class ApiService { return object.username; } + public getUserID(): string { + const objectStr = localStorage.getItem("jwt"); + + if (!objectStr) { + return ""; + } + + const jsonObj = JSON.parse(objectStr); + if (!jsonObj.userId) { + return ""; + } + + return jsonObj.userId; + } + // Route to know if rooms need authentication to join public async getRoomsRequireAuth(): Promise { const url: string = this.constructRequestUrl(`/auth/getRoomsRequireAuth`); @@ -1169,20 +1184,51 @@ public async login(email: string, password: string): Promise { } } - public async getImages(page: number, limit: number): Promise { try { const url: string = this.constructRequestUrl(`/image/getImages`); const headers = this.constructRequestHeaders(); - const params = { page: page, limit: limit}; + let params : ImagesParams = { page: page, limit: limit }; const result: AxiosResponse = await axios.get(url, { params: params, headers: headers }); if (result.status !== 200) { - throw new Error(`L'enregistrement a échoué. Status: ${result.status}`); + throw new Error(`L'affichage des images a échoué. Status: ${result.status}`); + } + const images = result.data; + + return images; + + } catch (error) { + console.log("Error details: ", error); + + 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.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); } - console.log(result.data); + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + + public async getUserImages(page: number, limit: number): Promise { + try { + const url: string = this.constructRequestUrl(`/image/getUserImages`); + const headers = this.constructRequestHeaders(); + let params : ImagesParams = { page: page, limit: limit }; + + const uid = this.getUserID(); + if(uid !== ''){ + params.uid = uid; + } + + const result: AxiosResponse = await axios.get(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`L'affichage des images de l'utilisateur a échoué. Status: ${result.status}`); + } const images = result.data; return images; diff --git a/docker-compose.yaml b/docker-compose.yaml index 539c800..8db680e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,6 +10,7 @@ services: - VITE_BACKEND_URL= # Define empty VITE_BACKEND_SOCKET_URL so it will default to window.location.host - VITE_BACKEND_SOCKET_URL= + - IMG_URL: https://evalsa.etsmtl.ca ports: - "5173:5173" restart: always diff --git a/server/controllers/images.js b/server/controllers/images.js index c5675d4..e0ac12c 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -54,15 +54,32 @@ class ImagesController { try { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 5; + const images = await this.images.getImages(page, limit); - const imagesBit = await this.images.getImages(page, limit); - - if (!imagesBit || imagesBit.length === 0) { + if (!images || images.length === 0) { throw new AppError(IMAGE_NOT_FOUND); } res.setHeader('Content-Type', 'application/json'); - return res.status(200).json(imagesBit); + return res.status(200).json(images); + } catch (error) { + return next(error); + } + }; + + getUserImages = async (req, res, next) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 5; + const uid = req.query.uid; + const images = await this.images.getUserImages(page, limit, uid); + + if (!images || images.length === 0) { + throw new AppError(IMAGE_NOT_FOUND); + } + + res.setHeader('Content-Type', 'application/json'); + return res.status(200).json(images); } catch (error) { return next(error); } diff --git a/server/models/images.js b/server/models/images.js index 13d6ad0..036ce1e 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -49,11 +49,40 @@ class Images { const imagesCollection = conn.collection('images'); - const total = await imagesCollection.countDocuments(); // Efficient total count + const total = await imagesCollection.countDocuments(); if (!total || total === 0) return { images: [], total }; const result = await imagesCollection.find({}) - .sort({ created_at: 1 }) // Ensure 'created_at' is indexed + .sort({ created_at: 1 }) + .skip((page - 1) * limit) + .limit(limit) + .toArray(); + + const objImages = result.map(image => ({ + id: image._id, + user: image.userId, + file_name: image.file_name, + file_content: image.file_content.toString('base64'), + mime_type: image.mime_type + })); + + let respObj = { + images: objImages, + total: total + } + + return respObj; + } + + async getUserImages(page, limit, uid) { + await this.db.connect() + const conn = this.db.getConnection(); + const imagesCollection = conn.collection('images'); + const total = await imagesCollection.countDocuments({ userId: uid }); + if (!total || total === 0) return { images: [], total }; + + const result = await imagesCollection.find({ userId: uid }) + .sort({ created_at: 1 }) .skip((page - 1) * limit) .limit(limit) .toArray(); diff --git a/server/routers/images.js b/server/routers/images.js index 626e6e8..40aa481 100644 --- a/server/routers/images.js +++ b/server/routers/images.js @@ -13,5 +13,6 @@ const upload = multer({ storage: storage }); router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(images.upload)); router.get("/get/:id", asyncHandler(images.get)); router.get("/getImages", asyncHandler(images.getImages)); +router.get("/getUserImages", asyncHandler(images.getUserImages)); module.exports = router; From 9ff28827611e858839c06ccbe6b96250e31343d5 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Tue, 11 Mar 2025 19:53:34 -0400 Subject: [PATCH 17/46] updated path --- client/src/components/ImageGallery/ImageGallery.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index 7184cda..069a846 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -20,7 +20,7 @@ import CloseIcon from "@mui/icons-material/Close"; import EditIcon from "@mui/icons-material/Edit"; import { Images } from "../../Types/Images"; import ApiService from '../../services/ApiService'; -import { ENV_VARIABLES } from '../constants'; +import { ENV_VARIABLES } from '../../constants'; type Props = { galleryOpen: boolean; From 66ce4937d93a8a0c0656de0cba6438e62fcb3677 Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Tue, 11 Mar 2025 22:12:36 -0400 Subject: [PATCH 18/46] error when user uses his own URL to copy a quiz --- .../ShareQuizModal/ShareQuizModal.tsx | 4 +-- client/src/pages/Teacher/Share/Share.tsx | 17 ++++++----- client/src/services/ApiService.tsx | 30 ++++++++++++++----- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/client/src/components/ShareQuizModal/ShareQuizModal.tsx b/client/src/components/ShareQuizModal/ShareQuizModal.tsx index 7bd5dd5..336aadb 100644 --- a/client/src/components/ShareQuizModal/ShareQuizModal.tsx +++ b/client/src/components/ShareQuizModal/ShareQuizModal.tsx @@ -40,10 +40,10 @@ const ShareQuizModal: React.FC = ({ quiz }) => { const quizUrl = `${window.location.origin}/teacher/share/${quiz._id}`; navigator.clipboard.writeText(quizUrl) .then(() => { - window.alert('URL copied to clipboard!'); + window.alert('URL a été copiée avec succès.'); }) .catch(() => { - window.alert('Failed to copy URL to clipboard.'); + window.alert('Une erreur est survenue lors de la copie de l\'URL.'); }); handleCloseModal(); diff --git a/client/src/pages/Teacher/Share/Share.tsx b/client/src/pages/Teacher/Share/Share.tsx index 093787e..613d290 100644 --- a/client/src/pages/Teacher/Share/Share.tsx +++ b/client/src/pages/Teacher/Share/Share.tsx @@ -1,18 +1,12 @@ -// EditorQuiz.tsx import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; - import { FolderType } from '../../../Types/FolderType'; - - import './share.css'; import { Button, NativeSelect } from '@mui/material'; import ReturnButton from 'src/components/ReturnButton/ReturnButton'; - import ApiService from '../../../services/ApiService'; const Share: React.FC = () => { - console.log('Component rendered'); const navigate = useNavigate(); const { id } = useParams(); @@ -23,7 +17,6 @@ const Share: React.FC = () => { useEffect(() => { const fetchData = async () => { - console.log("QUIZID : " + id) if (!id) { window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`) console.error('Quiz not found for id:', id); @@ -36,9 +29,17 @@ const Share: React.FC = () => { navigate("/login"); return; } + + const quizIds = await ApiService.getAllQuizIds(); + + if (quizIds.includes(id)) { + window.alert(`Le quiz que vous essayez d'importer existe déjà sur votre compte.`) + navigate('/teacher/dashboard'); + return; + } const userFolders = await ApiService.getUserFolders(); - + if (userFolders.length == 0) { window.alert(`Vous n'avez aucun dossier.\nVeuillez en créer un et revenir à ce lien`) navigate('/teacher/dashboard'); diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index b13a369..7764555 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -74,8 +74,6 @@ class ApiService { return false; } - console.log("ApiService: isLoggedIn: Token:", token); - // Update token expiry this.saveToken(token); @@ -91,7 +89,6 @@ class ApiService { } try { - console.log("ApiService: isLoggedInTeacher: Token:", token); const decodedToken = jwtDecode(token) as { roles: string[] }; /////// REMOVE BELOW @@ -103,7 +100,6 @@ class ApiService { const userRoles = decodedToken.roles; const requiredRole = 'teacher'; - console.log("ApiService: isLoggedInTeacher: UserRoles:", userRoles); if (!userRoles || !userRoles.includes(requiredRole)) { return false; } @@ -178,7 +174,6 @@ class ApiService { const result: AxiosResponse = await axios.post(url, body, { headers: headers }); - console.log(result); if (result.status == 200) { //window.location.href = result.request.responseURL; window.location.href = '/login'; @@ -190,7 +185,6 @@ class ApiService { return true; } catch (error) { - console.log("Error details: ", error); if (axios.isAxiosError(error)) { const err = error as AxiosError; @@ -553,7 +547,6 @@ public async login(email: string, password: string): Promise { const headers = this.constructRequestHeaders(); const body = { folderId }; - console.log(headers); const result: AxiosResponse = await axios.post(url, body, { headers: headers }); if (result.status !== 200) { @@ -1169,6 +1162,29 @@ public async login(email: string, password: string): Promise { } // NOTE : Get Image pas necessaire + public async getAllQuizIds(): Promise { + try { + const folders = await this.getUserFolders(); + + const allQuizIds: string[] = []; + + for (const folder of folders) { + const folderQuizzes = await this.getFolderContent(folder._id); + + if (Array.isArray(folderQuizzes)) { + allQuizIds.push(...folderQuizzes.map(quiz => quiz._id)); + } + } + + return allQuizIds; + } catch (error) { + console.error('Failed to get all quiz ids:', error); + throw error; + } + } + + + } const apiService = new ApiService(); From 2ca21a7d6b80100d297dce32c01a4c918b58b4b2 Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Tue, 11 Mar 2025 22:16:04 -0400 Subject: [PATCH 19/46] folders id error fixed --- client/src/services/ApiService.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index 7764555..fa2b4e7 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -1168,12 +1168,16 @@ public async login(email: string, password: string): Promise { const allQuizIds: string[] = []; - for (const folder of folders) { - const folderQuizzes = await this.getFolderContent(folder._id); + if (Array.isArray(folders)) { + for (const folder of folders) { + const folderQuizzes = await this.getFolderContent(folder._id); - if (Array.isArray(folderQuizzes)) { - allQuizIds.push(...folderQuizzes.map(quiz => quiz._id)); + if (Array.isArray(folderQuizzes)) { + allQuizIds.push(...folderQuizzes.map(quiz => quiz._id)); + } } + } else { + console.error('Failed to get user folders:', folders); } return allQuizIds; From f0b3d74c3126a207e546fcfa80aefedbdb097d74 Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Tue, 11 Mar 2025 22:23:16 -0400 Subject: [PATCH 20/46] test fixed --- .../__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx b/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx index 7f7812c..2a878a5 100644 --- a/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx +++ b/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx @@ -67,7 +67,7 @@ describe('ShareQuizModal', () => { // Check if the alert is shown await waitFor(() => { - expect(window.alert).toHaveBeenCalledWith('URL copied to clipboard!'); + expect(window.alert).toHaveBeenCalledWith('URL a été copiée avec succès.'); }); }); From 6b654c769c56e9e254c859e799a51120b7c219df Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Tue, 11 Mar 2025 23:05:53 -0400 Subject: [PATCH 21/46] test for ApiService new methods --- .../services/ShareQuizService.test.tsx | 83 +++++ .../services/getAllQuizIdsService.test.tsx | 42 +++ package-lock.json | 329 ++++++++++++++++++ package.json | 5 + 4 files changed, 459 insertions(+) create mode 100644 client/src/__tests__/services/ShareQuizService.test.tsx create mode 100644 client/src/__tests__/services/getAllQuizIdsService.test.tsx create mode 100644 package-lock.json create mode 100644 package.json diff --git a/client/src/__tests__/services/ShareQuizService.test.tsx b/client/src/__tests__/services/ShareQuizService.test.tsx new file mode 100644 index 0000000..c1e473e --- /dev/null +++ b/client/src/__tests__/services/ShareQuizService.test.tsx @@ -0,0 +1,83 @@ +import axios from 'axios'; +import ApiService from '../../services/ApiService'; +import { ENV_VARIABLES } from '../../constants'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +describe('ApiService', () => { + describe('shareQuiz', () => { + it('should call the API to share a quiz and return true on success', async () => { + const quizId = '123'; + const email = 'test@example.com'; + const response = { status: 200 }; + mockedAxios.put.mockResolvedValue(response); + + const result = await ApiService.ShareQuiz(quizId, email); + + expect(mockedAxios.put).toHaveBeenCalledWith( + `${ENV_VARIABLES.VITE_BACKEND_URL}/api/quiz/Share`, + { quizId, email }, + { headers: expect.any(Object) } + ); + expect(result).toBe(true); + }); + + it('should return an error message if the API call fails', async () => { + const quizId = '123'; + const email = 'test@example.com'; + const errorMessage = 'An unexpected error occurred.'; + mockedAxios.put.mockRejectedValue({ response: { data: { error: errorMessage } } }); + + const result = await ApiService.ShareQuiz(quizId, email); + + expect(result).toBe(errorMessage); + }); + + it('should return a generic error message if an unexpected error occurs', async () => { + const quizId = '123'; + const email = 'test@example.com'; + mockedAxios.put.mockRejectedValue(new Error('Unexpected error')); + + const result = await ApiService.ShareQuiz(quizId, email); + + expect(result).toBe('An unexpected error occurred.'); + }); + }); + + describe('getSharedQuiz', () => { + it('should call the API to get a shared quiz and return the quiz data on success', async () => { + const quizId = '123'; + const quizData = 'Quiz data'; + const response = { status: 200, data: { data: quizData } }; + mockedAxios.get.mockResolvedValue(response); + + const result = await ApiService.getSharedQuiz(quizId); + + expect(mockedAxios.get).toHaveBeenCalledWith( + `${ENV_VARIABLES.VITE_BACKEND_URL}/api/quiz/getShare/${quizId}`, + { headers: expect.any(Object) } + ); + expect(result).toBe(quizData); + }); + + it('should return an error message if the API call fails', async () => { + const quizId = '123'; + const errorMessage = 'An unexpected error occurred.'; + mockedAxios.get.mockRejectedValue({ response: { data: { error: errorMessage } } }); + + const result = await ApiService.getSharedQuiz(quizId); + + expect(result).toBe(errorMessage); + }); + + it('should return a generic error message if an unexpected error occurs', async () => { + const quizId = '123'; + mockedAxios.get.mockRejectedValue(new Error('Unexpected error')); + + const result = await ApiService.getSharedQuiz(quizId); + + expect(result).toBe('An unexpected error occurred.'); + }); + }); +}); \ No newline at end of file diff --git a/client/src/__tests__/services/getAllQuizIdsService.test.tsx b/client/src/__tests__/services/getAllQuizIdsService.test.tsx new file mode 100644 index 0000000..222e7db --- /dev/null +++ b/client/src/__tests__/services/getAllQuizIdsService.test.tsx @@ -0,0 +1,42 @@ +import axios from 'axios'; +import ApiService from '../../services/ApiService'; +import { FolderType } from '../../Types/FolderType'; +import { QuizType } from '../../Types/QuizType'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +describe('ApiService', () => { + describe('getAllQuizIds', () => { + it('should return all quiz IDs from all folders', async () => { + const folders: FolderType[] = [ + { _id: 'folder1', title: 'Folder 1', userId: 'user1', created_at: new Date().toISOString() }, + { _id: 'folder2', title: 'Folder 2', userId: 'user2', created_at: new Date().toISOString() } + ]; + const quizzesFolder1: QuizType[] = [ + { _id: 'quiz1', title: 'Quiz 1', content: [], folderId: 'folder1', folderName: 'Folder 1', userId: 'user1', created_at: new Date(), updated_at: new Date() }, + { _id: 'quiz2', title: 'Quiz 2', content: [], folderId: 'folder1', folderName: 'Folder 1', userId: 'user1', created_at: new Date(), updated_at: new Date() } + ]; + const quizzesFolder2: QuizType[] = [ + { _id: 'quiz3', title: 'Quiz 3', content: [], folderId: 'folder2', folderName: 'Folder 2', userId: 'user2', created_at: new Date(), updated_at: new Date() } + ]; + + mockedAxios.get + .mockResolvedValueOnce({ status: 200, data: { data: folders } }) + .mockResolvedValueOnce({ status: 200, data: { data: quizzesFolder1 } }) + .mockResolvedValueOnce({ status: 200, data: { data: quizzesFolder2 } }); + + const result = await ApiService.getAllQuizIds(); + + expect(result).toEqual(['quiz1', 'quiz2', 'quiz3']); + }); + + it('should return an empty array if no folders are found', async () => { + mockedAxios.get.mockResolvedValueOnce({ status: 200, data: { data: [] } }); + + const result = await ApiService.getAllQuizIds(); + + expect(result).toEqual([]); + }); + }); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..144d2f8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,329 @@ +{ + "name": "EvalueTonSavoir", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios-mock-adapter": "^2.1.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "peer": true + }, + "node_modules/axios": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-mock-adapter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz", + "integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "peer": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "peer": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "peer": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a8332a4 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "axios-mock-adapter": "^2.1.0" + } +} From f4bcfd27b95552e6d6743b92a442e1ef3547014b Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Tue, 11 Mar 2025 23:15:59 -0400 Subject: [PATCH 22/46] ajout test getuserimages --- server/__tests__/image.test.js | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js index 1a56431..671ebe8 100644 --- a/server/__tests__/image.test.js +++ b/server/__tests__/image.test.js @@ -194,4 +194,53 @@ describe('Images', () => { expect(result).toEqual({ images: [], total: 0 }); }); }); + + describe('getUserImages', () => { + it('should return empty images array when no images exist', async () => { + mockImagesCollection.countDocuments.mockResolvedValue(0); + + const result = await images.getUserImages(1, 10, 'user123'); + + expect(result).toEqual({ images: [], total: 0 }); + expect(db.connect).toHaveBeenCalled(); + expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' }); + }); + + it('should return images when they exist', async () => { + const mockImages = [ + { + _id: 'img1', + userId: 'user123', + file_name: 'image1.png', + file_content: Buffer.from('testdata'), + mime_type: 'image/png' + } + ]; + + mockImagesCollection.countDocuments.mockResolvedValue(1); + mockImagesCollection.find.mockReturnValue({ + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn().mockResolvedValue(mockImages) + }); + + const result = await images.getUserImages(1, 10, 'user123'); + + expect(result).toEqual({ + images: [ + { + id: 'img1', + user: 'user123', + file_name: 'image1.png', + file_content: Buffer.from('testdata').toString('base64'), + mime_type: 'image/png' + } + ], + total: 1 + }); + expect(db.connect).toHaveBeenCalled(); + expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' }); + }); + }); }); \ No newline at end of file From b7ec35c0c96465058a68fe5074765a59bd4f1b36 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 13 Mar 2025 18:02:26 -0400 Subject: [PATCH 23/46] FIX - ajustement delete image et tests --- .../ImageGallery/ImageGallery.test.tsx | 68 ++++--- .../components/ImageGallery/ImageGallery.tsx | 179 ++++++++++-------- client/src/services/ApiService.tsx | 30 +++ server/__tests__/image.test.js | 61 +++++- server/controllers/images.js | 17 ++ server/models/images.js | 18 ++ server/routers/images.js | 1 + 7 files changed, 269 insertions(+), 105 deletions(-) diff --git a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx index 9604b35..f1da956 100644 --- a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx +++ b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx @@ -32,7 +32,6 @@ describe("ImageDialog Component", () => { render( @@ -50,7 +49,6 @@ describe("ImageDialog Component", () => { render( @@ -68,7 +66,6 @@ describe("ImageDialog Component", () => { render( @@ -85,32 +82,11 @@ describe("ImageDialog Component", () => { expect(screen.getByText("Copié!")).toBeInTheDocument(); }); - test("shows edit field when admin clicks edit button", async () => { - await act(async () => { - render( - - ); - }); - - await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled()); - - const editButton = screen.getByTestId("edit-button-1"); - fireEvent.click(editButton); - - expect(screen.getByDisplayValue("image1.jpg")).toBeInTheDocument(); - }); - test("navigates to next and previous page", async () => { await act(async () => { render( @@ -127,4 +103,48 @@ describe("ImageDialog Component", () => { await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3)); }); + + test("deletes an image successfully", async () => { + jest.spyOn(ApiService, "deleteImage").mockResolvedValue(true); + + await act(async () => { + render( + + ); + }); + + await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled()); + + fireEvent.click(screen.getByTestId("delete-button-1")); + + await waitFor(() => expect(ApiService.deleteImage).toHaveBeenCalledWith("1")); + + expect(screen.queryByTestId("delete-button-1")).not.toBeInTheDocument(); + }); + + test("handles failed delete when image is linked", async () => { + jest.spyOn(ApiService, "deleteImage").mockResolvedValue(false); + + await act(async () => { + render( + + ); + }); + + await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled()); + + fireEvent.click(screen.getByTestId("delete-button-1")); + + await waitFor(() => expect(ApiService.deleteImage).toHaveBeenCalledWith("1")); + + expect(screen.getByText("Confirmer la suppression")).toBeInTheDocument(); + }); }); \ No newline at end of file diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index 069a846..60dfc14 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -12,31 +12,30 @@ import { TableRow, IconButton, Paper, - TextField, - Box + Box, + CircularProgress } from "@mui/material"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import CloseIcon from "@mui/icons-material/Close"; -import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; import { Images } from "../../Types/Images"; import ApiService from '../../services/ApiService'; import { ENV_VARIABLES } from '../../constants'; type Props = { galleryOpen: boolean; - admin: boolean; setDialogOpen: React.Dispatch>; setImageLinks: React.Dispatch>; -} - -const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setImageLinks }) => { +}; +const ImageDialog: React.FC = ({ galleryOpen, setDialogOpen, setImageLinks }) => { const [copiedId, setCopiedId] = useState(null); - const [editingId, setEditingId] = useState(null); const [images, setImages] = useState([]); const [totalImg, setTotalImg] = useState(0); const [imgPage, setImgPage] = useState(1); const [imgLimit] = useState(3); + const [loading, setLoading] = useState(false); + const [deleteConfirm, setDeleteConfirm] = useState<{ id: string | null; linked: boolean }>({ id: null, linked: false }); const fetchImages = async (page: number, limit: number) => { const data = await ApiService.getImages(page, limit); @@ -46,37 +45,50 @@ const ImageDialog: React.FC = ({ galleryOpen, admin, setDialogOpen, setIm useEffect(() => { fetchImages(imgPage, imgLimit); - }, [imgPage]); // Re-fetch images when page changes - - const handleEditClick = (id: string) => { - setEditingId(id === editingId ? null : id); - }; + }, [imgPage]); const onCopy = (id: string) => { const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; - console.log(escLink); setCopiedId(id); setImageLinks(prevLinks => [...prevLinks, escLink]); }; - const handleNextPage = () => { - if ((imgPage * imgLimit) < totalImg) { - setImgPage(prev => prev + 1); - } + const handleDelete = async (id: string) => { + setLoading(true); + const isDeleted = await ApiService.deleteImage(id); + setLoading(false); + if (!isDeleted) { + setDeleteConfirm({ id, linked: true }); + } else { + setImages(images.filter(image => image.id !== id)); + setDeleteConfirm({ id: null, linked: false }); + } }; - + + const confirmDelete = async () => { + if (deleteConfirm.id) { + setLoading(true); + await ApiService.deleteImage(deleteConfirm.id); + setImages(images.filter(image => image.id !== deleteConfirm.id)); + setDeleteConfirm({ id: null, linked: false }); + setLoading(false); + } + }; + + const handleNextPage = () => { + if ((imgPage * imgLimit) < totalImg) { + setImgPage(prev => prev + 1); + } + }; + const handlePrevPage = () => { - if (imgPage > 1) { - setImgPage(prev => prev - 1); - } + if (imgPage > 1) { + setImgPage(prev => prev - 1); + } }; return ( - setDialogOpen(false)} - maxWidth="xl" // 'md' stands for medium size - > + setDialogOpen(false)} maxWidth="xl"> Images disponibles = ({ galleryOpen, admin, setDialogOpen, setIm - - - - {images.map((obj: Images) => ( - - - {`Image - - - {admin && editingId === obj.id ? ( - - ) : ( - obj.file_name - )} - - - {obj.id} - onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}> - - - {admin && ( - <> - handleEditClick(obj.id)} size="small" color="primary" data-testid={`edit-button-${obj.id}`}> - - - - )} - {copiedId === obj.id && Copié!} - - - ))} - -
-
-
- - - - + {loading ? ( + + - + ) : ( + + + + {images.map((obj: Images) => ( + + + {`Image + + {obj.file_name} + + {obj.id} + onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}> + + + handleDelete(obj.id)} size="small" color="secondary" data-testid={`delete-button-${obj.id}`}> + + + {copiedId === obj.id && Copié!} + + + ))} + +
+
+ )} + + {deleteConfirm.linked && ( + setDeleteConfirm({ id: null, linked: false })}> + Confirmer la suppression + + Cette image est liée à d'autres objets. Êtes-vous sûr de vouloir la supprimer ? + + + + + + + )} + + + + + +
); }; diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index 0984652..b3b73ed 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -1247,6 +1247,36 @@ public async login(email: string, password: string): Promise { } } + public async deleteImage(imgId: string): Promise { + try { + const url: string = this.constructRequestUrl(`/image/delete`); + const headers = this.constructRequestHeaders(); + const uid = this.getUserID(); + let params = { uid: uid, imgId: imgId }; + + const result: AxiosResponse = await axios.delete(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`La suppression de l'image a échoué. Status: ${result.status}`); + } + const deleted = result.data.delete; + + return deleted; + + } catch (error) { + console.log("Error details: ", error); + + 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.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + } const apiService = new ApiService(); diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js index 671ebe8..85b49fd 100644 --- a/server/__tests__/image.test.js +++ b/server/__tests__/image.test.js @@ -88,7 +88,8 @@ describe('Images', () => { insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }), findOne: jest.fn(), find: jest.fn().mockReturnValue(mockFindCursor), - countDocuments: jest.fn() + countDocuments: jest.fn(), + deleteOne: jest.fn(), }; dbConn = { @@ -243,4 +244,62 @@ describe('Images', () => { expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' }); }); }); + + describe('delete', () => { + + it('should delete the image when it exists', async () => { + const uid = 'user123'; + const imgId = 'img123'; + + // Simulate the image being found in the collection + mockImagesCollection.find.mockResolvedValue([{ _id: imgId }]); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 1 }); + + const result = await images.delete(uid, imgId); + + expect(db.connect).toHaveBeenCalled(); + expect(db.getConnection).toHaveBeenCalled(); + expect(mockImagesCollection.find).toHaveBeenCalledWith({ + userId: uid, + content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, + }); + expect(mockImagesCollection.deleteOne).toHaveBeenCalledWith({ _id: imgId }); + expect(result).toEqual({ deleted: true }); + }); + + it('should not delete the image when it does not exist', async () => { + const uid = 'user123'; + const imgId = 'img123'; + + // Simulate the image not being found in the collection + mockImagesCollection.find.mockResolvedValue([]); + + const result = await images.delete(uid, imgId); + + expect(db.connect).toHaveBeenCalled(); + expect(mockImagesCollection.find).toHaveBeenCalledWith({ + userId: uid, + content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, + }); + expect(mockImagesCollection.deleteOne).toHaveBeenCalled(); + expect(result).toEqual({ deleted: false }); + }); + + it('should return false if the delete operation fails', async () => { + const uid = 'user123'; + const imgId = 'img123'; + + // Simulate the image being found, but the delete operation failing + mockImagesCollection.find.mockResolvedValue([{ _id: imgId }]); + + const result = await images.delete(uid, imgId); + + expect(db.connect).toHaveBeenCalled(); + expect(mockImagesCollection.find).toHaveBeenCalledWith({ + userId: uid, + content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, + }); + expect(result).toEqual({ deleted: false }); + }); + }); }); \ No newline at end of file diff --git a/server/controllers/images.js b/server/controllers/images.js index e0ac12c..7de02ad 100644 --- a/server/controllers/images.js +++ b/server/controllers/images.js @@ -85,6 +85,23 @@ class ImagesController { } }; + delete = async (req, res, next) => { + try { + const uid = req.query.uid; + const imgId = req.query.imgId; + + if (!uid || !imgId) { + throw new AppError(MISSING_REQUIRED_PARAMETER); + } + const images = await this.images.delete(uid, imgId); + + res.setHeader('Content-Type', 'application/json'); + return res.status(200).json(images); + } catch (error) { + return next(error); + } + }; + } module.exports = ImagesController; diff --git a/server/models/images.js b/server/models/images.js index 036ce1e..1dc4912 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -102,6 +102,24 @@ class Images { return respObj; } + + async delete(uid, imgId) { + let resp = false; + await this.db.connect() + const conn = this.db.getConnection(); + const imgsColl = conn.collection('files'); + const rgxImg = new RegExp(`/api/image/get/${imgId}`); + + const result = await imgsColl.find({ userId: uid, content: { $regex: rgxImg }}); + + if(result){ + const isDeleted = await imgsColl.deleteOne({ _id: imgId }); + if(isDeleted){ + resp = true; + } + } + return { deleted: resp }; + } } module.exports = Images; diff --git a/server/routers/images.js b/server/routers/images.js index 40aa481..2be24d2 100644 --- a/server/routers/images.js +++ b/server/routers/images.js @@ -14,5 +14,6 @@ router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(im router.get("/get/:id", asyncHandler(images.get)); router.get("/getImages", asyncHandler(images.getImages)); router.get("/getUserImages", asyncHandler(images.getUserImages)); +router.get("/delete", asyncHandler(images.delete)); module.exports = router; From b7a7962435f059393fefcc6c4a4ce37edca227ac Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 13 Mar 2025 18:06:24 -0400 Subject: [PATCH 24/46] FIX admin param error --- client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index 6876150..6ce512d 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -302,7 +302,6 @@ const QuizForm: React.FC = () => { From 1c16e06319d909a2eb5c6d8576f0f1d6d1bc9efe Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 13 Mar 2025 21:48:15 -0400 Subject: [PATCH 25/46] FIX - erreur infinite loading --- client/src/components/ImageGallery/ImageGallery.tsx | 2 +- client/src/services/ApiService.tsx | 9 +++++---- server/routers/images.js | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index 60dfc14..85d80ed 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -124,7 +124,7 @@ const ImageDialog: React.FC = ({ galleryOpen, setDialogOpen, setImageLink onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}> - handleDelete(obj.id)} size="small" color="secondary" data-testid={`delete-button-${obj.id}`}> + handleDelete(obj.id)} size="small" color="primary" data-testid={`delete-button-${obj.id}`}> {copiedId === obj.id && Copié!} diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index b3b73ed..acf4066 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -144,13 +144,14 @@ class ApiService { } public getUserID(): string { - const objectStr = localStorage.getItem("jwt"); + const token = localStorage.getItem("jwt"); - if (!objectStr) { + if (!token) { return ""; } - const jsonObj = JSON.parse(objectStr); + const jsonObj = jwtDecode(token) as { userId: string }; + if (!jsonObj.userId) { return ""; } @@ -1259,8 +1260,8 @@ public async login(email: string, password: string): Promise { if (result.status !== 200) { throw new Error(`La suppression de l'image a échoué. Status: ${result.status}`); } - const deleted = result.data.delete; + const deleted = result.data.deleted; return deleted; } catch (error) { diff --git a/server/routers/images.js b/server/routers/images.js index 2be24d2..f2d601a 100644 --- a/server/routers/images.js +++ b/server/routers/images.js @@ -14,6 +14,6 @@ router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(im router.get("/get/:id", asyncHandler(images.get)); router.get("/getImages", asyncHandler(images.getImages)); router.get("/getUserImages", asyncHandler(images.getUserImages)); -router.get("/delete", asyncHandler(images.delete)); +router.delete("/delete", asyncHandler(images.delete)); module.exports = router; From 92699f9a837d5bce6c57f63fd6ecda25c3502a03 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 13 Mar 2025 22:44:43 -0400 Subject: [PATCH 26/46] FIX objectID et mauvaise collection --- server/models/images.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/models/images.js b/server/models/images.js index 1dc4912..16e52de 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -107,13 +107,13 @@ class Images { let resp = false; await this.db.connect() const conn = this.db.getConnection(); - const imgsColl = conn.collection('files'); + const quizColl = conn.collection('files'); const rgxImg = new RegExp(`/api/image/get/${imgId}`); - const result = await imgsColl.find({ userId: uid, content: { $regex: rgxImg }}); - - if(result){ - const isDeleted = await imgsColl.deleteOne({ _id: imgId }); + const result = await quizColl.find({ userId: uid, content: { $regex: rgxImg }}).toArray(); + if(!result || result.length < 1){ + const imgsColl = conn.collection('images'); + const isDeleted = await imgsColl.deleteOne({ _id: ObjectId.createFromHexString(imgId) }); if(isDeleted){ resp = true; } From e2376dd8c34d318b08566e732f29c5e5f4f813b3 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 14 Mar 2025 00:45:59 -0400 Subject: [PATCH 27/46] FIX undefined IMG_URL --- client/src/constants.tsx | 2 +- docker-compose-local.yaml | 4 +++- docker-compose.yaml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/src/constants.tsx b/client/src/constants.tsx index fcdd278..4f6be91 100644 --- a/client/src/constants.tsx +++ b/client/src/constants.tsx @@ -2,7 +2,7 @@ const ENV_VARIABLES = { MODE: process.env.MODE || "production", VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", - IMG_URL: process.env.MODE == "development" ? process.env.VITE_BACKEND_URL : process.env.IMG_URL, + IMG_URL: import.meta.env.VITE_IMG_URL || "http://localhost", BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '', FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : '' }; diff --git a/docker-compose-local.yaml b/docker-compose-local.yaml index 0d8d61a..b77c340 100644 --- a/docker-compose-local.yaml +++ b/docker-compose-local.yaml @@ -7,6 +7,8 @@ services: context: ./client dockerfile: Dockerfile container_name: frontend + environment: + VITE_IMG_URL: http://localhost ports: - "5173:5173" restart: always @@ -54,7 +56,7 @@ services: image: mongo container_name: mongo ports: - - "27017:27017" + - "27019:27017" tty: true volumes: - mongodb_data:/data/db diff --git a/docker-compose.yaml b/docker-compose.yaml index 8db680e..d133982 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,7 @@ services: - VITE_BACKEND_URL= # Define empty VITE_BACKEND_SOCKET_URL so it will default to window.location.host - VITE_BACKEND_SOCKET_URL= - - IMG_URL: https://evalsa.etsmtl.ca + - VITE_IMG_URL=https://evalsa.etsmtl.ca ports: - "5173:5173" restart: always From ab9636c8db1e0f8a7e337ec157b7f7274c00f797 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 14 Mar 2025 00:53:58 -0400 Subject: [PATCH 28/46] test issues --- client/src/constants.tsx | 2 +- client/tsconfig.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/constants.tsx b/client/src/constants.tsx index 4f6be91..1e6dd3e 100644 --- a/client/src/constants.tsx +++ b/client/src/constants.tsx @@ -2,7 +2,7 @@ const ENV_VARIABLES = { MODE: process.env.MODE || "production", VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", - IMG_URL: import.meta.env.VITE_IMG_URL || "http://localhost", + IMG_URL: typeof import.meta !== "undefined" ? import.meta.env.VITE_IMG_URL || "http://localhost" : process.env.VITE_IMG_URL || "http://localhost", BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '', FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : '' }; diff --git a/client/tsconfig.json b/client/tsconfig.json index 5115a44..058fb19 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -27,7 +27,8 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "esModuleInterop": true + "esModuleInterop": true, + "types": ["vite/client"] // ✅ Add this }, // "exclude": [ // // "src/components/GiftTemplate/**/*", From d9480e2d14246c4461cffa1442dff0094518de90 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 14 Mar 2025 00:54:14 -0400 Subject: [PATCH 29/46] fichier manquant --- client/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/tsconfig.json b/client/tsconfig.json index 058fb19..d99f011 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -28,7 +28,7 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "esModuleInterop": true, - "types": ["vite/client"] // ✅ Add this + "types": ["vite/client"] }, // "exclude": [ // // "src/components/GiftTemplate/**/*", From 4e0d5d778da58b8452b4665bba37d4fa34002805 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 14 Mar 2025 01:56:53 -0400 Subject: [PATCH 30/46] fixed server tests --- client/src/constants.tsx | 2 +- client/tsconfig.json | 3 +- server/__tests__/image.test.js | 160 +++++++++++++++++++++------------ 3 files changed, 107 insertions(+), 58 deletions(-) diff --git a/client/src/constants.tsx b/client/src/constants.tsx index 1e6dd3e..fcdd278 100644 --- a/client/src/constants.tsx +++ b/client/src/constants.tsx @@ -2,7 +2,7 @@ const ENV_VARIABLES = { MODE: process.env.MODE || "production", VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", - IMG_URL: typeof import.meta !== "undefined" ? import.meta.env.VITE_IMG_URL || "http://localhost" : process.env.VITE_IMG_URL || "http://localhost", + IMG_URL: process.env.MODE == "development" ? process.env.VITE_BACKEND_URL : process.env.IMG_URL, BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '', FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : '' }; diff --git a/client/tsconfig.json b/client/tsconfig.json index d99f011..5115a44 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -27,8 +27,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "types": ["vite/client"] + "esModuleInterop": true }, // "exclude": [ // // "src/components/GiftTemplate/**/*", diff --git a/server/__tests__/image.test.js b/server/__tests__/image.test.js index 85b49fd..7ce4ad1 100644 --- a/server/__tests__/image.test.js +++ b/server/__tests__/image.test.js @@ -68,6 +68,17 @@ describe.skip("GET /get", () => { }) +jest.mock('mongodb', () => { + const originalModule = jest.requireActual('mongodb'); + return { + ...originalModule, + ObjectId: { + ...originalModule.ObjectId, + createFromHexString: jest.fn().mockReturnValue('507f191e810c19729de860ea'), // Return a valid 24-character ObjectId string + }, + }; +}); + describe('Images', () => { let db; let images; @@ -76,24 +87,41 @@ describe('Images', () => { let mockFindCursor; beforeEach(() => { + + const mockImagesCursor = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn() + }; - mockFindCursor = { - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - toArray: jest.fn(), - }; + const mockFilesCursor = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn() + }; mockImagesCollection = { insertOne: jest.fn().mockResolvedValue({ insertedId: 'image123' }), findOne: jest.fn(), - find: jest.fn().mockReturnValue(mockFindCursor), + find: jest.fn().mockReturnValue(mockImagesCursor), countDocuments: jest.fn(), - deleteOne: jest.fn(), + deleteOne: jest.fn() }; + mockFilesCollection = { + find: jest.fn().mockReturnValue(mockFilesCursor) + }; + dbConn = { - collection: jest.fn().mockReturnValue(mockImagesCollection) + collection: jest.fn((name) => { + if (name === 'images') { + return mockImagesCollection; + } else if (name === 'files') { + return mockFilesCollection; + } + }) }; db = { @@ -166,8 +194,16 @@ describe('Images', () => { ]; mockImagesCollection.countDocuments.mockResolvedValue(2); - mockFindCursor.toArray.mockResolvedValue(mockImages); + // Create a mock cursor for images collection + const mockFindCursor = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + toArray: jest.fn().mockResolvedValue(mockImages), // Return mock images when toArray is called + }; + // Mock the find method to return the mock cursor + mockImagesCollection.find.mockReturnValue(mockFindCursor); const result = await images.getImages(1, 10); expect(db.connect).toHaveBeenCalled(); @@ -188,7 +224,6 @@ describe('Images', () => { it('should return an empty array if no images are found', async () => { mockImagesCollection.countDocuments.mockResolvedValue(0); - mockFindCursor.toArray.mockResolvedValue([]); const result = await images.getImages(1, 10); @@ -244,62 +279,77 @@ describe('Images', () => { expect(mockImagesCollection.countDocuments).toHaveBeenCalledWith({ userId: 'user123' }); }); }); - describe('delete', () => { - - it('should delete the image when it exists', async () => { + it('should not delete the image when it exists in the files collection', async () => { const uid = 'user123'; - const imgId = 'img123'; - - // Simulate the image being found in the collection - mockImagesCollection.find.mockResolvedValue([{ _id: imgId }]); - mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 1 }); - + const imgId = '507f191e810c19729de860ea'; // A valid 24-character ObjectId string + + // Mock the files collection cursor to simulate an image found + const mockFilesCursor = { + toArray: jest.fn().mockResolvedValue([{ _id: imgId }]) // Image found + }; + + mockFilesCollection.find.mockReturnValue(mockFilesCursor); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 0 }); + const result = await images.delete(uid, imgId); - - expect(db.connect).toHaveBeenCalled(); - expect(db.getConnection).toHaveBeenCalled(); - expect(mockImagesCollection.find).toHaveBeenCalledWith({ + + // Ensure the files collection is queried + expect(dbConn.collection).toHaveBeenCalledWith('files'); + expect(mockFilesCollection.find).toHaveBeenCalledWith({ userId: uid, content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, }); - expect(mockImagesCollection.deleteOne).toHaveBeenCalledWith({ _id: imgId }); + + // Ensure the images collection is queried for deletion + expect(dbConn.collection).toHaveBeenCalledWith('files'); + expect(mockImagesCollection.deleteOne).not.toHaveBeenCalledWith({ + _id: ObjectId.createFromHexString(imgId), // Ensure the ObjectId is created correctly + }); + + expect(result).toEqual({ deleted: false }); + }); + + it('should delete the image if not found in the files collection', async () => { + const uid = 'user123'; + const imgId = '507f191e810c19729de860ea'; + + // Mock the files collection cursor to simulate the image not being found + const mockFindCursor = { + toArray: jest.fn().mockResolvedValue([]) // Empty array means image not found + }; + + mockFilesCollection.find.mockReturnValue(mockFindCursor); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 1 }); + + const result = await images.delete(uid, imgId); + + // Ensure the deleteOne is not called if the image is not found + expect(mockImagesCollection.deleteOne).toHaveBeenCalled(); expect(result).toEqual({ deleted: true }); }); - - it('should not delete the image when it does not exist', async () => { + + it('should return false if the delete operation fails in the images collection', async () => { const uid = 'user123'; - const imgId = 'img123'; - - // Simulate the image not being found in the collection - mockImagesCollection.find.mockResolvedValue([]); - + const imgId = '507f191e810c19729de860ea'; + + // Mock the files collection cursor to simulate the image being found + const mockFindCursor = { + toArray: jest.fn().mockResolvedValue([{ _id: imgId }]) // Image found + }; + + mockFilesCollection.find.mockReturnValue(mockFindCursor); + mockImagesCollection.deleteOne.mockResolvedValue({ deletedCount: 0 }); // Simulate failure + const result = await images.delete(uid, imgId); - - expect(db.connect).toHaveBeenCalled(); - expect(mockImagesCollection.find).toHaveBeenCalledWith({ - userId: uid, - content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, - }); - expect(mockImagesCollection.deleteOne).toHaveBeenCalled(); - expect(result).toEqual({ deleted: false }); - }); - - it('should return false if the delete operation fails', async () => { - const uid = 'user123'; - const imgId = 'img123'; - - // Simulate the image being found, but the delete operation failing - mockImagesCollection.find.mockResolvedValue([{ _id: imgId }]); - - const result = await images.delete(uid, imgId); - - expect(db.connect).toHaveBeenCalled(); - expect(mockImagesCollection.find).toHaveBeenCalledWith({ - userId: uid, - content: { $regex: new RegExp(`/api/image/get/${imgId}`) }, + + // Ensure the images collection deletion is called + expect(mockImagesCollection.deleteOne).not.toHaveBeenCalledWith({ + _id: ObjectId.createFromHexString(imgId), // Ensure the ObjectId is created correctly }); + expect(result).toEqual({ deleted: false }); }); + }); }); \ No newline at end of file From c50cd3e6e7a624df277f1e0c160f64f425d0c8b0 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 20 Mar 2025 21:11:46 -0400 Subject: [PATCH 31/46] =?UTF-8?q?FIX=20-=20Changement=20de=20table=20en=20?= =?UTF-8?q?gallery=20-=20Ajout=20option=20de=20r=C3=A9solution=20-=20Ajout?= =?UTF-8?q?=20warning=20-=20Int=C3=A9gration=20de=20upload=20-=20Ajout=20d?= =?UTF-8?q?e=20snackbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/Types/ImageType.tsx | 17 + .../components/ImageGallery/ImageGallery.tsx | 359 ++++++++++++------ .../ImageGalleryModal/ImageGalleryModal.tsx | 54 +++ .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 100 +---- 4 files changed, 320 insertions(+), 210 deletions(-) create mode 100644 client/src/Types/ImageType.tsx create mode 100644 client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx diff --git a/client/src/Types/ImageType.tsx b/client/src/Types/ImageType.tsx new file mode 100644 index 0000000..d0a1541 --- /dev/null +++ b/client/src/Types/ImageType.tsx @@ -0,0 +1,17 @@ +export interface ImageType { + id: string; + file_content: string; + file_name: string; + mime_type: string; +} + +export interface ImagesResponse { + images: ImageType[]; + total: number; +} + +export interface ImagesParams { + page: number; + limit: number; + uid?: string; +} \ No newline at end of file diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index 85d80ed..dfb63d8 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -1,169 +1,280 @@ import React, { useState, useEffect } from "react"; import { + Box, + CircularProgress, + Button, + IconButton, + Card, + CardContent, Dialog, - DialogTitle, DialogContent, DialogActions, - Button, - Table, - TableBody, - TableCell, - TableContainer, - TableRow, - IconButton, - Paper, - Box, - CircularProgress + DialogTitle, + DialogContentText, + Tabs, + Tab, + TextField, Snackbar, + Alert } from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import CloseIcon from "@mui/icons-material/Close"; -import DeleteIcon from "@mui/icons-material/Delete"; -import { Images } from "../../Types/Images"; -import ApiService from '../../services/ApiService'; -import { ENV_VARIABLES } from '../../constants'; +import { ImageType } from "../../Types/ImageType"; +import ApiService from "../../services/ApiService"; +import { Upload } from "@mui/icons-material"; -type Props = { - galleryOpen: boolean; - setDialogOpen: React.Dispatch>; - setImageLinks: React.Dispatch>; -}; +interface ImagesProps { + handleCopy?: (id: string) => void; +} -const ImageDialog: React.FC = ({ galleryOpen, setDialogOpen, setImageLinks }) => { - const [copiedId, setCopiedId] = useState(null); - const [images, setImages] = useState([]); +const ImageGallery: React.FC = ({ handleCopy }) => { + const [images, setImages] = useState([]); const [totalImg, setTotalImg] = useState(0); const [imgPage, setImgPage] = useState(1); - const [imgLimit] = useState(3); + const [imgLimit] = useState(6); const [loading, setLoading] = useState(false); - const [deleteConfirm, setDeleteConfirm] = useState<{ id: string | null; linked: boolean }>({ id: null, linked: false }); + const [selectedImage, setSelectedImage] = useState(null); + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + const [imageToDelete, setImageToDelete] = useState(null); + const [tabValue, setTabValue] = useState(0); + const [importedImage, setImportedImage] = useState(null); + const [preview, setPreview] = useState(null); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(""); + const [snackbarSeverity, setSnackbarSeverity] = useState<"success" | "error">("success"); - const fetchImages = async (page: number, limit: number) => { - const data = await ApiService.getImages(page, limit); + const fetchImages = async () => { + setLoading(true); + const data = await ApiService.getImages(imgPage, imgLimit); setImages(data.images); setTotalImg(data.total); + setLoading(false); }; useEffect(() => { - fetchImages(imgPage, imgLimit); + fetchImages(); }, [imgPage]); - const onCopy = (id: string) => { - const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; - setCopiedId(id); - setImageLinks(prevLinks => [...prevLinks, escLink]); - }; - - const handleDelete = async (id: string) => { - setLoading(true); - const isDeleted = await ApiService.deleteImage(id); - setLoading(false); - if (!isDeleted) { - setDeleteConfirm({ id, linked: true }); - } else { - setImages(images.filter(image => image.id !== id)); - setDeleteConfirm({ id: null, linked: false }); - } - }; - - const confirmDelete = async () => { - if (deleteConfirm.id) { + const handleDelete = async () => { + if (imageToDelete) { setLoading(true); - await ApiService.deleteImage(deleteConfirm.id); - setImages(images.filter(image => image.id !== deleteConfirm.id)); - setDeleteConfirm({ id: null, linked: false }); + const isDeleted = await ApiService.deleteImage(imageToDelete.id); setLoading(false); + + if (isDeleted) { + setImages(images.filter((image) => image.id !== imageToDelete.id)); + setSnackbarMessage("Image supprimée avec succès !"); + setSnackbarSeverity("success"); + } else { + setSnackbarMessage("Erreur lors de la suppression de l'image. Veuillez réessayer."); + setSnackbarSeverity("error"); + } + + setSnackbarOpen(true); + setSelectedImage(null); + setImageToDelete(null); + setOpenDeleteDialog(false); } }; - const handleNextPage = () => { - if ((imgPage * imgLimit) < totalImg) { - setImgPage(prev => prev + 1); + const defaultHandleCopy = (id: string) => { + if (navigator.clipboard) { + navigator.clipboard.writeText(id); } }; - const handlePrevPage = () => { - if (imgPage > 1) { - setImgPage(prev => prev - 1); + const handleCopyFunction = handleCopy || defaultHandleCopy; + + const handleImageUpload = (event: React.ChangeEvent) => { + const file = event.target.files ? event.target.files[0] : null; + setImportedImage(file); + if (file) { + const objectUrl = URL.createObjectURL(file); + setPreview(objectUrl); + } + }; + + const handleSaveImage = async () => { + try { + if (!importedImage) { + setSnackbarMessage("Veuillez d'abord choisir une image à téléverser."); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + return; + } + + const imageUrl = await ApiService.uploadImage(importedImage); + + if (imageUrl.includes("ERROR")) { + setSnackbarMessage("Une erreur est survenue. Veuillez réessayer plus tard."); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + return; + } + fetchImages(); + + setSnackbarMessage("Téléversée avec succès !"); + setSnackbarSeverity("success"); + setSnackbarOpen(true); + + // Reset the input field and preview after successful upload + setImportedImage(null); + setPreview(null); + } catch (error) { + setSnackbarMessage(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`); + setSnackbarSeverity("error"); + setSnackbarOpen(true); } }; return ( - setDialogOpen(false)} maxWidth="xl"> - - Images disponibles - setDialogOpen(false)} - style={{ position: "absolute", right: 8, top: 8 }} - > - - - - - {loading ? ( - - - - ) : ( - - - - {images.map((obj: Images) => ( - - + + setTabValue(newValue)}> + + + + {tabValue === 0 && ( + <> + {loading ? ( + + + + ) : ( + <> + + {images.map((obj) => ( + setSelectedImage(obj)}> + {`Image - - {obj.file_name} - - {obj.id} - onCopy(obj.id)} size="small" data-testid={`copy-button-${obj.id}`}> - + + + { + e.stopPropagation(); + handleCopyFunction(obj.id); + }} + color="primary" > + + - handleDelete(obj.id)} size="small" color="primary" data-testid={`delete-button-${obj.id}`}> - + + { + e.stopPropagation(); + setImageToDelete(obj); + setOpenDeleteDialog(true); + }} + color="error" > + + - {copiedId === obj.id && Copié!} - - + + ))} - -
-
- )} -
- {deleteConfirm.linked && ( - setDeleteConfirm({ id: null, linked: false })}> - Confirmer la suppression - - Cette image est liée à d'autres objets. Êtes-vous sûr de vouloir la supprimer ? - - - - - - + + + + + + + )} + )} - - - - + {tabValue === 1 && ( + + {/* Image Preview at the top */} + {preview && ( + + Preview + + )} + + + + - -
+ )} + setSelectedImage(null)} maxWidth="md"> + setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}> + + + + {selectedImage && ( + Enlarged view + )} + + + + {/* Delete Confirmation Dialog */} + setOpenDeleteDialog(false)}> + Supprimer + + Voulez-vous supprimer cette image? + + + + + + + + setSnackbarOpen(false)}> + setSnackbarOpen(false)} severity={snackbarSeverity} sx={{ width: "100%" }}> + {snackbarMessage} + + + ); }; -export default ImageDialog; +export default ImageGallery; diff --git a/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx new file mode 100644 index 0000000..7ea2404 --- /dev/null +++ b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx @@ -0,0 +1,54 @@ +import React, { useState } from "react"; +import { + Button, + IconButton, + Dialog, + DialogContent, +} from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import ImageGallery from "../ImageGallery"; +import { ImageSearch } from "@mui/icons-material"; + + +interface ImageGalleryModalProps { + handleCopy?: (id: string) => void; +} + + +const ImageGalleryModal: React.FC = ({ handleCopy }) => { + const [open, setOpen] = useState(false); + + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + return ( + <> + + + + + + + + + + + + ); +}; + +export default ImageGalleryModal; diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index 6ce512d..d3989a3 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -1,5 +1,5 @@ // EditorQuiz.tsx -import React, { useState, useEffect, useRef, CSSProperties } from 'react'; +import React, { useState, useEffect, CSSProperties } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { FolderType } from '../../../Types/FolderType'; @@ -11,13 +11,13 @@ import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview import { QuizType } from '../../../Types/QuizType'; import './editorQuiz.css'; -import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material'; +import { Button, TextField, NativeSelect, Divider } from '@mui/material'; import ReturnButton from 'src/components/ReturnButton/ReturnButton'; -import ImageGallery from 'src/components/ImageGallery/ImageGallery'; +import ImageGalleryModal from 'src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal'; import ApiService from '../../../services/ApiService'; import { escapeForGIFT } from '../../../utils/giftUtils'; -import { Upload, ImageSearch } from '@mui/icons-material'; +import { ENV_VARIABLES } from '../../../constants'; interface EditQuizParams { id: string; @@ -39,9 +39,6 @@ const QuizForm: React.FC = () => { const handleSelectFolder = (event: React.ChangeEvent) => { setSelectedFolder(event.target.value); }; - const fileInputRef = useRef(null); - const [dialogOpen, setDialogOpen] = useState(false); - const [galleryOpen, setGalleryOpen] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false); const scrollToTop = () => { @@ -168,44 +165,16 @@ const QuizForm: React.FC = () => { return
Chargement...
; } - const handleSaveImage = async () => { - try { - const inputElement = document.getElementById('file-input') as HTMLInputElement; - - if (!inputElement?.files || inputElement.files.length === 0) { - setDialogOpen(true); - return; - } - - if (!inputElement.files || inputElement.files.length === 0) { - window.alert("Veuillez d'abord choisir une image à téléverser.") - return; - } - - const imageUrl = await ApiService.uploadImage(inputElement.files[0]); - - // Check for errors - if(imageUrl.indexOf("ERROR") >= 0) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) - return; - } - - setImageLinks(prevLinks => [...prevLinks, imageUrl]); - - // Reset the file input element - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - } catch (error) { - window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`) - - } - }; - const handleCopyToClipboard = async (link: string) => { navigator.clipboard.writeText(link); } + const handleCopyImage = (id: string) => { + const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; + navigator.clipboard.writeText(id); + setImageLinks(prevLinks => [...prevLinks, escLink]); + } + return (
@@ -260,51 +229,10 @@ const QuizForm: React.FC = () => { onEditorChange={handleUpdatePreview} />
-
- - setDialogOpen(false)} > - Erreur - - Veuillez d'abord choisir une image à téléverser. - - - - - +
+

Mes images :

+
- -

Mes images :

- - - - -
@@ -319,7 +247,7 @@ const QuizForm: React.FC = () => {
    {imageLinks.map((link, index) => { - const imgTag = `![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`; + const imgTag = `[markdown]![alt_text](${escapeForGIFT(link)} "texte de l'infobulle") {T}`; return (
  • Date: Thu, 20 Mar 2025 23:19:54 -0400 Subject: [PATCH 32/46] Fix - tests --- .../ImageGallery/ImageGallery.test.tsx | 159 ++++-------------- .../components/ImageGallery/ImageGallery.tsx | 8 +- 2 files changed, 37 insertions(+), 130 deletions(-) diff --git a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx index f1da956..761029f 100644 --- a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx +++ b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx @@ -1,12 +1,10 @@ -import React from "react"; +import React, { act } from "react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; -import ImageDialog from "../../../components/ImageGallery/ImageGallery"; +import ImageGallery from "../../../components/ImageGallery/ImageGallery"; import ApiService from "../../../services/ApiService"; import { Images } from "../../../Types/Images"; -import { act } from "react"; import "@testing-library/jest-dom"; -// Mock ApiService jest.mock("../../../services/ApiService"); const mockImages: Images[] = [ @@ -15,136 +13,43 @@ const mockImages: Images[] = [ { id: "3", file_name: "image3.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content3" }, ]; -describe("ImageDialog Component", () => { - let setDialogOpenMock: jest.Mock; - let setImageLinksMock: jest.Mock; +beforeAll(() => { + Object.assign(navigator, { + clipboard: { + writeText: jest.fn(), + }, + }); +}); +describe("ImageGallery", () => { beforeEach(() => { - jest.clearAllMocks(); - setDialogOpenMock = jest.fn(); - setImageLinksMock = jest.fn(); - jest.spyOn(ApiService, "getImages").mockResolvedValue({ images: mockImages, total: 6 }); + (ApiService.getUserImages as jest.Mock).mockResolvedValue({ images: mockImages, total: 3 }); + (ApiService.deleteImage as jest.Mock).mockResolvedValue(true); + (ApiService.uploadImage as jest.Mock).mockResolvedValue('mockImageUrl'); + + render(); }); - test("renders the dialog when open", async () => { - + it("should render images correctly", async () => { await act(async () => { - render( - - ); - }); - - expect(screen.getByText("Images disponibles")).toBeInTheDocument(); - await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3)); - expect(screen.getAllByRole("img")).toHaveLength(mockImages.length); - }); - - test("closes the dialog when close button is clicked", async () => { - - await act(async () => { - render( - - ); - }); - - fireEvent.click(screen.getByLabelText("close")); - expect(setDialogOpenMock).toHaveBeenCalledWith(false); - }); - - test("copies the image link when copy button is clicked", async () => { - - //const setImageLinksMock = jest.fn(); - await act(async () => { - render( - - ); - }); - - await act(async () => { - await waitFor(() => expect(screen.getAllByRole("img")).toHaveLength(mockImages.length)); + await screen.findByText("Gallery"); }); + + expect(screen.getByAltText("Image image1.jpg")).toBeInTheDocument(); + expect(screen.getByAltText("Image image2.jpg")).toBeInTheDocument(); + }); + + it("should handle copy action", async () => { + const handleCopyMock = jest.fn(); - // Click the copy button - fireEvent.click(screen.getByTestId("copy-button-1")); - // Check that "Copié!" appears - expect(screen.getByText("Copié!")).toBeInTheDocument(); + render(); + + const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/)); + await act(async () => { + fireEvent.click(copyButtons[0]); + }); + + expect(navigator.clipboard.writeText).toHaveBeenCalled(); }); - test("navigates to next and previous page", async () => { - await act(async () => { - render( - - ); - }); - - await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3)); - - fireEvent.click(screen.getByText("Suivant")); - - await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(2, 3)); - - fireEvent.click(screen.getByText("Précédent")); - - await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3)); - }); - - test("deletes an image successfully", async () => { - jest.spyOn(ApiService, "deleteImage").mockResolvedValue(true); - - await act(async () => { - render( - - ); - }); - - await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled()); - - fireEvent.click(screen.getByTestId("delete-button-1")); - - await waitFor(() => expect(ApiService.deleteImage).toHaveBeenCalledWith("1")); - - expect(screen.queryByTestId("delete-button-1")).not.toBeInTheDocument(); - }); - - test("handles failed delete when image is linked", async () => { - jest.spyOn(ApiService, "deleteImage").mockResolvedValue(false); - - await act(async () => { - render( - - ); - }); - - await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled()); - - fireEvent.click(screen.getByTestId("delete-button-1")); - - await waitFor(() => expect(ApiService.deleteImage).toHaveBeenCalledWith("1")); - - expect(screen.getByText("Confirmer la suppression")).toBeInTheDocument(); - }); }); \ No newline at end of file diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index dfb63d8..de907d8 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -45,7 +45,7 @@ const ImageGallery: React.FC = ({ handleCopy }) => { const fetchImages = async () => { setLoading(true); - const data = await ApiService.getImages(imgPage, imgLimit); + const data = await ApiService.getUserImages(imgPage, imgLimit); setImages(data.images); setTotalImg(data.total); setLoading(false); @@ -156,7 +156,8 @@ const ImageGallery: React.FC = ({ handleCopy }) => { e.stopPropagation(); handleCopyFunction(obj.id); }} - color="primary" > + color="primary" + data-testid={`gallery-tab-copy-${obj.id}`} > @@ -167,7 +168,8 @@ const ImageGallery: React.FC = ({ handleCopy }) => { setImageToDelete(obj); setOpenDeleteDialog(true); }} - color="error" > + color="error" + data-testid={`gallery-tab-delete-${obj.id}`} > From 1a7dc7fec21165d8a3fc7f64249978517f7792f7 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 21 Mar 2025 15:24:21 -0400 Subject: [PATCH 33/46] ajout tests --- .../ImageGallery/ImageGallery.test.tsx | 48 ++++++++++++++++--- .../components/ImageGallery/ImageGallery.tsx | 14 ++++-- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx index 761029f..2fdb583 100644 --- a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx +++ b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx @@ -22,10 +22,13 @@ beforeAll(() => { }); describe("ImageGallery", () => { + + let mockHandleDelete: jest.Mock; beforeEach(() => { (ApiService.getUserImages as jest.Mock).mockResolvedValue({ images: mockImages, total: 3 }); (ApiService.deleteImage as jest.Mock).mockResolvedValue(true); (ApiService.uploadImage as jest.Mock).mockResolvedValue('mockImageUrl'); + mockHandleDelete = jest.fn(); render(); }); @@ -43,13 +46,46 @@ describe("ImageGallery", () => { const handleCopyMock = jest.fn(); render(); - - const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/)); - await act(async () => { - fireEvent.click(copyButtons[0]); - }); + + const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/)); + await act(async () => { + fireEvent.click(copyButtons[0]); + }); expect(navigator.clipboard.writeText).toHaveBeenCalled(); }); -}); \ No newline at end of file + it("should delete an image and update the gallery", async () => { + const fetchImagesMock = jest.fn().mockResolvedValue({ images: mockImages.filter((image) => image.id !== "1"), total: 2 }); + + (ApiService.getUserImages as jest.Mock).mockImplementation(fetchImagesMock); + + render(); + + await act(async () => { + await screen.findByAltText("Image image1.jpg"); + }); + + const deleteButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-delete-/)); + fireEvent.click(deleteButtons[0]); + + await waitFor(() => { + expect(screen.getByText("Voulez-vous supprimer cette image?")).toBeInTheDocument(); + }); + + const confirmDeleteButton = screen.getByText("Delete"); + await act(async () => { + fireEvent.click(confirmDeleteButton); + }); + + await waitFor(() => { + expect(ApiService.deleteImage).toHaveBeenCalledWith("1"); + }); + + await waitFor(() => { + expect(screen.queryByAltText("Image image1.jpg")).toBeNull(); + expect(screen.getByText("Image supprimée avec succès !")).toBeInTheDocument(); + }); + }); + +}); diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index de907d8..1bed68e 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -25,9 +25,10 @@ import { Upload } from "@mui/icons-material"; interface ImagesProps { handleCopy?: (id: string) => void; + handleDelete?: (id: string) => void; } -const ImageGallery: React.FC = ({ handleCopy }) => { +const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { const [images, setImages] = useState([]); const [totalImg, setTotalImg] = useState(0); const [imgPage, setImgPage] = useState(1); @@ -55,14 +56,16 @@ const ImageGallery: React.FC = ({ handleCopy }) => { fetchImages(); }, [imgPage]); - const handleDelete = async () => { + const defaultHandleDelete = async (id: string) => { if (imageToDelete) { setLoading(true); - const isDeleted = await ApiService.deleteImage(imageToDelete.id); + const isDeleted = await ApiService.deleteImage(id); setLoading(false); if (isDeleted) { - setImages(images.filter((image) => image.id !== imageToDelete.id)); + //setImages(images.filter((image) => image.id !== id)); + setImgPage(1); + fetchImages(); setSnackbarMessage("Image supprimée avec succès !"); setSnackbarSeverity("success"); } else { @@ -84,6 +87,7 @@ const ImageGallery: React.FC = ({ handleCopy }) => { }; const handleCopyFunction = handleCopy || defaultHandleCopy; + const handleDeleteFunction = handleDelete || defaultHandleDelete; const handleImageUpload = (event: React.ChangeEvent) => { const file = event.target.files ? event.target.files[0] : null; @@ -264,7 +268,7 @@ const ImageGallery: React.FC = ({ handleCopy }) => { - From 755d14a5b753bbf4c10bad113c309437150d9dde Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 21 Mar 2025 23:29:20 -0400 Subject: [PATCH 34/46] ajout dernier tests --- .../ImageGallery/ImageGallery.test.tsx | 72 ++++++++++++++++--- .../components/ImageGallery/ImageGallery.tsx | 22 ++++-- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx index 2fdb583..d60a0c9 100644 --- a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx +++ b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx @@ -1,9 +1,10 @@ import React, { act } from "react"; +import "@testing-library/jest-dom"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import ImageGallery from "../../../components/ImageGallery/ImageGallery"; import ApiService from "../../../services/ApiService"; import { Images } from "../../../Types/Images"; -import "@testing-library/jest-dom"; +import userEvent from "@testing-library/user-event"; jest.mock("../../../services/ApiService"); @@ -14,6 +15,7 @@ const mockImages: Images[] = [ ]; beforeAll(() => { + global.URL.createObjectURL = jest.fn(() => 'mockedObjectUrl'); Object.assign(navigator, { clipboard: { writeText: jest.fn(), @@ -22,15 +24,16 @@ beforeAll(() => { }); describe("ImageGallery", () => { - let mockHandleDelete: jest.Mock; - beforeEach(() => { + + beforeEach(async () => { (ApiService.getUserImages as jest.Mock).mockResolvedValue({ images: mockImages, total: 3 }); (ApiService.deleteImage as jest.Mock).mockResolvedValue(true); (ApiService.uploadImage as jest.Mock).mockResolvedValue('mockImageUrl'); + await act(async () => { + render(); + }); mockHandleDelete = jest.fn(); - - render(); }); it("should render images correctly", async () => { @@ -44,8 +47,9 @@ describe("ImageGallery", () => { it("should handle copy action", async () => { const handleCopyMock = jest.fn(); - - render(); + await act(async () => { + render(); + }); const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/)); await act(async () => { @@ -60,7 +64,9 @@ describe("ImageGallery", () => { (ApiService.getUserImages as jest.Mock).mockImplementation(fetchImagesMock); - render(); + await act(async () => { + render(); + }); await act(async () => { await screen.findByAltText("Image image1.jpg"); @@ -88,4 +94,54 @@ describe("ImageGallery", () => { }); }); + it("should upload an image and display success message", async () => { + const importTab = screen.getByRole("tab", { name: /import/i }); + fireEvent.click(importTab); + + const fileInputs = await screen.findAllByTestId("file-input"); + const fileInput = fileInputs[1]; + + expect(fileInput).toBeInTheDocument(); + + const file = new File(["image"], "image.jpg", { type: "image/jpeg" }); + await userEvent.upload(fileInput, file); + + + await waitFor(() => screen.getByAltText("Preview")); + const previewImage = screen.getByAltText("Preview"); + + expect(previewImage).toBeInTheDocument(); + + const uploadButton = screen.getByRole('button', { name: /téléverser/i }); + fireEvent.click(uploadButton); + const successMessage = await screen.findByText(/téléversée avec succès/i); + expect(successMessage).toBeInTheDocument(); + }); + + it("should close the image preview dialog when close button is clicked", async () => { + const imageCard = screen.getByAltText("Image image1.jpg"); + fireEvent.click(imageCard); + + const dialogImage = await screen.findByAltText("Enlarged view"); + expect(dialogImage).toBeInTheDocument(); + + const closeButton = screen.getByTestId("close-button"); + fireEvent.click(closeButton); + + await waitFor(() => { + expect(screen.queryByAltText("Enlarged view")).not.toBeInTheDocument(); + }); + }); + + it("should show an error message when no file is selected", async () => { + const importTab = screen.getByRole("tab", { name: /import/i }); + fireEvent.click(importTab); + const uploadButton = screen.getByRole('button', { name: /téléverser/i }); + fireEvent.click(uploadButton); + + await waitFor(() => { + expect(screen.getByText("Veuillez choisir une image à téléverser.")).toBeInTheDocument(); + }); + }); + }); diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index 1bed68e..f404b91 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -101,7 +101,7 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { const handleSaveImage = async () => { try { if (!importedImage) { - setSnackbarMessage("Veuillez d'abord choisir une image à téléverser."); + setSnackbarMessage("Veuillez choisir une image à téléverser."); setSnackbarSeverity("error"); setSnackbarOpen(true); return; @@ -131,6 +131,10 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { } }; + const handleCloseSnackbar = () => { + setSnackbarOpen(false); + }; + return ( setTabValue(newValue)}> @@ -224,9 +228,11 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { = ({ handleCopy, handleDelete }) => { )} setSelectedImage(null)} maxWidth="md"> - setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}> + setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }} + data-testid="close-button"> @@ -274,8 +281,15 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { - setSnackbarOpen(false)}> - setSnackbarOpen(false)} severity={snackbarSeverity} sx={{ width: "100%" }}> + + {snackbarMessage} From 78e398ecd89833c23d8be942bd16d03c391698b2 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Sat, 22 Mar 2025 03:49:28 -0400 Subject: [PATCH 35/46] ajout test ImageGalleryModal --- .../ImageGallery/ImageGalleryModal.test.tsx | 44 +++++++++++++++++++ .../ImageGalleryModal/ImageGalleryModal.tsx | 3 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 client/src/__tests__/components/ImageGallery/ImageGalleryModal.test.tsx diff --git a/client/src/__tests__/components/ImageGallery/ImageGalleryModal.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGalleryModal.test.tsx new file mode 100644 index 0000000..f5a65a6 --- /dev/null +++ b/client/src/__tests__/components/ImageGallery/ImageGalleryModal.test.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import ImageGalleryModal from "../../../components/ImageGallery/ImageGalleryModal/ImageGalleryModal"; +import "@testing-library/jest-dom"; + +jest.mock("../../../components/ImageGallery/ImageGallery", () => ({ + __esModule: true, + default: jest.fn(() =>
    ), +})); + +describe("ImageGalleryModal", () => { + + it("renders button correctly", () => { + render(); + + const button = screen.getByLabelText(/images-open/i); + expect(button).toBeInTheDocument(); + }); + + it("opens the modal when button is clicked", () => { + render(); + + const button = screen.getByRole("button", { name: /images/i }); + fireEvent.click(button); + + const dialog = screen.getByRole("dialog"); + expect(dialog).toBeInTheDocument(); + }); + + + it("closes the modal when close button is clicked", async () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /images/i })); + + const closeButton = screen.getByRole("button", { name: /close/i }); + fireEvent.click(closeButton); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + +}); diff --git a/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx index 7ea2404..f960352 100644 --- a/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx +++ b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx @@ -25,7 +25,7 @@ const ImageGalleryModal: React.FC = ({ handleCopy }) => <> @@ -34,6 +34,7 @@ const ImageGalleryModal: React.FC = ({ handleCopy }) => Date: Sat, 22 Mar 2025 16:14:12 -0400 Subject: [PATCH 36/46] Node 22 server --- server/package-lock.json | 201 +++++++++------------------------------ server/package.json | 2 +- 2 files changed, 45 insertions(+), 158 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 0f006f8..715837e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -38,7 +38,7 @@ "supertest": "^6.3.4" }, "engines": { - "node": "20.x" + "node": "22.x" } }, "node_modules/@ampproject/remapping": { @@ -55,68 +55,20 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@babel/compat-data": { "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", @@ -342,19 +294,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -369,88 +323,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@babel/parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", - "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.10" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -636,14 +530,15 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" @@ -704,14 +599,14 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", - "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -4772,7 +4667,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -6683,15 +6579,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/server/package.json b/server/package.json index cd0348b..3f0cfd4 100644 --- a/server/package.json +++ b/server/package.json @@ -42,7 +42,7 @@ "supertest": "^6.3.4" }, "engines": { - "node": "20.x" + "node": "22.x" }, "jest": { "testEnvironment": "node", From 6600da990ba4a514a3ac6385b1395609e653c7e1 Mon Sep 17 00:00:00 2001 From: NouhailaAater Date: Mon, 24 Mar 2025 02:52:41 -0400 Subject: [PATCH 37/46] Add Finish Quiz button --- .../pages/Teacher/ManageRoom/ManageRoom.tsx | 67 +++++++++++++------ .../pages/Teacher/ManageRoom/manageRoom.css | 8 ++- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx index 8c3d699..c938f7e 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -25,29 +25,29 @@ const ManageRoom: React.FC = () => { const navigate = useNavigate(); const [socket, setSocket] = useState(null); const [students, setStudents] = useState([]); - const { quizId = '', roomName = '' } = useParams<{ quizId: string, roomName: string }>(); + const { quizId = '', roomName = '' } = useParams<{ quizId: string; roomName: string }>(); const [quizQuestions, setQuizQuestions] = useState(); const [quiz, setQuiz] = useState(null); const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher'); const [connectingError, setConnectingError] = useState(''); const [currentQuestion, setCurrentQuestion] = useState(undefined); const [quizStarted, setQuizStarted] = useState(false); - const [formattedRoomName, setFormattedRoomName] = useState(""); + const [formattedRoomName, setFormattedRoomName] = useState(''); const [newlyConnectedUser, setNewlyConnectedUser] = useState(null); - // Handle the newly connected user in useEffect, because it needs state info + // Handle the newly connected user in useEffect, because it needs state info // not available in the socket.on() callback useEffect(() => { if (newlyConnectedUser) { console.log(`Handling newly connected user: ${newlyConnectedUser.name}`); setStudents((prevStudents) => [...prevStudents, newlyConnectedUser]); - + // only send nextQuestion if the quiz has started if (!quizStarted) { console.log(`!quizStarted: returning.... `); return; } - + if (quizMode === 'teacher') { webSocketService.nextQuestion({ roomName: formattedRoomName, @@ -60,7 +60,7 @@ const ManageRoom: React.FC = () => { } else { console.error('Invalid quiz mode:', quizMode); } - + // Reset the newly connected user state setNewlyConnectedUser(null); } @@ -91,7 +91,7 @@ const ManageRoom: React.FC = () => { return () => { disconnectWebSocket(); }; - }, [roomName, navigate]); + }, [roomName, navigate]); useEffect(() => { if (quizId) { @@ -138,8 +138,8 @@ const ManageRoom: React.FC = () => { setFormattedRoomName(roomNameUpper); console.log(`Creating WebSocket room named ${roomNameUpper}`); - /** - * ATTENTION: Lire les variables d'état dans + /** + * ATTENTION: Lire les variables d'état dans * les .on() n'est pas une bonne pratique. * Les valeurs sont celles au moment de la création * de la fonction et non au moment de l'exécution. @@ -179,7 +179,6 @@ const ManageRoom: React.FC = () => { }; useEffect(() => { - if (socket) { console.log(`Listening for submit-answer-room in room ${formattedRoomName}`); socket.on('submit-answer-room', (answerData: AnswerReceptionFromBackendType) => { @@ -253,10 +252,12 @@ const ManageRoom: React.FC = () => { if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return; setCurrentQuestion(quizQuestions[nextQuestionIndex]); - webSocketService.nextQuestion({roomName: formattedRoomName, - questions: quizQuestions, - questionIndex: nextQuestionIndex, - isLaunch: false}); + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: nextQuestionIndex, + isLaunch: false + }); }; const previousQuestion = () => { @@ -266,7 +267,12 @@ const ManageRoom: React.FC = () => { if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return; setCurrentQuestion(quizQuestions[prevQuestionIndex]); - webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: prevQuestionIndex, isLaunch: false}); + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: prevQuestionIndex, + isLaunch: false + }); }; const initializeQuizQuestion = () => { @@ -294,7 +300,12 @@ const ManageRoom: React.FC = () => { } setCurrentQuestion(quizQuestions[0]); - webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: 0, isLaunch: true}); + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: 0, + isLaunch: true + }); }; const launchStudentMode = () => { @@ -331,11 +342,21 @@ const ManageRoom: React.FC = () => { if (quiz?.content && quizQuestions) { setCurrentQuestion(quizQuestions[questionIndex]); if (quizMode === 'teacher') { - webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex, isLaunch: false}); + webSocketService.nextQuestion({ + roomName: formattedRoomName, + questions: quizQuestions, + questionIndex, + isLaunch: false + }); } } }; + const finishQuiz = () => { + disconnectWebSocket(); + navigate('/teacher/dashboard'); + }; + const handleReturn = () => { disconnectWebSocket(); navigate('/teacher/dashboard'); @@ -382,15 +403,15 @@ const ManageRoom: React.FC = () => { width: '100%' }} > - {( + {
    {students.length}/60
    - )} + }
    @@ -425,7 +446,6 @@ const ManageRoom: React.FC = () => { )} @@ -467,6 +487,11 @@ const ManageRoom: React.FC = () => {
)} +
+ +
) : ( Date: Mon, 24 Mar 2025 03:00:05 -0400 Subject: [PATCH 38/46] Test --- .../pages/ManageRoom/ManageRoom.test.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx index 01957df..2b5a016 100644 --- a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx +++ b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx @@ -294,5 +294,36 @@ describe('ManageRoom', () => { expect(screen.queryByText('Student 1')).not.toBeInTheDocument(); }); }); + + test('terminates the quiz and navigates to teacher dashboard when the "Terminer le quiz" button is clicked', async () => { + await act(async () => { + render( + + + + ); + }); + + await act(async () => { + const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1]; + createSuccessCallback('Test Room'); + }); + + fireEvent.click(screen.getByText('Lancer')); + fireEvent.click(screen.getByText('Rythme du professeur')); + fireEvent.click(screen.getAllByText('Lancer')[1]); + + await waitFor(() => { + expect(screen.getByText('Test Quiz')).toBeInTheDocument(); + }); + + const finishQuizButton = screen.getByText('Terminer le quiz'); + fireEvent.click(finishQuizButton); + + await waitFor(() => { + expect(navigate).toHaveBeenCalledWith('/teacher/dashboard'); + }); + }); + }); From 46c17ba1274907eb1f60e40e9a47f0bf2e9d9a2d Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:34:55 -0400 Subject: [PATCH 39/46] new changes with tests --- .../ShareQuizModal/ShareQuizModal.test.tsx | 133 ++++++------ .../ShareQuizModal/ShareQuizModal.tsx | 93 +++++--- .../src/pages/Teacher/Dashboard/Dashboard.tsx | 10 +- client/src/pages/Teacher/Share/Share.tsx | 203 +++++++++++------- 4 files changed, 261 insertions(+), 178 deletions(-) diff --git a/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx b/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx index 2a878a5..7027eaf 100644 --- a/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx +++ b/client/src/__tests__/components/ShareQuizModal/ShareQuizModal.test.tsx @@ -1,74 +1,81 @@ import React from 'react'; -import { render, fireEvent, screen, waitFor } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import ShareQuizModal from '../../../components/ShareQuizModal/ShareQuizModal'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import ShareQuizModal from '../../../components/ShareQuizModal/ShareQuizModal.tsx'; import { QuizType } from '../../../Types/QuizType'; -import ApiService from '../../../services/ApiService'; - -jest.mock('../../../services/ApiService'); - -Object.assign(navigator, { - clipboard: { - writeText: jest.fn().mockResolvedValue(undefined), - }, -}); - -window.alert = jest.fn(); - -const mockQuiz: QuizType = { - _id: '1', - title: 'Sample Quiz', - content: ['::Question 1:: What is 2+2? {=4 ~3 ~5}'], - folderId: 'folder1', - folderName: 'Sample Folder', - userId: 'user1', - created_at: new Date(), - updated_at: new Date(), -}; +import '@testing-library/jest-dom'; describe('ShareQuizModal', () => { + const mockQuiz: QuizType = { + _id: '123', + folderId: 'folder-123', + folderName: 'Test Folder', + userId: 'user-123', + title: 'Test Quiz', + content: ['Question 1', 'Question 2'], + created_at: new Date(), + updated_at: new Date(), + }; + beforeAll(() => { + // Properly mock the clipboard API + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: jest.fn().mockImplementation(() => Promise.resolve()), + }, + writable: true, + }); + }); - beforeEach(() => { - jest.clearAllMocks(); - }); + afterEach(() => { + jest.clearAllMocks(); + }); - it('should call ApiService.ShareQuiz when sharing by email', async () => { - render(); + it('renders the share button', () => { + render(); + expect(screen.getByLabelText('partager quiz')).toBeInTheDocument(); + expect(screen.getByTestId('ShareIcon')).toBeInTheDocument(); + }); - const shareButton = screen.getByRole('button', { name: /partager quiz/i }); - fireEvent.click(shareButton); + it('copies the quiz URL to clipboard when share button is clicked', async () => { + render(); + const shareButton = screen.getByLabelText('partager quiz'); + + await act(async () => { + fireEvent.click(shareButton); + }); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + `${window.location.origin}/teacher/share/${mockQuiz._id}` + ); + + // Check for feedback dialog content + expect(screen.getByText(/L'URL de partage pour le quiz/i)).toBeInTheDocument(); + expect(screen.getByText(mockQuiz.title)).toBeInTheDocument(); + expect(screen.getByText(/a été copiée\./i)).toBeInTheDocument(); + }); - const emailButton = screen.getByRole('button', { name: /partager par email/i }); - fireEvent.click(emailButton); - - const email = 'test@example.com'; - window.prompt = jest.fn().mockReturnValue(email); - - await fireEvent.click(emailButton); - - expect(ApiService.ShareQuiz).toHaveBeenCalledWith(mockQuiz._id, email); - }); - - it('copies the correct URL to the clipboard when sharing by URL', async () => { - render(); - - // Open the modal - const shareButton = screen.getByRole('button', { name: /partager quiz/i }); - fireEvent.click(shareButton); - - // Click the "Share by URL" button - const shareByUrlButton = screen.getByRole('button', { name: /partager par url/i }); - fireEvent.click(shareByUrlButton); - - // Check if the correct URL was copied - const expectedUrl = `${window.location.origin}/teacher/share/${mockQuiz._id}`; - expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expectedUrl); - - // Check if the alert is shown - await waitFor(() => { - expect(window.alert).toHaveBeenCalledWith('URL a été copiée avec succès.'); - }); - }); + it('shows error message when clipboard write fails', async () => { + // Override the mock to reject + (navigator.clipboard.writeText as jest.Mock).mockRejectedValueOnce(new Error('Clipboard write failed')); + + render(); + const shareButton = screen.getByLabelText('partager quiz'); + + await act(async () => { + fireEvent.click(shareButton); + }); + + expect(screen.getByText(/Une erreur est survenue lors de la copie de l'URL\./i)).toBeInTheDocument(); + }); + it('displays the quiz title in the success message', async () => { + render(); + const shareButton = screen.getByLabelText('partager quiz'); + + await act(async () => { + fireEvent.click(shareButton); + }); + + expect(screen.getByText(mockQuiz.title)).toBeInTheDocument(); + }); }); \ No newline at end of file diff --git a/client/src/components/ShareQuizModal/ShareQuizModal.tsx b/client/src/components/ShareQuizModal/ShareQuizModal.tsx index 336aadb..7d752bc 100644 --- a/client/src/components/ShareQuizModal/ShareQuizModal.tsx +++ b/client/src/components/ShareQuizModal/ShareQuizModal.tsx @@ -1,67 +1,90 @@ import React, { useState } from 'react'; -import { Dialog, DialogTitle, DialogActions, Button, Tooltip, IconButton } from '@mui/material'; +import { Dialog, DialogTitle, DialogActions, Button, Tooltip, IconButton, Typography, Box } from '@mui/material'; import { Share } from '@mui/icons-material'; import { QuizType } from '../../Types/QuizType'; -import ApiService from '../../services/ApiService'; interface ShareQuizModalProps { quiz: QuizType; } const ShareQuizModal: React.FC = ({ quiz }) => { - const [open, setOpen] = useState(false); - - const handleOpenModal = () => setOpen(true); + const [_open, setOpen] = useState(false); + const [feedback, setFeedback] = useState({ + open: false, + title: '', + isError: false + }); const handleCloseModal = () => setOpen(false); - const handleShareByEmail = async () => { - const email = prompt(`Veuillez saisir l'email de la personne avec qui vous souhaitez partager ce quiz`, ""); - - if (email) { - try { - const result = await ApiService.ShareQuiz(quiz._id, email); - - if (!result) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`); - return; - } - - window.alert(`Quiz partagé avec succès!`); - } catch (error) { - console.error('Erreur lors du partage du quiz:', error); - } - } - - handleCloseModal(); - }; - const handleShareByUrl = () => { const quizUrl = `${window.location.origin}/teacher/share/${quiz._id}`; navigator.clipboard.writeText(quizUrl) .then(() => { - window.alert('URL a été copiée avec succès.'); + setFeedback({ + open: true, + title: 'L\'URL de partage pour le quiz', + isError: false + }); }) .catch(() => { - window.alert('Une erreur est survenue lors de la copie de l\'URL.'); + setFeedback({ + open: true, + title: 'Une erreur est survenue lors de la copie de l\'URL.', + isError: true + }); }); handleCloseModal(); }; + const closeFeedback = () => { + setFeedback(prev => ({ ...prev, open: false })); + }; + return ( <> - - + + - - Choisissez une méthode de partage - - - + {/* Feedback Dialog */} + + + + {feedback.isError ? ( + + {feedback.title} + + ) : ( + <> + + L'URL de partage pour le quiz{' '} + + + {quiz.title} + + + {' '}a été copiée. + + + )} + + + + diff --git a/client/src/pages/Teacher/Dashboard/Dashboard.tsx b/client/src/pages/Teacher/Dashboard/Dashboard.tsx index d319515..3d4cc65 100644 --- a/client/src/pages/Teacher/Dashboard/Dashboard.tsx +++ b/client/src/pages/Teacher/Dashboard/Dashboard.tsx @@ -564,7 +564,7 @@ const Dashboard: React.FC = () => { {quizzesByFolder[folderName].map((quiz: QuizType) => (
- +
- + downloadTxtFile(quiz)} @@ -590,7 +590,7 @@ const Dashboard: React.FC = () => { - + handleEditQuiz(quiz)} @@ -600,7 +600,7 @@ const Dashboard: React.FC = () => { - + handleDuplicateQuiz(quiz)} @@ -610,7 +610,7 @@ const Dashboard: React.FC = () => { - + { const [quizTitle, setQuizTitle] = useState(''); const [selectedFolder, setSelectedFolder] = useState(''); - const [folders, setFolders] = useState([]); + const [quizExists, setQuizExists] = useState(false); + const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { - if (!id) { - window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`) - console.error('Quiz not found for id:', id); + try { + if (!id) { + window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`) + console.error('Quiz not found for id:', id); + navigate('/teacher/dashboard'); + return; + } + + if (!ApiService.isLoggedIn()) { + window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`); + navigate("/login"); + return; + } + + const quizIds = await ApiService.getAllQuizIds(); + + if (quizIds.includes(id)) { + setQuizExists(true); + setLoading(false); + return; + } + + const userFolders = await ApiService.getUserFolders(); + + if (userFolders.length == 0) { + window.alert(`Vous n'avez aucun dossier.\nVeuillez en créer un et revenir à ce lien`) + navigate('/teacher/dashboard'); + return; + } + + setFolders(userFolders as FolderType[]); + + const title = await ApiService.getSharedQuiz(id); + + if (!title) { + window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) + console.error('Quiz not found for id:', id); + navigate('/teacher/dashboard'); + return; + } + + setQuizTitle(title); + setLoading(false); + } catch (error) { + console.error('Error fetching data:', error); + setLoading(false); navigate('/teacher/dashboard'); - return; } - - if (!ApiService.isLoggedIn()) { - window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`); - navigate("/login"); - return; - } - - const quizIds = await ApiService.getAllQuizIds(); - - if (quizIds.includes(id)) { - window.alert(`Le quiz que vous essayez d'importer existe déjà sur votre compte.`) - navigate('/teacher/dashboard'); - return; - } - - const userFolders = await ApiService.getUserFolders(); - - if (userFolders.length == 0) { - window.alert(`Vous n'avez aucun dossier.\nVeuillez en créer un et revenir à ce lien`) - navigate('/teacher/dashboard'); - return; - } - - setFolders(userFolders as FolderType[]); - - const title = await ApiService.getSharedQuiz(id); - - if (!title) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) - console.error('Quiz not found for id:', id); - navigate('/teacher/dashboard'); - return; - } - - setQuizTitle(title); }; fetchData(); - }, []); + }, [id, navigate]); const handleSelectFolder = (event: React.ChangeEvent) => { setSelectedFolder(event.target.value); @@ -69,7 +77,6 @@ const Share: React.FC = () => { const handleQuizSave = async () => { try { - if (selectedFolder == '') { alert("Veuillez choisir un dossier"); return; @@ -91,41 +98,87 @@ const Share: React.FC = () => { } }; + if (loading) { + return
Chargement...
; + } + + if (quizExists) { + return ( +
+
+ +
+
Quiz déjà existant
+
+
+
+ +
+ + + Le quiz que vous essayez d'importer existe déjà sur votre compte. + + + + + + Si vous souhaitiez créer une copie de ce quiz, + vous pouvez utiliser la fonction "Dupliquer" disponible + dans votre tableau de bord. + + +
+
+ ); + } + return ( -
-
- -
-
Importation du Quiz: {quizTitle}
-
- Vous êtes sur le point d'importer le quiz {quizTitle}, choisissez un dossier dans lequel enregistrer ce nouveau quiz. +
+
+ +
+
Importation du Quiz: {quizTitle}
+
+ Vous êtes sur le point d'importer le quiz {quizTitle}, choisissez un dossier dans lequel enregistrer ce nouveau quiz. +
+
+
+
+ +
+
+ + + {folders.map((folder: FolderType) => ( + + ))} + + + +
-
-
- -
-
- - - {folders.map((folder: FolderType) => ( - - ))} - - - -
-
-
); }; -export default Share; +export default Share; \ No newline at end of file From 95f914ce3e9d89202a7b49a726f5ff20c186eb41 Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:42:41 -0400 Subject: [PATCH 40/46] share by email code and tests removed --- .../services/ShareQuizService.test.tsx | 40 ------------------- client/src/services/ApiService.tsx | 30 -------------- 2 files changed, 70 deletions(-) diff --git a/client/src/__tests__/services/ShareQuizService.test.tsx b/client/src/__tests__/services/ShareQuizService.test.tsx index c1e473e..1730ad4 100644 --- a/client/src/__tests__/services/ShareQuizService.test.tsx +++ b/client/src/__tests__/services/ShareQuizService.test.tsx @@ -5,46 +5,6 @@ import { ENV_VARIABLES } from '../../constants'; jest.mock('axios'); const mockedAxios = axios as jest.Mocked; -describe('ApiService', () => { - describe('shareQuiz', () => { - it('should call the API to share a quiz and return true on success', async () => { - const quizId = '123'; - const email = 'test@example.com'; - const response = { status: 200 }; - mockedAxios.put.mockResolvedValue(response); - - const result = await ApiService.ShareQuiz(quizId, email); - - expect(mockedAxios.put).toHaveBeenCalledWith( - `${ENV_VARIABLES.VITE_BACKEND_URL}/api/quiz/Share`, - { quizId, email }, - { headers: expect.any(Object) } - ); - expect(result).toBe(true); - }); - - it('should return an error message if the API call fails', async () => { - const quizId = '123'; - const email = 'test@example.com'; - const errorMessage = 'An unexpected error occurred.'; - mockedAxios.put.mockRejectedValue({ response: { data: { error: errorMessage } } }); - - const result = await ApiService.ShareQuiz(quizId, email); - - expect(result).toBe(errorMessage); - }); - - it('should return a generic error message if an unexpected error occurs', async () => { - const quizId = '123'; - const email = 'test@example.com'; - mockedAxios.put.mockRejectedValue(new Error('Unexpected error')); - - const result = await ApiService.ShareQuiz(quizId, email); - - expect(result).toBe('An unexpected error occurred.'); - }); - }); - describe('getSharedQuiz', () => { it('should call the API to get a shared quiz and return the quiz data on success', async () => { const quizId = '123'; diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index fa2b4e7..8f9cc67 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -836,36 +836,6 @@ public async login(email: string, password: string): Promise { } } - async ShareQuiz(quizId: string, email: string): Promise { - try { - if (!quizId || !email) { - throw new Error(`quizId and email are required.`); - } - - const url: string = this.constructRequestUrl(`/quiz/Share`); - const headers = this.constructRequestHeaders(); - const body = { quizId, email }; - - const result: AxiosResponse = await axios.put(url, body, { headers: headers }); - - if (result.status !== 200) { - throw new Error(`Update and share quiz failed. Status: ${result.status}`); - } - - return true; - } catch (error) { - console.log("Error details: ", error); - - if (axios.isAxiosError(error)) { - const err = error as AxiosError; - const data = err.response?.data as { error: string } | undefined; - return data?.error || 'Unknown server error during request.'; - } - - return `An unexpected error occurred.`; - } - } - async getSharedQuiz(quizId: string): Promise { try { if (!quizId) { From a26ffa2880145e9333b843398432a8d4968adbdf Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:56:46 -0400 Subject: [PATCH 41/46] test for already existing quizzes trying to be imported --- .../pages/Teacher/Share/Share.test.tsx | 95 +++++++++++++++++++ client/src/pages/Teacher/Share/Share.tsx | 8 +- 2 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 client/src/__tests__/pages/Teacher/Share/Share.test.tsx diff --git a/client/src/__tests__/pages/Teacher/Share/Share.test.tsx b/client/src/__tests__/pages/Teacher/Share/Share.test.tsx new file mode 100644 index 0000000..9cc3203 --- /dev/null +++ b/client/src/__tests__/pages/Teacher/Share/Share.test.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MemoryRouter, Route, Routes, useParams } from 'react-router-dom'; +import Share from '../../../../pages/Teacher/Share/Share.tsx'; +import ApiService from '../../../../services/ApiService'; +import '@testing-library/jest-dom'; + +// Mock the ApiService and react-router-dom +jest.mock('../../../../services/ApiService'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), + useNavigate: jest.fn(), +})); + +describe('Share Component', () => { + const mockNavigate = jest.fn(); + const mockUseParams = useParams as jest.Mock; + const mockApiService = ApiService as jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + mockUseParams.mockReturnValue({ id: 'quiz123' }); + require('react-router-dom').useNavigate.mockReturnValue(mockNavigate); + }); + + const renderComponent = (initialEntries = ['/share/quiz123']) => { + return render( + + + } /> + Dashboard
} /> + Login
} /> + + + ); + }; + + it('should show loading state initially', () => { + mockApiService.getAllQuizIds.mockResolvedValue([]); + mockApiService.getUserFolders.mockResolvedValue([]); + mockApiService.getSharedQuiz.mockResolvedValue('Test Quiz'); + + renderComponent(); + expect(screen.getByText('Chargement...')).toBeInTheDocument(); + }); + + it('should redirect to login if not authenticated', async () => { + mockApiService.isLoggedIn.mockReturnValue(false); + + renderComponent(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/login'); + }); + }); + + it('should show "quiz already exists" message when quiz exists', async () => { + mockApiService.isLoggedIn.mockReturnValue(true); + mockApiService.getAllQuizIds.mockResolvedValue(['quiz123']); + + renderComponent(); + + await waitFor(() => { + expect(screen.getByText('Quiz déjà existant')).toBeInTheDocument(); + expect(screen.getByText(/Le quiz que vous essayez d'importer existe déjà sur votre compte/i)).toBeInTheDocument(); + expect(screen.getByText('Retour au tableau de bord')).toBeInTheDocument(); + }); + }); + + it('should navigate to dashboard when clicking return button in "quiz exists" view', async () => { + mockApiService.isLoggedIn.mockReturnValue(true); + mockApiService.getAllQuizIds.mockResolvedValue(['quiz123']); + + renderComponent(); + + await waitFor(() => { + fireEvent.click(screen.getByText('Retour au tableau de bord')); + expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard'); + }); + }); + + it('should show error when no folders exist', async () => { + mockApiService.isLoggedIn.mockReturnValue(true); + mockApiService.getAllQuizIds.mockResolvedValue([]); + mockApiService.getUserFolders.mockResolvedValue([]); + + renderComponent(); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard'); + }); + }); + +}); \ No newline at end of file diff --git a/client/src/pages/Teacher/Share/Share.tsx b/client/src/pages/Teacher/Share/Share.tsx index f52ffc3..a1267fb 100644 --- a/client/src/pages/Teacher/Share/Share.tsx +++ b/client/src/pages/Teacher/Share/Share.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { FolderType } from '../../../Types/FolderType'; import './share.css'; -import { Button, NativeSelect, Typography, Box, Divider } from '@mui/material'; +import { Button, NativeSelect, Typography, Box } from '@mui/material'; import ReturnButton from 'src/components/ReturnButton/ReturnButton'; import ApiService from '../../../services/ApiService'; @@ -20,14 +20,12 @@ const Share: React.FC = () => { const fetchData = async () => { try { if (!id) { - window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`) console.error('Quiz not found for id:', id); navigate('/teacher/dashboard'); return; } if (!ApiService.isLoggedIn()) { - window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`); navigate("/login"); return; } @@ -43,7 +41,6 @@ const Share: React.FC = () => { const userFolders = await ApiService.getUserFolders(); if (userFolders.length == 0) { - window.alert(`Vous n'avez aucun dossier.\nVeuillez en créer un et revenir à ce lien`) navigate('/teacher/dashboard'); return; } @@ -53,7 +50,6 @@ const Share: React.FC = () => { const title = await ApiService.getSharedQuiz(id); if (!title) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) console.error('Quiz not found for id:', id); navigate('/teacher/dashboard'); return; @@ -83,7 +79,6 @@ const Share: React.FC = () => { } if (!id) { - window.alert(`Une erreur est survenue.\n Le quiz n'a pas été trouvé\nVeuillez réessayer plus tard`) console.error('Quiz not found for id:', id); navigate('/teacher/dashboard'); return; @@ -93,7 +88,6 @@ const Share: React.FC = () => { navigate('/teacher/dashboard'); } catch (error) { - window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`) console.log(error) } }; From 61bf9b9d86a56281767dab943ccf4343c8b22dba Mon Sep 17 00:00:00 2001 From: Philippe <83185129+phil3838@users.noreply.github.com> Date: Mon, 24 Mar 2025 17:05:01 -0400 Subject: [PATCH 42/46] lint ShareQuizService.test.tsx --- client/src/__tests__/services/ShareQuizService.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/__tests__/services/ShareQuizService.test.tsx b/client/src/__tests__/services/ShareQuizService.test.tsx index 1730ad4..78c1f65 100644 --- a/client/src/__tests__/services/ShareQuizService.test.tsx +++ b/client/src/__tests__/services/ShareQuizService.test.tsx @@ -39,5 +39,4 @@ const mockedAxios = axios as jest.Mocked; expect(result).toBe('An unexpected error occurred.'); }); - }); -}); \ No newline at end of file + }); \ No newline at end of file From d0328f9ec8a5101bb8d847843bebce4434107d64 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 27 Mar 2025 19:18:43 -0400 Subject: [PATCH 43/46] =?UTF-8?q?FIX=20ajustement=20gallery=20demand=C3=A9?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/.env.development | 1 + client/.env.example | 3 ++- .../components/ImageGallery/ImageGallery.tsx | 21 +++++++++++++------ client/src/constants.tsx | 2 +- .../pages/Teacher/EditorQuiz/EditorQuiz.tsx | 1 - server/models/images.js | 2 +- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/client/.env.development b/client/.env.development index e37f392..d1374e8 100644 --- a/client/.env.development +++ b/client/.env.development @@ -1,2 +1,3 @@ VITE_BACKEND_URL=http://localhost:4400 VITE_BACKEND_SOCKET_URL=http://localhost:4400 +VITE_IMG_URL=http://localhost:4400 diff --git a/client/.env.example b/client/.env.example index 944dcbb..787c0f6 100644 --- a/client/.env.example +++ b/client/.env.example @@ -1,2 +1,3 @@ VITE_BACKEND_URL=http://localhost:4400 -VITE_AZURE_BACKEND_URL=http://localhost:4400 \ No newline at end of file +VITE_AZURE_BACKEND_URL=http://localhost:4400 +VITE_IMG_URL=http://localhost:4400 \ No newline at end of file diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index f404b91..cb60aa4 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -22,6 +22,8 @@ import CloseIcon from "@mui/icons-material/Close"; import { ImageType } from "../../Types/ImageType"; import ApiService from "../../services/ApiService"; import { Upload } from "@mui/icons-material"; +import { ENV_VARIABLES } from '../../constants'; +import { escapeForGIFT } from "src/utils/giftUtils"; interface ImagesProps { handleCopy?: (id: string) => void; @@ -63,10 +65,9 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { setLoading(false); if (isDeleted) { - //setImages(images.filter((image) => image.id !== id)); setImgPage(1); fetchImages(); - setSnackbarMessage("Image supprimée avec succès !"); + setSnackbarMessage("Image supprimée avec succès!"); setSnackbarSeverity("success"); } else { setSnackbarMessage("Erreur lors de la suppression de l'image. Veuillez réessayer."); @@ -82,11 +83,18 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { const defaultHandleCopy = (id: string) => { if (navigator.clipboard) { - navigator.clipboard.writeText(id); + const link = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; + const imgTag = `[markdown]![alt_text](${escapeForGIFT(link)} "texte de l'infobulle") {T}`; + setSnackbarMessage("Le lien Markdown de l’image a été copié dans le presse-papiers"); + setSnackbarSeverity("success"); + setSnackbarOpen(true); + navigator.clipboard.writeText(imgTag); + } + if(handleCopy) { + handleCopy(id); } }; - const handleCopyFunction = handleCopy || defaultHandleCopy; const handleDeleteFunction = handleDelete || defaultHandleDelete; const handleImageUpload = (event: React.ChangeEvent) => { @@ -121,9 +129,9 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { setSnackbarSeverity("success"); setSnackbarOpen(true); - // Reset the input field and preview after successful upload setImportedImage(null); setPreview(null); + setTabValue(0); } catch (error) { setSnackbarMessage(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`); setSnackbarSeverity("error"); @@ -135,6 +143,7 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { setSnackbarOpen(false); }; + return ( setTabValue(newValue)}> @@ -162,7 +171,7 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { { e.stopPropagation(); - handleCopyFunction(obj.id); + defaultHandleCopy(obj.id); }} color="primary" data-testid={`gallery-tab-copy-${obj.id}`} > diff --git a/client/src/constants.tsx b/client/src/constants.tsx index fcdd278..0cbbb9f 100644 --- a/client/src/constants.tsx +++ b/client/src/constants.tsx @@ -2,7 +2,7 @@ const ENV_VARIABLES = { MODE: process.env.MODE || "production", VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", - IMG_URL: process.env.MODE == "development" ? process.env.VITE_BACKEND_URL : process.env.IMG_URL, + IMG_URL: process.env.MODE == "development" ? process.env.VITE_BACKEND_URL : process.env.VITE_IMG_URL, BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '', FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : '' }; diff --git a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx index d3989a3..ab7ea13 100644 --- a/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx +++ b/client/src/pages/Teacher/EditorQuiz/EditorQuiz.tsx @@ -171,7 +171,6 @@ const QuizForm: React.FC = () => { const handleCopyImage = (id: string) => { const escLink = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; - navigator.clipboard.writeText(id); setImageLinks(prevLinks => [...prevLinks, escLink]); } diff --git a/server/models/images.js b/server/models/images.js index 16e52de..e3b6afd 100644 --- a/server/models/images.js +++ b/server/models/images.js @@ -82,7 +82,7 @@ class Images { if (!total || total === 0) return { images: [], total }; const result = await imagesCollection.find({ userId: uid }) - .sort({ created_at: 1 }) + .sort({ created_at: -1 }) .skip((page - 1) * limit) .limit(limit) .toArray(); From 9205c0c6bf7f6db2893b28f693d27bd4a449d428 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 27 Mar 2025 19:24:18 -0400 Subject: [PATCH 44/46] FIX test changement espacement --- .../src/__tests__/components/ImageGallery/ImageGallery.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx index d60a0c9..d2b3fd0 100644 --- a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx +++ b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx @@ -90,7 +90,7 @@ describe("ImageGallery", () => { await waitFor(() => { expect(screen.queryByAltText("Image image1.jpg")).toBeNull(); - expect(screen.getByText("Image supprimée avec succès !")).toBeInTheDocument(); + expect(screen.getByText("Image supprimée avec succès!")).toBeInTheDocument(); }); }); From 8f6cf8cffa3236522288ce94a82cd11de107221d Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 27 Mar 2025 19:51:30 -0400 Subject: [PATCH 45/46] ajustement route --- server/routers/images.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/images.js b/server/routers/images.js index f2d601a..94d2802 100644 --- a/server/routers/images.js +++ b/server/routers/images.js @@ -13,7 +13,7 @@ const upload = multer({ storage: storage }); router.post("/upload", jwt.authenticate, upload.single('image'), asyncHandler(images.upload)); router.get("/get/:id", asyncHandler(images.get)); router.get("/getImages", asyncHandler(images.getImages)); -router.get("/getUserImages", asyncHandler(images.getUserImages)); -router.delete("/delete", asyncHandler(images.delete)); +router.get("/getUserImages", jwt.authenticate, asyncHandler(images.getUserImages)); +router.delete("/delete", jwt.authenticate, asyncHandler(images.delete)); module.exports = router; From c13e73208e3f69408a69ff45936e9d3c3ba13958 Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Mon, 7 Apr 2025 15:43:41 -0400 Subject: [PATCH 46/46] franciser, npm audit fix --- client/package-lock.json | 62 ++++++++----------- client/package.json | 2 +- .../ImageGallery/ImageGallery.test.tsx | 2 +- .../components/ImageGallery/ImageGallery.tsx | 2 +- 4 files changed, 30 insertions(+), 38 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 33cb29c..e066048 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -43,9 +43,9 @@ "@babel/preset-typescript": "^7.23.3", "@eslint/js": "^9.21.0", "@testing-library/dom": "^10.4.0", - "@testing-library/user-event": "^14.6.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.1", "@types/jest": "^29.5.13", "@types/node": "^22.13.5", "@types/react": "^18.2.15", @@ -420,26 +420,24 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1894,10 +1892,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", - "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1906,14 +1903,13 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -1947,10 +1943,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -4441,7 +4436,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "dev": true, - "license": "MIT", "engines": { "node": ">=12", "npm": ">=6" @@ -5350,10 +5344,9 @@ } }, "node_modules/axios": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", - "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", - "license": "MIT", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -12925,10 +12918,9 @@ } }, "node_modules/vite": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", - "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", - "license": "MIT", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", + "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", diff --git a/client/package.json b/client/package.json index 78f60be..e980a43 100644 --- a/client/package.json +++ b/client/package.json @@ -47,9 +47,9 @@ "@babel/preset-typescript": "^7.23.3", "@eslint/js": "^9.21.0", "@testing-library/dom": "^10.4.0", - "@testing-library/user-event": "^14.6.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.1", "@types/jest": "^29.5.13", "@types/node": "^22.13.5", "@types/react": "^18.2.15", diff --git a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx index d2b3fd0..5586a6a 100644 --- a/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx +++ b/client/src/__tests__/components/ImageGallery/ImageGallery.test.tsx @@ -38,7 +38,7 @@ describe("ImageGallery", () => { it("should render images correctly", async () => { await act(async () => { - await screen.findByText("Gallery"); + await screen.findByText("Galerie"); }); expect(screen.getByAltText("Image image1.jpg")).toBeInTheDocument(); diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index cb60aa4..ff5c6e8 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -147,7 +147,7 @@ const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { return ( setTabValue(newValue)}> - + {tabValue === 0 && (