Skip to content

Commit

Permalink
feat: add retry button on fill in blanks
Browse files Browse the repository at this point in the history
  • Loading branch information
ReidyT committed Jul 16, 2024
1 parent d349f87 commit 1b1291b
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 47 deletions.
34 changes: 30 additions & 4 deletions cypress/e2e/play/fillBlanks.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
54 changes: 49 additions & 5 deletions src/components/play/PlayFillInTheBlanks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,42 @@ 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;
lastUserAnswer?: FillTheBlanksAppDataData;
isReadonly: boolean;
values: FillTheBlanksAppSettingData;
response: FillTheBlanksAppDataData;
numberOfRetry: number;
setResponse: (text: string) => void;
};

Expand All @@ -38,6 +67,7 @@ const PlayFillInTheBlanks = ({
isReadonly,
values,
response,
numberOfRetry,
setResponse,
}: Props) => {
const [state, setState] = useState<{ answers: Word[]; words: Word[] }>({
Expand All @@ -48,6 +78,20 @@ const PlayFillInTheBlanks = ({
const { t } = useTranslation();
const [prevWords, setPrevWords] = useState<string[]>();

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);
Expand Down Expand Up @@ -75,7 +119,7 @@ const PlayFillInTheBlanks = ({
};

const onDelete = (e: React.MouseEvent<HTMLSpanElement>) => {
if (isReadonly) {
if (userCannotPlay) {
return;
}

Expand All @@ -95,7 +139,7 @@ const PlayFillInTheBlanks = ({
};

const onDrop = (e: React.DragEvent<HTMLSpanElement>, dropId: number) => {
if (isReadonly) {
if (userCannotPlay) {
return;
}

Expand Down Expand Up @@ -132,17 +176,17 @@ const PlayFillInTheBlanks = ({

return (
<Box width="100%">
<Answers answers={state.answers} isReadonly={isReadonly} />
<Answers answers={state.answers} isReadonly={userCannotPlay} />
<BlankedText
showCorrection={showCorrection}
showCorrectness={showCorrectness}
isReadonly={isReadonly}
isReadonly={userCannotPlay}
words={state.words}
prevWords={prevWords}
onDrop={onDrop}
onDelete={onDelete}
/>
{!showCorrection && prevWords && (
{showCorrectness && !isReadonly && (
<Typography variant="body1" color="error" mt={2}>
{t(QUIZ_TRANSLATIONS.RESPONSE_NOT_CORRECT)}
</Typography>
Expand Down
13 changes: 11 additions & 2 deletions src/components/play/PlayView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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
);
Expand All @@ -71,6 +78,7 @@ const PlayView = () => {
setUserAnswers([]);
setShowCorrectness(false);
setNumberOfSubmit(0);
setNumberOfRetry(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentIdx]);

Expand Down Expand Up @@ -120,7 +128,7 @@ const PlayView = () => {
};

const handleRetry = () => {
setNumberOfSubmit(numberOfSubmit + 1);
setNumberOfRetry(numberOfRetry + 1);
setShowCorrectness(false);
};

Expand Down Expand Up @@ -215,6 +223,7 @@ const PlayView = () => {
setShowCorrectness={setShowCorrectness}
setNewResponse={setNewResponse}
numberOfSubmit={numberOfSubmit}
numberOfRetry={numberOfRetry}
currentNumberOfAttempts={numberOfAnswers}
maxNumberOfAttempts={maxAttempts}
resetNumberOfSubmit={() => setNumberOfSubmit(0)}
Expand Down
4 changes: 4 additions & 0 deletions src/components/play/PlayViewQuestionType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Props = {
isCorrect: boolean;
latestAnswer?: AppData;
numberOfSubmit: number;
numberOfRetry: number;
currentNumberOfAttempts: number;
maxNumberOfAttempts: number;

Expand All @@ -47,6 +48,7 @@ export const PlayViewQuestionType = ({
isCorrect,
latestAnswer,
numberOfSubmit,
numberOfRetry,
currentNumberOfAttempts,
maxNumberOfAttempts,
resetNumberOfSubmit,
Expand Down Expand Up @@ -96,6 +98,7 @@ export const PlayViewQuestionType = ({
showCorrection={showCorrection}
showCorrectness={showCorrectness}
numberOfSubmit={numberOfSubmit}
numberOfRetry={numberOfRetry}
/>
);
}
Expand Down Expand Up @@ -126,6 +129,7 @@ export const PlayViewQuestionType = ({
showCorrection={showCorrection}
showCorrectness={showCorrectness}
isReadonly={isReadonly}
numberOfRetry={numberOfRetry}
/>
);
}
Expand Down
20 changes: 4 additions & 16 deletions src/components/play/fillInTheBlanks/Blank.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ interface WordBoxProps extends TypographyProps {
showCorrectness: boolean;
isCorrect: boolean;
isReadonly: boolean;
hasChanged: boolean;
filled: string;
}

Expand All @@ -21,18 +20,13 @@ export const WordBox = styled(Typography)<WordBoxProps>(
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 {
Expand All @@ -59,7 +53,6 @@ type Props = {
showCorrection: boolean;
showCorrectness: boolean;
isReadonly: boolean;
hasChanged: boolean;
dataCy: string;
onDrop: (e: React.DragEvent<HTMLSpanElement>, id: number) => void;
onDelete: (e: React.MouseEvent<HTMLSpanElement>) => void;
Expand All @@ -72,7 +65,6 @@ const Blank = ({
showCorrection,
showCorrectness,
isReadonly,
hasChanged,
dataCy,
onDrop,
onDelete,
Expand All @@ -90,9 +82,6 @@ const Blank = ({

const _handleDragOver = (e: React.DragEvent<HTMLSpanElement>) => {
e.preventDefault();
if (!isReadonly) {
setState({ backgroundColor: 'yellow' });
}
};

const _handleDragLeave = (e: React.DragEvent<HTMLSpanElement>) => {
Expand All @@ -110,7 +99,6 @@ const Blank = ({
filled={text}
isCorrect={isCorrect}
isReadonly={isReadonly}
hasChanged={hasChanged}
backgroundColor={state.backgroundColor}
onDragLeave={_handleDragLeave}
onDragOver={_handleDragOver}
Expand Down
15 changes: 2 additions & 13 deletions src/components/play/fillInTheBlanks/BlankedText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Typography data-cy={buildBlankedTextWordCy(word.id)}>
Expand All @@ -51,9 +45,6 @@ const BlankedText = ({
);
}

const prevWord = prevWords?.at(previousAnswerIdx++) ?? '';
const hasChanged = !prevWords || prevWord !== word.displayed;

return (
<Blank
dataCy={buildBlankedTextWordCy(word.id)}
Expand All @@ -66,11 +57,9 @@ const BlankedText = ({
onDrop={onDrop}
onDelete={onDelete}
text={word.displayed ?? ' '}
hasChanged={hasChanged}
/>
);
});
};

return <WordWrapper>{renderWords()}</WordWrapper>;
};
Expand Down
4 changes: 3 additions & 1 deletion src/components/play/multipleChoices/PlayMultipleChoices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ type Props = {
showCorrection: boolean;
showCorrectness: boolean;
numberOfSubmit: number;
numberOfRetry: number;
setResponse: (d: MultipleChoiceAppDataData['choices']) => void;
};

Expand All @@ -142,6 +143,7 @@ const PlayMultipleChoices = ({
showCorrection,
showCorrectness,
numberOfSubmit,
numberOfRetry,
setResponse,
}: Props): JSX.Element => {
const { t } = useTranslation();
Expand All @@ -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) =>
Expand Down
Loading

0 comments on commit 1b1291b

Please sign in to comment.