Merge pull request #97 from ets-cfuhrman-pfe/fuhrmanator/issue64

Fuhrmanator/issue64 Supporter le format HTML, mises à jour des tests du frontend
This commit is contained in:
Christopher (Cris) Fuhrman 2024-09-15 22:50:38 -04:00 committed by GitHub
commit 3c699a3379
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1260 additions and 1809 deletions

View file

@ -14,15 +14,15 @@ jobs:
steps: steps:
- name: Check Out Repo - name: Check Out Repo
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: '18'
- name: Install Dependencies - name: Install Dependencies
run: npm install run: npm ci
working-directory: ./client working-directory: ./client
- name: Run Tests - name: Run Tests

2539
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,51 +18,51 @@
"@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@mui/icons-material": "^5.14.18", "@mui/icons-material": "^6.1.0",
"@mui/lab": "^5.0.0-alpha.153", "@mui/lab": "^5.0.0-alpha.153",
"@mui/material": "^5.15.11", "@mui/material": "^6.1.0",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"axios": "^1.6.7", "axios": "^1.6.7",
"esbuild": "^0.20.2", "esbuild": "^0.23.1",
"gift-pegjs": "^0.2.1", "gift-pegjs": "^1.0.2",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"katex": "^0.16.9", "katex": "^0.16.11",
"marked": "^9.1.2", "marked": "^14.1.2",
"nanoid": "^5.0.2", "nanoid": "^5.0.2",
"react": "^18.2.0", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.3.1",
"react-latex": "^1.2.0", "react-latex": "^2.0.0",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-router-dom": "^6.16.0", "react-router-dom": "^6.26.2",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"socket.io-client": "^4.7.2", "socket.io-client": "^4.7.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vite-plugin-checker": "^0.6.2" "vite-plugin-checker": "^0.8.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.23.3", "@babel/preset-env": "^7.23.3",
"@babel/preset-react": "^7.23.3", "@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3", "@babel/preset-typescript": "^7.23.3",
"@testing-library/jest-dom": "^6.1.4", "@testing-library/dom": "^10.4.0",
"@testing-library/react": "^14.1.0", "@testing-library/jest-dom": "^6.5.0",
"@types/jest": "^29.5.8", "@testing-library/react": "^16.0.1",
"@types/node": "^20.8.8", "@types/jest": "^29.5.13",
"@types/node": "^22.5.5",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/react-latex": "^2.0.3", "@types/react-latex": "^2.0.3",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^8.5.0",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.3.2",
"eslint": "^8.45.0", "eslint": "^9.10.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^5.1.0-rc-206df66e-20240912",
"eslint-plugin-react-refresh": "^0.4.3", "eslint-plugin-react-refresh": "^0.4.12",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"typescript": "^5.0.2", "typescript": "^5.6.2",
"vite": "^4.4.5", "vite": "^5.4.5",
"vite-plugin-environment": "^1.1.3", "vite-plugin-environment": "^1.1.3"
"vite-plugin-rewrite-all": "^1.0.1"
} }
} }

View file

@ -27,7 +27,7 @@ const handleLogout = () => {
} }
const isLoggedIn = () => { const isLoggedIn = () => {
return ApiService.isLogedIn(); return ApiService.isLoggedIn();
} }
function App() { function App() {

View file

@ -63,8 +63,18 @@ describe('TextType', () => {
text: '**Bold**', text: '**Bold**',
format: 'markdown' format: 'markdown'
}; };
const expectedOutput = '<strong>Bold</strong>'; // TODO: investigate why the output has an extra newline
expect(TextType({ text: input })).toContain(expectedOutput); const expectedOutput = '<strong>Bold</strong>\n';
expect(TextType({ text: input })).toBe(expectedOutput);
});
it('should format text with HTML correctly', () => {
const input: TextFormat = {
text: '<em>yes</em>',
format: 'html'
};
const expectedOutput = '<em>yes</em>';
expect(TextType({ text: input })).toBe(expectedOutput);
}); });
it('should format plain text correctly', () => { it('should format plain text correctly', () => {

View file

@ -1,6 +1,8 @@
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import MultipleChoiceQuestion from '../../../../components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion'; import MultipleChoiceQuestion from '../../../../components/Questions/MultipleChoiceQuestion/MultipleChoiceQuestion';
import { act } from 'react';
import { MemoryRouter } from 'react-router-dom';
const questionStem = 'Question stem'; const questionStem = 'Question stem';
const sampleFeedback = 'Feedback'; const sampleFeedback = 'Feedback';
@ -14,11 +16,13 @@ describe('MultipleChoiceQuestion', () => {
beforeEach(() => { beforeEach(() => {
render( render(
<MultipleChoiceQuestion <MemoryRouter>
globalFeedback={sampleFeedback} <MultipleChoiceQuestion
choices={choices} globalFeedback={sampleFeedback}
handleOnSubmitAnswer={mockHandleOnSubmitAnswer} questionContent={{text: questionStem, format: 'plain'}} /> choices={choices}
); handleOnSubmitAnswer={mockHandleOnSubmitAnswer}
questionStem={{ text: questionStem, format: 'plain' }} />
</MemoryRouter>);
}); });
test('renders the question and choices', () => { test('renders the question and choices', () => {
@ -30,16 +34,24 @@ describe('MultipleChoiceQuestion', () => {
test('does not submit when no answer is selected', () => { test('does not submit when no answer is selected', () => {
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
fireEvent.click(submitButton); act(() => {
fireEvent.click(submitButton);
});
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled(); expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
}); });
test('submits the selected answer', () => { test('submits the selected answer', () => {
const choiceButton = screen.getByText('Choice 1').closest('button'); const choiceButton = screen.getByText('Choice 1').closest('button');
if (!choiceButton) throw new Error('Choice button not found'); if (!choiceButton) throw new Error('Choice button not found');
fireEvent.click(choiceButton); act(() => {
fireEvent.click(choiceButton);
});
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
fireEvent.click(submitButton); act(() => {
fireEvent.click(submitButton);
});
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith('Choice 1'); expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith('Choice 1');
}); });
}); });

View file

@ -12,8 +12,8 @@ describe('Questions Component', () => {
type: 'TF', type: 'TF',
stem: { format: 'plain', text: 'Sample True/False Question' }, stem: { format: 'plain', text: 'Sample True/False Question' },
isTrue: true, isTrue: true,
incorrectFeedback: null, falseFeedback: null,
correctFeedback: null, trueFeedback: null,
title: 'True/False Question', title: 'True/False Question',
hasEmbeddedAnswers: false, hasEmbeddedAnswers: false,
globalFeedback: null, globalFeedback: null,

View file

@ -1,7 +1,8 @@
// TrueFalseQuestion.test.tsx // TrueFalseQuestion.test.tsx
import { render, fireEvent, screen } from '@testing-library/react'; import { render, fireEvent, screen, act } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import TrueFalseQuestion from '../../../../components/Questions/TrueFalseQuestion/TrueFalseQuestion'; import TrueFalseQuestion from '../../../../components/Questions/TrueFalseQuestion/TrueFalseQuestion';
import { MemoryRouter } from 'react-router-dom';
describe('TrueFalseQuestion Component', () => { describe('TrueFalseQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn(); const mockHandleSubmitAnswer = jest.fn();
@ -15,7 +16,10 @@ describe('TrueFalseQuestion Component', () => {
}; };
beforeEach(() => { beforeEach(() => {
render(<TrueFalseQuestion questionContent={{text: sampleStem, format: 'plain'}} {...sampleProps} />); render(
<MemoryRouter>
<TrueFalseQuestion questionContent={{ text: sampleStem, format: 'plain' }} {...sampleProps} />
</MemoryRouter>);
}); });
it('renders correctly', () => { it('renders correctly', () => {
@ -27,14 +31,14 @@ describe('TrueFalseQuestion Component', () => {
it('Submit button should be disabled if no option is selected', () => { it('Submit button should be disabled if no option is selected', () => {
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
expect(submitButton).toBeDisabled(); expect(submitButton).toBeDisabled();
}); });
it('not submit answer if no option is selected', () => { it('not submit answer if no option is selected', () => {
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
act(() => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
});
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled(); expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
}); });
@ -43,9 +47,13 @@ describe('TrueFalseQuestion Component', () => {
const trueButton = screen.getByText('Vrai'); const trueButton = screen.getByText('Vrai');
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
fireEvent.click(trueButton); act(() => {
fireEvent.click(trueButton);
});
fireEvent.click(submitButton); act(() => {
fireEvent.click(submitButton);
});
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(true); expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(true);
}); });
@ -53,10 +61,12 @@ describe('TrueFalseQuestion Component', () => {
it('submits answer correctly for False', () => { it('submits answer correctly for False', () => {
const falseButton = screen.getByText('Faux'); const falseButton = screen.getByText('Faux');
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
act(() => {
fireEvent.click(falseButton); fireEvent.click(falseButton);
});
fireEvent.click(submitButton); act(() => {
fireEvent.click(submitButton);
});
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(false); expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(false);
}); });

View file

@ -1,141 +1,95 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { render, screen, fireEvent, act } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { parse } from 'gift-pegjs';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { QuestionType } from '../../../../Types/QuestionType'; import { QuestionType } from '../../../../Types/QuestionType';
import StudentModeQuiz from '../../../../components/StudentModeQuiz/StudentModeQuiz'; import StudentModeQuiz from '../../../../components/StudentModeQuiz/StudentModeQuiz';
const mockQuestions: QuestionType[] = [ const mockGiftQuestions = parse(
{ `::Sample Question 1:: Sample Question 1 {=Option A ~Option B}
question: {
id: '1', ::Sample Question 2:: Sample Question 2 {T}`);
type: 'MC',
stem: { format: 'plain', text: 'Sample Question 1' }, const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
title: 'Sample Question 1', question.id = (index + 1).toString();
hasEmbeddedAnswers: false, const newMockQuestion: QuestionType = {
globalFeedback: null, question: question,
choices: [ };
{ text: { format: 'plain', text: 'Option A' }, isCorrect: true, weight: 1, feedback: null }, return newMockQuestion;
{ text: { format: 'plain', text: 'Option B' }, isCorrect: false, weight: 0, feedback: null }, });
],
},
},
{
question: {
id: '2',
type: 'TF',
stem: { format: 'plain', text: 'Sample Question 2' },
isTrue: true,
incorrectFeedback: null,
correctFeedback: null,
title: 'Question 2',
hasEmbeddedAnswers: false,
globalFeedback: null,
},
},
];
const mockSubmitAnswer = jest.fn(); const mockSubmitAnswer = jest.fn();
const mockDisconnectWebSocket = jest.fn(); const mockDisconnectWebSocket = jest.fn();
describe('StudentModeQuiz', () => { beforeEach(() => {
test('renders the initial question', async () => { render(
render( <MemoryRouter>
<MemoryRouter> <StudentModeQuiz
<StudentModeQuiz
questions={mockQuestions}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket}
/>
</MemoryRouter>
);
// wait for the question to be rendered
await waitFor(() => {
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
expect(screen.getByText('Option A')).toBeInTheDocument();
expect(screen.getByText('Option B')).toBeInTheDocument();
expect(screen.getByText('Quitter')).toBeInTheDocument();
});
});
test('handles answer submission text', async () => {
render(
<MemoryRouter>
<StudentModeQuiz
questions={mockQuestions} questions={mockQuestions}
submitAnswer={mockSubmitAnswer} submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket} disconnectWebSocket={mockDisconnectWebSocket}
/> />
</MemoryRouter> </MemoryRouter>);
); });
fireEvent.click(screen.getByText('Option A')); describe('StudentModeQuiz', () => {
fireEvent.click(screen.getByText('Répondre')); test('renders the initial question', async () => {
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
expect(screen.getByText('Option A')).toBeInTheDocument();
expect(screen.getByText('Option B')).toBeInTheDocument();
expect(screen.getByText('Quitter')).toBeInTheDocument();
});
await waitFor(() => { test('handles answer submission text', async () => {
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1'); act(() => {
fireEvent.click(screen.getByText('Option A'));
}); });
act(() => {
fireEvent.click(screen.getByText('Répondre'));
});
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1');
}); });
test('handles quit button click', async () => { test('handles quit button click', async () => {
render( act(() => {
<MemoryRouter> fireEvent.click(screen.getByText('Quitter'));
<StudentModeQuiz
questions={mockQuestions}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket}
/>
</MemoryRouter>);
fireEvent.click(screen.getByText('Quitter'));
await waitFor(() => {
expect(mockDisconnectWebSocket).toHaveBeenCalled();
}); });
expect(mockDisconnectWebSocket).toHaveBeenCalled();
}); });
test('navigates to the next question', async () => { test('navigates to the next question', async () => {
render( act(() => {
<MemoryRouter> fireEvent.click(screen.getByText('Option A'));
<StudentModeQuiz
questions={mockQuestions}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket}
/>
</MemoryRouter>);
fireEvent.click(screen.getByText('Option A'));
fireEvent.click(screen.getByText('Répondre'));
fireEvent.click(screen.getByText('Question suivante'));
await waitFor(() => {
const sampleQuestionElements = screen.queryAllByText(/Sample question 2/i);
expect(sampleQuestionElements.length).toBeGreaterThan(0);
expect(screen.getByText('V')).toBeInTheDocument();
}); });
act(() => {
fireEvent.click(screen.getByText('Répondre'));
});
act(() => {
fireEvent.click(screen.getByText('Question suivante'));
});
const sampleQuestionElements = screen.queryAllByText(/Sample question 2/i);
expect(sampleQuestionElements.length).toBeGreaterThan(0);
expect(screen.getByText('V')).toBeInTheDocument();
}); });
test('navigates to the previous question', async () => { test('navigates to the previous question', async () => {
render( act(() => {
<MemoryRouter> fireEvent.click(screen.getByText('Option A'));
<StudentModeQuiz
questions={mockQuestions}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket}
/>
</MemoryRouter>);
fireEvent.click(screen.getByText('Option A'));
fireEvent.click(screen.getByText('Répondre'));
fireEvent.click(screen.getByText('Question précédente'));
await waitFor(() => {
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
expect(screen.getByText('Option B')).toBeInTheDocument();
}); });
act(() => {
fireEvent.click(screen.getByText('Répondre'));
});
act(() => {
fireEvent.click(screen.getByText('Question précédente'));
});
expect(screen.getByText('Sample Question 1')).toBeInTheDocument();
expect(screen.getByText('Option B')).toBeInTheDocument();
}); });
}); });

View file

@ -1,36 +1,31 @@
//TeacherModeQuiz.test.tsx //TeacherModeQuiz.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'; import { render, fireEvent, act } from '@testing-library/react';
import { screen } from '@testing-library/dom';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { GIFTQuestion } from 'gift-pegjs'; import { parse } from 'gift-pegjs';
import TeacherModeQuiz from '../../../../components/TeacherModeQuiz/TeacherModeQuiz'; import TeacherModeQuiz from '../../../../components/TeacherModeQuiz/TeacherModeQuiz';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
// import { mock } from 'node:test';
const mockGiftQuestions = parse(
`::Sample Question:: Sample Question {=Option A ~Option B}`);
describe('TeacherModeQuiz', () => { describe('TeacherModeQuiz', () => {
const mockQuestion: GIFTQuestion = { const mockQuestion = mockGiftQuestions[0];
id: '1', mockQuestion.id = '1';
type: 'MC',
stem: { format: 'plain', text: 'Sample Question' },
title: 'Sample Question',
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: [
{ text: { format: 'plain', text: 'Option A' }, isCorrect: true, weight: 1, feedback: null },
{ text: { format: 'plain', text: 'Option B' }, isCorrect: false, weight: 0, feedback: null },
],
};
const mockSubmitAnswer = jest.fn(); const mockSubmitAnswer = jest.fn();
const mockDisconnectWebSocket = jest.fn(); const mockDisconnectWebSocket = jest.fn();
beforeEach(() => { beforeEach(async () => {
render( render(
<MemoryRouter> <MemoryRouter>
<TeacherModeQuiz <TeacherModeQuiz
questionInfos={{ question: mockQuestion }} questionInfos={{ question: mockQuestion }}
submitAnswer={mockSubmitAnswer} submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket} disconnectWebSocket={mockDisconnectWebSocket} />
/>
</MemoryRouter> </MemoryRouter>
); );
}); });
@ -45,16 +40,21 @@ describe('TeacherModeQuiz', () => {
}); });
test('handles answer submission and displays wait text', () => { test('handles answer submission and displays wait text', () => {
fireEvent.click(screen.getByText('Option A'));
fireEvent.click(screen.getByText('Répondre'));
act(() => {
fireEvent.click(screen.getByText('Option A'));
});
act(() => {
fireEvent.click(screen.getByText('Répondre'));
});
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1'); expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', '1');
expect(screen.getByText('En attente pour la prochaine question...')).toBeInTheDocument(); expect(screen.getByText('En attente pour la prochaine question...')).toBeInTheDocument();
}); });
test('handles disconnect button click', () => { test('handles disconnect button click', () => {
fireEvent.click(screen.getByText('Quitter')); act(() => {
fireEvent.click(screen.getByText('Quitter'));
});
expect(mockDisconnectWebSocket).toHaveBeenCalled(); expect(mockDisconnectWebSocket).toHaveBeenCalled();
}); });
}); });

View file

@ -1,28 +1,30 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import Dashboard from '../../../../pages/Teacher/Dashboard/Dashboard'; import Dashboard from '../../../../pages/Teacher/Dashboard/Dashboard';
import { act } from 'react';
const localStorageMock = (() => { // const localStorageMock = (() => {
let store: Record<string, string> = {}; // let store: Record<string, string> = {};
return { // return {
getItem: (key: string) => store[key] || null, // getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => (store[key] = value.toString()), // setItem: (key: string, value: string) => (store[key] = value.toString()),
clear: () => (store = {}), // clear: () => (store = {}),
}; // };
})(); // })();
Object.defineProperty(window, 'localStorage', { value: localStorageMock }); // Object.defineProperty(window, 'localStorage', { value: localStorageMock });
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useNavigate: jest.fn(), useNavigate: jest.fn(),
})); }));
// NOTE: these tests are for an old version that kept quizzes in local storage and had no login
describe.skip('Dashboard Component', () => { describe.skip('Dashboard Component', () => {
beforeEach(() => { // beforeEach(() => {
localStorage.setItem('quizzes', JSON.stringify([])); // localStorage.setItem('quizzes', JSON.stringify([]));
}); // });
test('renders Dashboard with default state', () => { test('renders Dashboard with default state', () => {
render( render(
@ -33,7 +35,7 @@ describe.skip('Dashboard Component', () => {
expect(screen.getByText(/Tableau de bord/i)).toBeInTheDocument(); expect(screen.getByText(/Tableau de bord/i)).toBeInTheDocument();
}); });
test('adds a quiz and checks if it is displayed', () => { test.skip('adds a quiz and checks if it is displayed', () => {
const mockQuizzes = [ const mockQuizzes = [
{ {
id: '1', id: '1',
@ -52,17 +54,18 @@ describe.skip('Dashboard Component', () => {
expect(screen.getByText(/Sample Quiz/i)).toBeInTheDocument(); expect(screen.getByText(/Sample Quiz/i)).toBeInTheDocument();
}); });
test('opens ImportModal when "Importer" button is clicked', async () => { test.skip('opens ImportModal when "Importer" button is clicked', async () => {
render( render(
<MemoryRouter> <MemoryRouter>
<Dashboard /> <Dashboard />
</MemoryRouter> </MemoryRouter>
); );
fireEvent.click(screen.getByText(/Importer/i)); act(() => {
await waitFor(() => { fireEvent.click(screen.getByText(/Importer/i));
expect(screen.getByText(/Importation de quiz/i)).toBeInTheDocument();
}); });
expect(screen.getByText(/Importation de quiz/i)).toBeInTheDocument();
}); });
}); });

View file

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

View file

@ -67,8 +67,8 @@ const multiple: GIFTQuestion[] = [
hasEmbeddedAnswers: false, hasEmbeddedAnswers: false,
globalFeedback: null, globalFeedback: null,
isTrue: false, isTrue: false,
incorrectFeedback: null, falseFeedback: null,
correctFeedback: null trueFeedback: null
}, },
{ {
type: 'Short', type: 'Short',

View file

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

View file

@ -13,8 +13,8 @@ export default function TrueFalse({
title, title,
isTrue, isTrue,
stem, stem,
correctFeedback, trueFeedback: trueFeedback,
incorrectFeedback, falseFeedback: falseFeedback,
globalFeedback globalFeedback
}: TrueFalseOptions): string { }: TrueFalseOptions): string {
const choices: TextChoice[] = [ const choices: TextChoice[] = [
@ -25,7 +25,7 @@ export default function TrueFalse({
}, },
isCorrect: isTrue, isCorrect: isTrue,
weight: null, weight: null,
feedback: isTrue ? correctFeedback : incorrectFeedback feedback: isTrue ? trueFeedback : falseFeedback
}, },
{ {
text: { text: {
@ -34,7 +34,7 @@ export default function TrueFalse({
}, },
isCorrect: !isTrue, isCorrect: !isTrue,
weight: null, weight: null,
feedback: !isTrue ? correctFeedback : incorrectFeedback feedback: !isTrue ? trueFeedback : falseFeedback
} }
]; ];

View file

@ -12,31 +12,9 @@ export interface DisplayOptions {
preview: boolean; preview: boolean;
} }
export type QuestionType = export {
| 'Description' QuestionType, FormatType, NumericalType, TextFormat, NumericalFormat, TextChoice, NumericalChoice, Question, Description, Category, MultipleChoice, ShortAnswer, Numerical, Essay, TrueFalse,
| 'Category' Matching, Match, GIFTQuestion } from 'gift-pegjs';
| 'MC'
| 'Numerical'
| 'Short'
| 'Essay'
| 'TF'
| 'Matching';
export type FormatType = 'moodle' | 'html' | 'markdown' | 'plain';
export type NumericalType = 'simple' | 'range' | 'high-low';
export interface TextFormat {
format: FormatType;
text: string;
}
export interface NumericalFormat {
type: NumericalType;
number?: number;
range?: number;
numberHigh?: number;
numberLow?: number;
}
export interface Choice { export interface Choice {
isCorrect: boolean; isCorrect: boolean;
@ -44,77 +22,3 @@ export interface Choice {
text: TextFormat | NumericalFormat; text: TextFormat | NumericalFormat;
feedback: TextFormat | null; feedback: TextFormat | null;
} }
export interface TextChoice extends Choice {
text: TextFormat;
}
export interface NumericalChoice extends Choice {
text: NumericalFormat;
}
export interface Question {
type: QuestionType;
title: string | null;
stem: TextFormat;
hasEmbeddedAnswers: boolean;
globalFeedback: TextFormat | null;
}
export interface Description {
type: Extract<QuestionType, 'Description'>;
title: string | null;
stem: TextFormat;
hasEmbeddedAnswers: boolean;
}
export interface Category {
type: Extract<QuestionType, 'Category'>;
title: string;
}
export interface MultipleChoice extends Question {
type: Extract<QuestionType, 'MC'>;
choices: TextChoice[];
}
export interface ShortAnswer extends Question {
type: Extract<QuestionType, 'Short'>;
choices: TextChoice[];
}
export interface Numerical extends Question {
type: Extract<QuestionType, 'Numerical'>;
choices: NumericalChoice[] | NumericalFormat;
}
export interface Essay extends Question {
type: Extract<QuestionType, 'Essay'>;
}
export interface TrueFalse extends Question {
type: Extract<QuestionType, 'TF'>;
isTrue: boolean;
incorrectFeedback: TextFormat | null;
correctFeedback: TextFormat | null;
}
export interface Matching extends Question {
type: Extract<QuestionType, 'Matching'>;
matchPairs: Match[];
}
export interface Match {
subquestion: TextFormat;
subanswer: string;
}
export type GIFTQuestion =
| Description
| Category
| MultipleChoice
| ShortAnswer
| Numerical
| Essay
| TrueFalse
| Matching;

View file

@ -14,7 +14,7 @@ type Choices = {
}; };
interface Props { interface Props {
questionContent: TextFormat; questionStem: TextFormat;
choices: Choices[]; choices: Choices[];
globalFeedback?: string | undefined; globalFeedback?: string | undefined;
handleOnSubmitAnswer?: (answer: string) => void; handleOnSubmitAnswer?: (answer: string) => void;
@ -22,7 +22,7 @@ interface Props {
} }
const MultipleChoiceQuestion: React.FC<Props> = (props) => { const MultipleChoiceQuestion: React.FC<Props> = (props) => {
const { questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props; const { questionStem: questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props;
const [answer, setAnswer] = useState<string>(); const [answer, setAnswer] = useState<string>();
useEffect(() => { useEffect(() => {

View file

@ -41,7 +41,7 @@ const Question: React.FC<QuestionProps> = ({
case 'MC': case 'MC':
questionTypeComponent = ( questionTypeComponent = (
<MultipleChoiceQuestion <MultipleChoiceQuestion
questionContent={question.stem} questionStem={question.stem}
choices={question.choices} choices={question.choices}
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer} showAnswer={showAnswer}

View file

@ -27,7 +27,7 @@ const TrueFalseQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: TextType({text: questionContent}) }} /> <div dangerouslySetInnerHTML={{ __html: TextType({ text: questionContent }) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
<Button <Button

View file

@ -43,7 +43,7 @@ const Dashboard: React.FC = () => {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
if (!ApiService.isLogedIn()) { if (!ApiService.isLoggedIn()) {
navigate("/teacher/login"); navigate("/teacher/login");
return; return;
} }

View file

@ -31,7 +31,7 @@ const Share: React.FC = () => {
return; return;
} }
if (!ApiService.isLogedIn()) { if (!ApiService.isLoggedIn()) {
window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`); window.alert(`Vous n'êtes pas connecté.\nVeuillez vous connecter et revenir à ce lien`);
navigate("/teacher/login"); navigate("/teacher/login");
return; return;

View file

@ -18,7 +18,7 @@ class ApiService {
} }
private constructRequestHeaders(): any { private constructRequestHeaders(): any {
if (this.isLogedIn()) { if (this.isLoggedIn()) {
return { return {
Authorization: `Bearer ${this.getToken()}`, Authorization: `Bearer ${this.getToken()}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -63,7 +63,7 @@ class ApiService {
return object.token; return object.token;
} }
public isLogedIn(): boolean { public isLoggedIn(): boolean {
const token = this.getToken() const token = this.getToken()
if (token == null) { if (token == null) {

View file

@ -5,7 +5,7 @@ class WebSocketService {
private socket: Socket | null = null; private socket: Socket | null = null;
connect(backendUrl: string): Socket { connect(backendUrl: string): Socket {
console.log(backendUrl); // console.log(backendUrl);
this.socket = io(`${backendUrl}`, { this.socket = io(`${backendUrl}`, {
transports: ['websocket'], transports: ['websocket'],
reconnectionAttempts: 1 reconnectionAttempts: 1