Skip to content

Commit 8d73a48

Browse files
authored
feat: add retry button on fill in blanks (#168)
1 parent c691e77 commit 8d73a48

File tree

9 files changed

+109
-47
lines changed

9 files changed

+109
-47
lines changed

cypress/e2e/play/fillBlanks.cy.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import {
1414
FILL_BLANKS_CORRECTION_CY,
1515
PLAY_VIEW_QUESTION_TITLE_CY,
16+
PLAY_VIEW_RETRY_BUTTON_CY,
1617
PLAY_VIEW_SUBMIT_BUTTON_CY,
1718
buildBlankedTextWordCy,
1819
buildFillBlanksAnswerId,
@@ -153,6 +154,20 @@ const removeAnswer = (answer: Word, shouldFail: boolean) => {
153154
}
154155
};
155156

157+
const checkBadAnswersAreReset = (state: { answers: Word[]; words: Word[] }) => {
158+
const correctAnswersId = state.words
159+
.filter((w) => w.displayed === w.text)
160+
.map((w) => w.id);
161+
162+
state.answers.forEach((answer) => {
163+
if (correctAnswersId.find((id) => id === answer.id)) {
164+
cy.get(`[data-id="${answer.id}"]`).should('contain', answer.text);
165+
} else {
166+
cy.get(`[data-id="${answer.id}"]`).should('contain', '');
167+
}
168+
});
169+
};
170+
156171
describe('Play Fill In The Blanks', () => {
157172
describe('Only 1 attempt', () => {
158173
const NUMBER_OF_ATTEMPTS = 1;
@@ -440,7 +455,17 @@ describe('Play Fill In The Blanks', () => {
440455
// hints should be displayed
441456
cy.checkHintsPlay(fillBlanksAppSettingsData.hints);
442457

443-
removeAnswer(answers[0], false);
458+
// check that user have to click on retry before updating their answer
459+
removeAnswer(answers[2], true);
460+
cy.get(dataCyWrapper(PLAY_VIEW_RETRY_BUTTON_CY)).click();
461+
462+
// check that the input is reset on retry and keep only correct answers
463+
checkBadAnswersAreReset(
464+
splitSentence(partiallyCorrectAppData.data.text)
465+
);
466+
467+
removeAnswer(answers[2], false);
468+
444469
checkInputDisabled(false);
445470
cy.checkQuizNavigation({
446471
questionId: id,
@@ -475,7 +500,8 @@ describe('Play Fill In The Blanks', () => {
475500
// hints should be displayed
476501
cy.checkHintsPlay(fillBlanksAppSettingsData.hints);
477502

478-
removeAnswer(answers[0], false);
503+
cy.get(dataCyWrapper(PLAY_VIEW_RETRY_BUTTON_CY)).click();
504+
479505
checkInputDisabled(false);
480506
cy.checkQuizNavigation({
481507
questionId: id,
@@ -510,8 +536,8 @@ describe('Play Fill In The Blanks', () => {
510536
// we do not check correction: nothing matches
511537
// but we want to know that the app didn't crash
512538

513-
removeAnswer(answers[0], false);
514-
checkInputDisabled(false);
539+
cy.get(dataCyWrapper(PLAY_VIEW_RETRY_BUTTON_CY)).click();
540+
515541
cy.checkQuizNavigation({
516542
questionId: id,
517543
numberOfAttempts: NUMBER_OF_ATTEMPTS,

src/components/play/PlayFillInTheBlanks.tsx

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,42 @@ import Answers from './fillInTheBlanks/Answers';
2121
import BlankedText from './fillInTheBlanks/BlankedText';
2222
import Correction from './fillInTheBlanks/Correction';
2323

24+
type State = { answers: Word[]; words: Word[] };
25+
26+
const isWordCorrect = (w: Word) => w.displayed === w.text;
27+
28+
const resetWrongAnswers = (state: State): State => {
29+
const newWords = state.words.map((w) => {
30+
const isCorrect = isWordCorrect(w);
31+
32+
// Type word cannot be placed
33+
if (w.type === 'word') {
34+
return w;
35+
}
36+
37+
return {
38+
...w,
39+
// set displayed to empty string to reset the answer
40+
displayed: isCorrect ? w.displayed : '',
41+
placed: isCorrect,
42+
};
43+
});
44+
const newAnswers = state.answers.map((a) => ({
45+
...a,
46+
placed: Boolean(newWords.find((w) => w.id === a.id)?.displayed),
47+
}));
48+
49+
return { answers: newAnswers, words: newWords };
50+
};
51+
2452
type Props = {
2553
showCorrection: boolean;
2654
showCorrectness: boolean;
2755
lastUserAnswer?: FillTheBlanksAppDataData;
2856
isReadonly: boolean;
2957
values: FillTheBlanksAppSettingData;
3058
response: FillTheBlanksAppDataData;
59+
numberOfRetry: number;
3160
setResponse: (text: string) => void;
3261
};
3362

@@ -38,6 +67,7 @@ const PlayFillInTheBlanks = ({
3867
isReadonly,
3968
values,
4069
response,
70+
numberOfRetry,
4171
setResponse,
4272
}: Props) => {
4373
const [state, setState] = useState<{ answers: Word[]; words: Word[] }>({
@@ -48,6 +78,20 @@ const PlayFillInTheBlanks = ({
4878
const { t } = useTranslation();
4979
const [prevWords, setPrevWords] = useState<string[]>();
5080

81+
const userCannotPlay = isReadonly || showCorrection || showCorrectness;
82+
83+
useEffect(() => {
84+
console.log('words', state.words);
85+
}, [state.words]);
86+
87+
// reset wrong answers on retry
88+
useEffect(() => {
89+
const newState = resetWrongAnswers(state);
90+
setState(newState);
91+
saveResponse(newState.words);
92+
// eslint-disable-next-line react-hooks/exhaustive-deps
93+
}, [numberOfRetry]);
94+
5195
useEffect(() => {
5296
if (lastUserAnswer) {
5397
const regExp = RegExp(ANSWER_REGEXP);
@@ -75,7 +119,7 @@ const PlayFillInTheBlanks = ({
75119
};
76120

77121
const onDelete = (e: React.MouseEvent<HTMLSpanElement>) => {
78-
if (isReadonly) {
122+
if (userCannotPlay) {
79123
return;
80124
}
81125

@@ -95,7 +139,7 @@ const PlayFillInTheBlanks = ({
95139
};
96140

97141
const onDrop = (e: React.DragEvent<HTMLSpanElement>, dropId: number) => {
98-
if (isReadonly) {
142+
if (userCannotPlay) {
99143
return;
100144
}
101145

@@ -132,17 +176,17 @@ const PlayFillInTheBlanks = ({
132176

133177
return (
134178
<Box width="100%">
135-
<Answers answers={state.answers} isReadonly={isReadonly} />
179+
<Answers answers={state.answers} isReadonly={userCannotPlay} />
136180
<BlankedText
137181
showCorrection={showCorrection}
138182
showCorrectness={showCorrectness}
139-
isReadonly={isReadonly}
183+
isReadonly={userCannotPlay}
140184
words={state.words}
141185
prevWords={prevWords}
142186
onDrop={onDrop}
143187
onDelete={onDelete}
144188
/>
145-
{!showCorrection && prevWords && (
189+
{showCorrectness && !isReadonly && (
146190
<Typography variant="body1" color="error" mt={2}>
147191
{t(QUIZ_TRANSLATIONS.RESPONSE_NOT_CORRECT)}
148192
</Typography>

src/components/play/PlayView.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ import PlayExplanation from './PlayExplanation';
2929
import PlayHints from './PlayHints';
3030
import PlayViewQuestionType from './PlayViewQuestionType';
3131

32+
const QUESTION_TYPES_WITH_RETRY_BTN = [
33+
QuestionType.MULTIPLE_CHOICES,
34+
QuestionType.FILL_BLANKS,
35+
];
36+
3237
const PlayView = () => {
3338
const { t } = useTranslation();
3439
const { data: responses, isSuccess } = hooks.useAppData();
@@ -51,6 +56,8 @@ const PlayView = () => {
5156
// this state is used to determine if the animations should be activated or not.
5257
// for multiple choice, it must be a counter to avoid animating once only.
5358
const [numberOfSubmit, setNumberOfSubmit] = useState(0);
59+
// used to reset the user's answer on retry.
60+
const [numberOfRetry, setNumberOfRetry] = useState(0);
5461

5562
const numberOfAnswers = userAnswers.length;
5663
const latestAnswer = userAnswers.at(numberOfAnswers - 1);
@@ -59,7 +66,7 @@ const PlayView = () => {
5966
const isReadonly = isCorrect || maxAttemptsReached;
6067
const showCorrection = isCorrect || numberOfAnswers >= maxAttempts;
6168
const displaySubmitBtn = !(
62-
currentQuestion.data.type === QuestionType.MULTIPLE_CHOICES &&
69+
QUESTION_TYPES_WITH_RETRY_BTN.includes(currentQuestion.data.type) &&
6370
showCorrectness &&
6471
!showCorrection
6572
);
@@ -71,6 +78,7 @@ const PlayView = () => {
7178
setUserAnswers([]);
7279
setShowCorrectness(false);
7380
setNumberOfSubmit(0);
81+
setNumberOfRetry(0);
7482
// eslint-disable-next-line react-hooks/exhaustive-deps
7583
}, [currentIdx]);
7684

@@ -120,7 +128,7 @@ const PlayView = () => {
120128
};
121129

122130
const handleRetry = () => {
123-
setNumberOfSubmit(numberOfSubmit + 1);
131+
setNumberOfRetry(numberOfRetry + 1);
124132
setShowCorrectness(false);
125133
};
126134

@@ -215,6 +223,7 @@ const PlayView = () => {
215223
setShowCorrectness={setShowCorrectness}
216224
setNewResponse={setNewResponse}
217225
numberOfSubmit={numberOfSubmit}
226+
numberOfRetry={numberOfRetry}
218227
currentNumberOfAttempts={numberOfAnswers}
219228
maxNumberOfAttempts={maxAttempts}
220229
resetNumberOfSubmit={() => setNumberOfSubmit(0)}

src/components/play/PlayViewQuestionType.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Props = {
3030
isCorrect: boolean;
3131
latestAnswer?: AppData;
3232
numberOfSubmit: number;
33+
numberOfRetry: number;
3334
currentNumberOfAttempts: number;
3435
maxNumberOfAttempts: number;
3536

@@ -47,6 +48,7 @@ export const PlayViewQuestionType = ({
4748
isCorrect,
4849
latestAnswer,
4950
numberOfSubmit,
51+
numberOfRetry,
5052
currentNumberOfAttempts,
5153
maxNumberOfAttempts,
5254
resetNumberOfSubmit,
@@ -96,6 +98,7 @@ export const PlayViewQuestionType = ({
9698
showCorrection={showCorrection}
9799
showCorrectness={showCorrectness}
98100
numberOfSubmit={numberOfSubmit}
101+
numberOfRetry={numberOfRetry}
99102
/>
100103
);
101104
}
@@ -126,6 +129,7 @@ export const PlayViewQuestionType = ({
126129
showCorrection={showCorrection}
127130
showCorrectness={showCorrectness}
128131
isReadonly={isReadonly}
132+
numberOfRetry={numberOfRetry}
129133
/>
130134
);
131135
}

src/components/play/fillInTheBlanks/Blank.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ interface WordBoxProps extends TypographyProps {
99
showCorrectness: boolean;
1010
isCorrect: boolean;
1111
isReadonly: boolean;
12-
hasChanged: boolean;
1312
filled: string;
1413
}
1514

@@ -21,18 +20,13 @@ export const WordBox = styled(Typography)<WordBoxProps>(
2120
showCorrectness,
2221
isCorrect,
2322
isReadonly,
24-
hasChanged,
2523
filled,
2624
}) => {
2725
let bgColor = backgroundColor ?? 'transparent';
28-
if (!hasChanged) {
29-
if (showCorrection || showCorrectness) {
30-
bgColor = isCorrect
31-
? theme.palette.success.main
32-
: theme.palette.error.main;
33-
} else {
34-
bgColor = theme.palette.warning.main;
35-
}
26+
if (showCorrection || showCorrectness) {
27+
bgColor = isCorrect
28+
? theme.palette.success.light
29+
: theme.palette.error.light;
3630
}
3731

3832
return {
@@ -59,7 +53,6 @@ type Props = {
5953
showCorrection: boolean;
6054
showCorrectness: boolean;
6155
isReadonly: boolean;
62-
hasChanged: boolean;
6356
dataCy: string;
6457
onDrop: (e: React.DragEvent<HTMLSpanElement>, id: number) => void;
6558
onDelete: (e: React.MouseEvent<HTMLSpanElement>) => void;
@@ -72,7 +65,6 @@ const Blank = ({
7265
showCorrection,
7366
showCorrectness,
7467
isReadonly,
75-
hasChanged,
7668
dataCy,
7769
onDrop,
7870
onDelete,
@@ -90,9 +82,6 @@ const Blank = ({
9082

9183
const _handleDragOver = (e: React.DragEvent<HTMLSpanElement>) => {
9284
e.preventDefault();
93-
if (!isReadonly) {
94-
setState({ backgroundColor: 'yellow' });
95-
}
9685
};
9786

9887
const _handleDragLeave = (e: React.DragEvent<HTMLSpanElement>) => {
@@ -110,7 +99,6 @@ const Blank = ({
11099
filled={text}
111100
isCorrect={isCorrect}
112101
isReadonly={isReadonly}
113-
hasChanged={hasChanged}
114102
backgroundColor={state.backgroundColor}
115103
onDragLeave={_handleDragLeave}
116104
onDragOver={_handleDragOver}

src/components/play/fillInTheBlanks/BlankedText.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,8 @@ const BlankedText = ({
3535
onDrop,
3636
onDelete,
3737
}: Props) => {
38-
const renderWords = () => {
39-
// This idx is used to compare the current blank with
40-
// the previous answer and the current filled word.
41-
// Because "words" doesn't contains WORD types, this
42-
// index must be incremented each time a WORD is found.
43-
let previousAnswerIdx = 0;
44-
45-
return words.map((word, i) => {
38+
const renderWords = () =>
39+
words.map((word, i) => {
4640
if (word.type === FILL_BLANKS_TYPE.WORD) {
4741
return (
4842
<Typography data-cy={buildBlankedTextWordCy(word.id)}>
@@ -51,9 +45,6 @@ const BlankedText = ({
5145
);
5246
}
5347

54-
const prevWord = prevWords?.at(previousAnswerIdx++) ?? '';
55-
const hasChanged = !prevWords || prevWord !== word.displayed;
56-
5748
return (
5849
<Blank
5950
dataCy={buildBlankedTextWordCy(word.id)}
@@ -66,11 +57,9 @@ const BlankedText = ({
6657
onDrop={onDrop}
6758
onDelete={onDelete}
6859
text={word.displayed ?? ' '}
69-
hasChanged={hasChanged}
7060
/>
7161
);
7262
});
73-
};
7463

7564
return <WordWrapper>{renderWords()}</WordWrapper>;
7665
};

src/components/play/multipleChoices/PlayMultipleChoices.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ type Props = {
132132
showCorrection: boolean;
133133
showCorrectness: boolean;
134134
numberOfSubmit: number;
135+
numberOfRetry: number;
135136
setResponse: (d: MultipleChoiceAppDataData['choices']) => void;
136137
};
137138

@@ -142,6 +143,7 @@ const PlayMultipleChoices = ({
142143
showCorrection,
143144
showCorrectness,
144145
numberOfSubmit,
146+
numberOfRetry,
145147
setResponse,
146148
}: Props): JSX.Element => {
147149
const { t } = useTranslation();
@@ -151,7 +153,7 @@ const PlayMultipleChoices = ({
151153
const choiceStates = choices.map((choice) =>
152154
computeChoiceState(choice, lastUserAnswer?.choices, showCorrection)
153155
);
154-
const isAnimating = numberOfSubmit > 0;
156+
const isAnimating = numberOfSubmit + numberOfRetry > 0;
155157
const showError =
156158
choiceStates.some(
157159
(state) =>

0 commit comments

Comments
 (0)