diff --git a/cypress/e2e/play/fillBlanks.cy.ts b/cypress/e2e/play/fillBlanks.cy.ts index 031567b5..d33b1279 100644 --- a/cypress/e2e/play/fillBlanks.cy.ts +++ b/cypress/e2e/play/fillBlanks.cy.ts @@ -13,6 +13,7 @@ import { import { FILL_BLANKS_CORRECTION_CY, PLAY_VIEW_QUESTION_TITLE_CY, + PLAY_VIEW_RETRY_BUTTON_CY, PLAY_VIEW_SUBMIT_BUTTON_CY, buildBlankedTextWordCy, buildFillBlanksAnswerId, @@ -153,6 +154,20 @@ const removeAnswer = (answer: Word, shouldFail: boolean) => { } }; +const checkBadAnswersAreReset = (state: { answers: Word[]; words: Word[] }) => { + const correctAnswersId = state.words + .filter((w) => w.displayed === w.text) + .map((w) => w.id); + + state.answers.forEach((answer) => { + if (correctAnswersId.find((id) => id === answer.id)) { + cy.get(`[data-id="${answer.id}"]`).should('contain', answer.text); + } else { + cy.get(`[data-id="${answer.id}"]`).should('contain', ''); + } + }); +}; + describe('Play Fill In The Blanks', () => { describe('Only 1 attempt', () => { const NUMBER_OF_ATTEMPTS = 1; @@ -440,7 +455,17 @@ describe('Play Fill In The Blanks', () => { // hints should be displayed cy.checkHintsPlay(fillBlanksAppSettingsData.hints); - removeAnswer(answers[0], false); + // check that user have to click on retry before updating their answer + removeAnswer(answers[2], true); + cy.get(dataCyWrapper(PLAY_VIEW_RETRY_BUTTON_CY)).click(); + + // check that the input is reset on retry and keep only correct answers + checkBadAnswersAreReset( + splitSentence(partiallyCorrectAppData.data.text) + ); + + removeAnswer(answers[2], false); + checkInputDisabled(false); cy.checkQuizNavigation({ questionId: id, @@ -475,7 +500,8 @@ describe('Play Fill In The Blanks', () => { // hints should be displayed cy.checkHintsPlay(fillBlanksAppSettingsData.hints); - removeAnswer(answers[0], false); + cy.get(dataCyWrapper(PLAY_VIEW_RETRY_BUTTON_CY)).click(); + checkInputDisabled(false); cy.checkQuizNavigation({ questionId: id, @@ -510,8 +536,8 @@ describe('Play Fill In The Blanks', () => { // we do not check correction: nothing matches // but we want to know that the app didn't crash - removeAnswer(answers[0], false); - checkInputDisabled(false); + cy.get(dataCyWrapper(PLAY_VIEW_RETRY_BUTTON_CY)).click(); + cy.checkQuizNavigation({ questionId: id, numberOfAttempts: NUMBER_OF_ATTEMPTS, diff --git a/src/components/play/PlayFillInTheBlanks.tsx b/src/components/play/PlayFillInTheBlanks.tsx index 0715a5a9..b67c890b 100644 --- a/src/components/play/PlayFillInTheBlanks.tsx +++ b/src/components/play/PlayFillInTheBlanks.tsx @@ -21,6 +21,34 @@ import Answers from './fillInTheBlanks/Answers'; import BlankedText from './fillInTheBlanks/BlankedText'; import Correction from './fillInTheBlanks/Correction'; +type State = { answers: Word[]; words: Word[] }; + +const isWordCorrect = (w: Word) => w.displayed === w.text; + +const resetWrongAnswers = (state: State): State => { + const newWords = state.words.map((w) => { + const isCorrect = isWordCorrect(w); + + // Type word cannot be placed + if (w.type === 'word') { + return w; + } + + return { + ...w, + // set displayed to empty string to reset the answer + displayed: isCorrect ? w.displayed : '', + placed: isCorrect, + }; + }); + const newAnswers = state.answers.map((a) => ({ + ...a, + placed: Boolean(newWords.find((w) => w.id === a.id)?.displayed), + })); + + return { answers: newAnswers, words: newWords }; +}; + type Props = { showCorrection: boolean; showCorrectness: boolean; @@ -28,6 +56,7 @@ type Props = { isReadonly: boolean; values: FillTheBlanksAppSettingData; response: FillTheBlanksAppDataData; + numberOfRetry: number; setResponse: (text: string) => void; }; @@ -38,6 +67,7 @@ const PlayFillInTheBlanks = ({ isReadonly, values, response, + numberOfRetry, setResponse, }: Props) => { const [state, setState] = useState<{ answers: Word[]; words: Word[] }>({ @@ -48,6 +78,20 @@ const PlayFillInTheBlanks = ({ const { t } = useTranslation(); const [prevWords, setPrevWords] = useState(); + const userCannotPlay = isReadonly || showCorrection || showCorrectness; + + useEffect(() => { + console.log('words', state.words); + }, [state.words]); + + // reset wrong answers on retry + useEffect(() => { + const newState = resetWrongAnswers(state); + setState(newState); + saveResponse(newState.words); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [numberOfRetry]); + useEffect(() => { if (lastUserAnswer) { const regExp = RegExp(ANSWER_REGEXP); @@ -75,7 +119,7 @@ const PlayFillInTheBlanks = ({ }; const onDelete = (e: React.MouseEvent) => { - if (isReadonly) { + if (userCannotPlay) { return; } @@ -95,7 +139,7 @@ const PlayFillInTheBlanks = ({ }; const onDrop = (e: React.DragEvent, dropId: number) => { - if (isReadonly) { + if (userCannotPlay) { return; } @@ -132,17 +176,17 @@ const PlayFillInTheBlanks = ({ return ( - + - {!showCorrection && prevWords && ( + {showCorrectness && !isReadonly && ( {t(QUIZ_TRANSLATIONS.RESPONSE_NOT_CORRECT)} diff --git a/src/components/play/PlayView.tsx b/src/components/play/PlayView.tsx index 2ddf13fe..56ef7d00 100644 --- a/src/components/play/PlayView.tsx +++ b/src/components/play/PlayView.tsx @@ -29,6 +29,11 @@ import PlayExplanation from './PlayExplanation'; import PlayHints from './PlayHints'; import PlayViewQuestionType from './PlayViewQuestionType'; +const QUESTION_TYPES_WITH_RETRY_BTN = [ + QuestionType.MULTIPLE_CHOICES, + QuestionType.FILL_BLANKS, +]; + const PlayView = () => { const { t } = useTranslation(); const { data: responses, isSuccess } = hooks.useAppData(); @@ -51,6 +56,8 @@ const PlayView = () => { // this state is used to determine if the animations should be activated or not. // for multiple choice, it must be a counter to avoid animating once only. const [numberOfSubmit, setNumberOfSubmit] = useState(0); + // used to reset the user's answer on retry. + const [numberOfRetry, setNumberOfRetry] = useState(0); const numberOfAnswers = userAnswers.length; const latestAnswer = userAnswers.at(numberOfAnswers - 1); @@ -59,7 +66,7 @@ const PlayView = () => { const isReadonly = isCorrect || maxAttemptsReached; const showCorrection = isCorrect || numberOfAnswers >= maxAttempts; const displaySubmitBtn = !( - currentQuestion.data.type === QuestionType.MULTIPLE_CHOICES && + QUESTION_TYPES_WITH_RETRY_BTN.includes(currentQuestion.data.type) && showCorrectness && !showCorrection ); @@ -71,6 +78,7 @@ const PlayView = () => { setUserAnswers([]); setShowCorrectness(false); setNumberOfSubmit(0); + setNumberOfRetry(0); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentIdx]); @@ -120,7 +128,7 @@ const PlayView = () => { }; const handleRetry = () => { - setNumberOfSubmit(numberOfSubmit + 1); + setNumberOfRetry(numberOfRetry + 1); setShowCorrectness(false); }; @@ -215,6 +223,7 @@ const PlayView = () => { setShowCorrectness={setShowCorrectness} setNewResponse={setNewResponse} numberOfSubmit={numberOfSubmit} + numberOfRetry={numberOfRetry} currentNumberOfAttempts={numberOfAnswers} maxNumberOfAttempts={maxAttempts} resetNumberOfSubmit={() => setNumberOfSubmit(0)} diff --git a/src/components/play/PlayViewQuestionType.tsx b/src/components/play/PlayViewQuestionType.tsx index eff37661..c9c92445 100644 --- a/src/components/play/PlayViewQuestionType.tsx +++ b/src/components/play/PlayViewQuestionType.tsx @@ -30,6 +30,7 @@ type Props = { isCorrect: boolean; latestAnswer?: AppData; numberOfSubmit: number; + numberOfRetry: number; currentNumberOfAttempts: number; maxNumberOfAttempts: number; @@ -47,6 +48,7 @@ export const PlayViewQuestionType = ({ isCorrect, latestAnswer, numberOfSubmit, + numberOfRetry, currentNumberOfAttempts, maxNumberOfAttempts, resetNumberOfSubmit, @@ -96,6 +98,7 @@ export const PlayViewQuestionType = ({ showCorrection={showCorrection} showCorrectness={showCorrectness} numberOfSubmit={numberOfSubmit} + numberOfRetry={numberOfRetry} /> ); } @@ -126,6 +129,7 @@ export const PlayViewQuestionType = ({ showCorrection={showCorrection} showCorrectness={showCorrectness} isReadonly={isReadonly} + numberOfRetry={numberOfRetry} /> ); } diff --git a/src/components/play/fillInTheBlanks/Blank.tsx b/src/components/play/fillInTheBlanks/Blank.tsx index 39caa60d..ccec97c7 100644 --- a/src/components/play/fillInTheBlanks/Blank.tsx +++ b/src/components/play/fillInTheBlanks/Blank.tsx @@ -9,7 +9,6 @@ interface WordBoxProps extends TypographyProps { showCorrectness: boolean; isCorrect: boolean; isReadonly: boolean; - hasChanged: boolean; filled: string; } @@ -21,18 +20,13 @@ export const WordBox = styled(Typography)( showCorrectness, isCorrect, isReadonly, - hasChanged, filled, }) => { let bgColor = backgroundColor ?? 'transparent'; - if (!hasChanged) { - if (showCorrection || showCorrectness) { - bgColor = isCorrect - ? theme.palette.success.main - : theme.palette.error.main; - } else { - bgColor = theme.palette.warning.main; - } + if (showCorrection || showCorrectness) { + bgColor = isCorrect + ? theme.palette.success.light + : theme.palette.error.light; } return { @@ -59,7 +53,6 @@ type Props = { showCorrection: boolean; showCorrectness: boolean; isReadonly: boolean; - hasChanged: boolean; dataCy: string; onDrop: (e: React.DragEvent, id: number) => void; onDelete: (e: React.MouseEvent) => void; @@ -72,7 +65,6 @@ const Blank = ({ showCorrection, showCorrectness, isReadonly, - hasChanged, dataCy, onDrop, onDelete, @@ -90,9 +82,6 @@ const Blank = ({ const _handleDragOver = (e: React.DragEvent) => { e.preventDefault(); - if (!isReadonly) { - setState({ backgroundColor: 'yellow' }); - } }; const _handleDragLeave = (e: React.DragEvent) => { @@ -110,7 +99,6 @@ const Blank = ({ filled={text} isCorrect={isCorrect} isReadonly={isReadonly} - hasChanged={hasChanged} backgroundColor={state.backgroundColor} onDragLeave={_handleDragLeave} onDragOver={_handleDragOver} diff --git a/src/components/play/fillInTheBlanks/BlankedText.tsx b/src/components/play/fillInTheBlanks/BlankedText.tsx index 94e6fe6d..35e7bf47 100644 --- a/src/components/play/fillInTheBlanks/BlankedText.tsx +++ b/src/components/play/fillInTheBlanks/BlankedText.tsx @@ -35,14 +35,8 @@ const BlankedText = ({ onDrop, onDelete, }: Props) => { - const renderWords = () => { - // This idx is used to compare the current blank with - // the previous answer and the current filled word. - // Because "words" doesn't contains WORD types, this - // index must be incremented each time a WORD is found. - let previousAnswerIdx = 0; - - return words.map((word, i) => { + const renderWords = () => + words.map((word, i) => { if (word.type === FILL_BLANKS_TYPE.WORD) { return ( @@ -51,9 +45,6 @@ const BlankedText = ({ ); } - const prevWord = prevWords?.at(previousAnswerIdx++) ?? ''; - const hasChanged = !prevWords || prevWord !== word.displayed; - return ( ); }); - }; return {renderWords()}; }; diff --git a/src/components/play/multipleChoices/PlayMultipleChoices.tsx b/src/components/play/multipleChoices/PlayMultipleChoices.tsx index ce5910d6..7ed4d327 100644 --- a/src/components/play/multipleChoices/PlayMultipleChoices.tsx +++ b/src/components/play/multipleChoices/PlayMultipleChoices.tsx @@ -132,6 +132,7 @@ type Props = { showCorrection: boolean; showCorrectness: boolean; numberOfSubmit: number; + numberOfRetry: number; setResponse: (d: MultipleChoiceAppDataData['choices']) => void; }; @@ -142,6 +143,7 @@ const PlayMultipleChoices = ({ showCorrection, showCorrectness, numberOfSubmit, + numberOfRetry, setResponse, }: Props): JSX.Element => { const { t } = useTranslation(); @@ -151,7 +153,7 @@ const PlayMultipleChoices = ({ const choiceStates = choices.map((choice) => computeChoiceState(choice, lastUserAnswer?.choices, showCorrection) ); - const isAnimating = numberOfSubmit > 0; + const isAnimating = numberOfSubmit + numberOfRetry > 0; const showError = choiceStates.some( (state) => diff --git a/src/langs/en.json b/src/langs/en.json index 618ba65e..6bbf488b 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -72,7 +72,7 @@ "MULTIPLE_CHOICE_NOT_CORRECT": "The answer is incomplete and/or contains incorrect choices.", "HINTS_TITLE": "Hints", "RESPONSE_NOT_CORRECT": "The answer you provided is not correct.", - "HINTS_SUB_TITLE": "Enter hints here to help the user. They are displayed when an incorrect answer is given and there are still attempts to be made.", + "HINTS_SUB_TITLE": "Enter hints here to help the user. They are displayed when an incorrect answer is given and there are still remaining attempts.", "HINTS_LABEL": "Hints", "HINTS_ALERT_TITLE": "Do you need some hints?", "EXPLANATIONS_TITLE": "Explanation", @@ -92,8 +92,8 @@ "QUESTION_POSITION_LABEL": "Position of the question in the quiz", "BUILDER_QUIZ_NAVIGATION_TITLE": "Quiz Navigation", "MULTIPLE_ATTEMPTS_SECTION_TITLE": "Number of attempts", - "MULTIPLE_ATTEMPTS_EXPLANATION_one": "Users cannot try again after sending a response. If you wish to allow multiple attempts after giving incorrect answers, please increase this number.", - "MULTIPLE_ATTEMPTS_EXPLANATION": "You're currently allowing the users to try again at most {{count}} times when the given answer is incorrect.", + "MULTIPLE_ATTEMPTS_EXPLANATION_one": "Users cannot retry after sending a response. If you wish to allow multiple attempts after giving incorrect answers, please increase this number.", + "MULTIPLE_ATTEMPTS_EXPLANATION": "You're currently allowing the users to retry at most {{count}} times if the given answer is incorrect.", "MULTIPLE_ATTEMPTS_SHOW_CORRECTNESS_CHECKBOX": "Display response errors after each attempt", "MULTIPLE_ATTEMPTS_SHOW_CORRECTNESS_TOOLTIP": "If the option is enabled, the user will see the corrections for each reply sent. If not, the user will only be informed that his answer is not entirely correct.", "MULTIPLE_CHOICE_SECTION_TITLE_CORRECT": "Your correct answers", @@ -105,6 +105,6 @@ "MULTIPLE_CHOICE_ADD_HINT_BTN": "add hint", "MULTIPLE_CHOICE_HINT_INPUT_LABEL": "Hint", "MULTIPLE_CHOICE_HINT_INPUT_DESCRIPTION": "Type here a hint to help the user to find the answer or to understand it", - "CREATE_QUIZ_NOT_EXAM_SOLUTION_WARNING": "Caution: Users with some informatics skills can retrieve quiz answers; consider to not use it as an exam method.", - "MULTIPLE_CORRECT_ANSWERS_AVALAIBLE_WARNING": "Did you know? Our multiple choice allows you to define several correct answers. Users will have to select all the correct answers for the question to be answered correctly. Don't hesitate to try it if you haven't already." + "CREATE_QUIZ_NOT_EXAM_SOLUTION_WARNING": "Caution: Users with some computer skills can retrieve quiz answers; it isn't a suitable method for an exam method.", + "MULTIPLE_CORRECT_ANSWERS_AVALAIBLE_WARNING": "Did you know? Our multiple choice question allows you to define several correct answers. Users will have to submit all the correct answers for the question to be marked as correct. Don't hesitate to try it out if you haven't already." } diff --git a/src/langs/fr.json b/src/langs/fr.json index 9550aa72..4e586b18 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -105,5 +105,5 @@ "MULTIPLE_CHOICE_HINT_INPUT_LABEL": "Indice", "CHOIX_MULTIPLES_HINT_INPUT_DESCRIPTION": "Tapez ici un indice pour aider l'utilisateur à trouver la réponse ou à la comprendre", "CREATE_QUIZ_NOT_EXAM_SOLUTION_WARNING": "Attention : Les utilisateurs ayant des compétences en informatique peuvent récupérer les réponses du quiz ; pensez à ne pas l'utiliser comme méthode d'examen", - "MULTIPLE_CORRECT_ANSWERS_AVALAIBLE_WARNING": "Le saviez-vous ? Notre choix multiple vous permet de définir plusieurs réponses correctes. Les utilisateurs devront sélectionner la totalité des bonnes réponses pour que la question soit considérée juste. N'hésitez pas à l'essayer si ce n'est pas déjà fait." + "MULTIPLE_CORRECT_ANSWERS_AVALAIBLE_WARNING": "Le saviez-vous ? La question à choix multiple vous permet de définir plusieurs réponses correctes. Les utilisateurs devront sélectionner la totalité des bonnes réponses pour que la question soit considérée juste. N'hésitez pas à l'essayer si ce n'est pas déjà fait." }