From 93d811f5ec1c65e4dde27b16cfe0108769e93d98 Mon Sep 17 00:00:00 2001 From: Thibault Reidy <147397675+ReidyT@users.noreply.github.com> Date: Mon, 22 Jan 2024 08:06:54 +0100 Subject: [PATCH] feat: provide hints when incorrect answer (#117) --- cypress/e2e/Admin/create/createView.cy.ts | 1 + cypress/e2e/Admin/create/fillBlanks.cy.ts | 7 ++- .../e2e/Admin/create/multipleChoices.cy.ts | 7 +++ cypress/e2e/Admin/create/slider.cy.ts | 7 ++- cypress/e2e/Admin/create/textInput.cy.ts | 9 +++- cypress/e2e/play/fillBlanks.cy.ts | 34 ++++++++++-- cypress/e2e/play/multipleChoices.cy.ts | 20 ++++++- cypress/e2e/play/playView.cy.ts | 4 +- cypress/e2e/play/slider.cy.ts | 18 ++++++- cypress/e2e/play/textInput.cy.ts | 25 ++++++++- cypress/fixtures/appSettings.ts | 6 +++ cypress/support/commands.ts | 54 ++++++++++++------- cypress/support/index.ts | 6 +++ src/components/create/CreateView.tsx | 31 +++++++++-- src/components/create/Explanation.tsx | 38 ------------- .../create/TitleDescriptionTextField.tsx | 45 ++++++++++++++++ src/components/play/PlayHints.tsx | 44 +++++++++++++++ src/components/play/PlayView.tsx | 9 ++++ src/components/types/types.ts | 1 + src/config/selectors.tsx | 2 + src/langs/constants.ts | 16 +++--- src/langs/en.ts | 4 ++ src/langs/fr.ts | 11 +++- 23 files changed, 319 insertions(+), 80 deletions(-) delete mode 100644 src/components/create/Explanation.tsx create mode 100644 src/components/create/TitleDescriptionTextField.tsx create mode 100644 src/components/play/PlayHints.tsx diff --git a/cypress/e2e/Admin/create/createView.cy.ts b/cypress/e2e/Admin/create/createView.cy.ts index 66cd6183..125146bc 100644 --- a/cypress/e2e/Admin/create/createView.cy.ts +++ b/cypress/e2e/Admin/create/createView.cy.ts @@ -41,6 +41,7 @@ const newMultipleChoiceData = { }, ], explanation: 'my new explanation', + hints: 'my new hints' }; describe('Create View', () => { diff --git a/cypress/e2e/Admin/create/fillBlanks.cy.ts b/cypress/e2e/Admin/create/fillBlanks.cy.ts index 8536a5a9..a7102e5f 100644 --- a/cypress/e2e/Admin/create/fillBlanks.cy.ts +++ b/cypress/e2e/Admin/create/fillBlanks.cy.ts @@ -26,6 +26,7 @@ const newFillBlanksData = { question: 'new question text', text: 'My text with and more ', explanation: 'new explanation', + hints: 'new hints', }; const { data } = QUESTION_APP_SETTINGS.find( @@ -41,7 +42,8 @@ const fillBlanksQuestion = ( text, question, explanation, - }: { text: string; question: string; explanation: string }, + hints, + }: { text: string; question: string; explanation: string; hints: string }, { shouldSave = true } = {} ) => { // fill question @@ -55,6 +57,7 @@ const fillBlanksQuestion = ( cy.get(dataCyWrapper(FILL_BLANKS_TEXT_FIELD_CY)).type(text); } + cy.fillHints(hints); cy.fillExplanation(explanation); // save @@ -133,6 +136,7 @@ describe('Fill in the Blanks', () => { 'have.value', data.text ); + cy.checkHintsField(data.hints); cy.checkExplanationField(data.explanation); }); @@ -152,6 +156,7 @@ describe('Fill in the Blanks', () => { 'have.value', newFillBlanksData.text ); + cy.checkHintsField(newFillBlanksData.hints); cy.checkExplanationField(newFillBlanksData.explanation); }); }); diff --git a/cypress/e2e/Admin/create/multipleChoices.cy.ts b/cypress/e2e/Admin/create/multipleChoices.cy.ts index b1f9d033..3765dc26 100644 --- a/cypress/e2e/Admin/create/multipleChoices.cy.ts +++ b/cypress/e2e/Admin/create/multipleChoices.cy.ts @@ -63,6 +63,7 @@ const newMultipleChoiceData = { }, ], explanation: 'my new explanation', + hints: 'my new hints', }; export const fillMultipleChoiceQuestion = ( @@ -70,10 +71,12 @@ export const fillMultipleChoiceQuestion = ( choices, question, explanation, + hints, }: { choices: MultipleChoicesChoice[]; question: string; explanation: string; + hints: string; }, { shouldSave = true } = {} ) => { @@ -126,6 +129,7 @@ export const fillMultipleChoiceQuestion = ( }); }); + cy.fillHints(hints); cy.fillExplanation(explanation); // save @@ -184,6 +188,7 @@ describe('Multiple Choices', () => { fillMultipleChoiceQuestion(newMultipleChoiceData); cy.checkErrorMessage({}); + cy.checkHintsField(newMultipleChoiceData.hints); cy.checkExplanationField(newMultipleChoiceData.explanation); }); @@ -286,6 +291,7 @@ describe('Multiple Choices', () => { }); cy.get(dataCyWrapper(QUESTION_BAR_PREV_CY)).should('be.disabled'); + cy.checkHintsField(data.hints); cy.checkExplanationField(data.explanation); }); @@ -312,6 +318,7 @@ describe('Multiple Choices', () => { ).should(isCorrect ? 'be.checked' : 'not.be.checked'); }); + cy.checkHintsField(newMultipleChoiceData.hints); cy.checkExplanationField(newMultipleChoiceData.explanation); }); diff --git a/cypress/e2e/Admin/create/slider.cy.ts b/cypress/e2e/Admin/create/slider.cy.ts index 2c1f8f64..decf5d3c 100644 --- a/cypress/e2e/Admin/create/slider.cy.ts +++ b/cypress/e2e/Admin/create/slider.cy.ts @@ -32,6 +32,7 @@ const newSliderData = { max: 90, value: 40, explanation: 'new explanation', + hints: 'new hints', }; const { data } = QUESTION_APP_SETTINGS.find( @@ -45,12 +46,13 @@ type SliderValues = { max: number; value: number; explanation: string; + hints: string; }; const id = data.questionId; const fillSliderQuestion = ( - { min, max, value, question, explanation }: SliderValues, + { min, max, value, question, explanation, hints }: SliderValues, originalAppSettingDataValue: number, { shouldSave = true } = {} ) => { @@ -78,6 +80,7 @@ const fillSliderQuestion = ( cy.get(`${dataCyWrapper(SLIDER_CY)}`).type(stepsString); } + cy.fillHints(hints); cy.fillExplanation(explanation); // save @@ -168,6 +171,7 @@ describe('Slider', () => { 'have.value', data.value ); + cy.checkHintsField(data.hints); cy.checkExplanationField(data.explanation); }); @@ -196,6 +200,7 @@ describe('Slider', () => { 'not.have.value', data.value ); + cy.checkHintsField(newSliderData.hints); cy.checkExplanationField(newSliderData.explanation); }); }); diff --git a/cypress/e2e/Admin/create/textInput.cy.ts b/cypress/e2e/Admin/create/textInput.cy.ts index 217ee720..e4a06ca4 100644 --- a/cypress/e2e/Admin/create/textInput.cy.ts +++ b/cypress/e2e/Admin/create/textInput.cy.ts @@ -26,6 +26,7 @@ const newTextInputData = { question: 'new question text', text: 'new text', explanation: 'my explanation', + hints: 'my hints', }; const { data } = QUESTION_APP_SETTINGS.find( @@ -40,7 +41,8 @@ const fillTextInputQuestion = ( text, question, explanation, - }: { text: string; question: string; explanation: string }, + hints, + }: { text: string; question: string; explanation: string; hints: string }, { shouldSave = true } = {} ) => { // fill question if not empty @@ -55,7 +57,8 @@ const fillTextInputQuestion = ( cy.get(`${dataCyWrapper(TEXT_INPUT_FIELD_CY)} input`).type(text); } - // fill explanation + // fill hints and explanation + cy.fillHints(hints); cy.fillExplanation(explanation); // save @@ -143,6 +146,7 @@ describe('Text Input', () => { data.text ); + cy.checkHintsField(data.hints); cy.checkExplanationField(data.explanation); }); @@ -163,6 +167,7 @@ describe('Text Input', () => { newTextInputData.text ); + cy.checkHintsField(newTextInputData.hints); cy.checkExplanationField(newTextInputData.explanation); }); }); diff --git a/cypress/e2e/play/fillBlanks.cy.ts b/cypress/e2e/play/fillBlanks.cy.ts index 8d7a9b27..12183a32 100644 --- a/cypress/e2e/play/fillBlanks.cy.ts +++ b/cypress/e2e/play/fillBlanks.cy.ts @@ -1,6 +1,9 @@ import { Context } from '@graasp/sdk'; -import { TextAppDataData } from '../../../src/components/types/types'; +import { + AppSettingData, + TextAppDataData, +} from '../../../src/components/types/types'; import { APP_SETTING_NAMES, FILL_BLANKS_TYPE, @@ -37,7 +40,7 @@ const id = data.questionId; const { text } = data as TextAppDataData; const { answers, words } = splitSentence(text); -const explanationShouldNotBeVisible = () => cy.checkExplanationPlay(undefined); +const fillBlanksAppSettingsData = APP_SETTINGS[3].data as AppSettingData; // verify all answers styles const checkCorrection = ( @@ -89,7 +92,7 @@ const checkCorrection = ( if (shouldBeVisible) { cy.checkExplanationPlay(data.explanation); } else { - explanationShouldNotBeVisible(); + cy.checkExplanationPlay(null); } }); }; @@ -193,6 +196,7 @@ describe('Play Fill In The Blanks', () => { ); } }); + cy.checkHintsPlay(null); cy.checkExplanationPlay(null); checkInputDisabled(false); cy.checkNumberOfAttemptsProgression({ @@ -235,6 +239,9 @@ describe('Play Fill In The Blanks', () => { checkCorrection(splitSentence(data.text)); + // hints should be hidden + cy.checkHintsPlay(null); + // success displayed in question bar cy.checkStepStatus(id, true); @@ -267,6 +274,9 @@ describe('Play Fill In The Blanks', () => { const data = partiallyCorrectAppData.data; checkCorrection(splitSentence(data.text)); + // hints should be hidden + cy.checkHintsPlay(null); + // success displayed in question bar cy.checkStepStatus(id, false); @@ -298,6 +308,9 @@ describe('Play Fill In The Blanks', () => { const data = shorterAppData.data; checkCorrection(splitSentence(data.text)); + // hints should be hidden + cy.checkHintsPlay(null); + // success displayed in question bar cy.checkStepStatus(id, false); @@ -326,6 +339,9 @@ describe('Play Fill In The Blanks', () => { cy.visit('/'); cy.get(dataCyWrapper(buildQuestionStepCy(id))).click(); + // hints should be hidden + cy.checkHintsPlay(null); + // we do not check correction: nothing matches // but we want to know that the app didn't crash @@ -400,6 +416,9 @@ describe('Play Fill In The Blanks', () => { checkCorrection(splitSentence(data.text)); + // hints should be hidden + cy.checkHintsPlay(null); + // success displayed in question bar cy.checkStepStatus(id, true); @@ -435,6 +454,9 @@ describe('Play Fill In The Blanks', () => { const data = partiallyCorrectAppData.data; checkCorrection(splitSentence(data.text), false); + // hints should be displayed + cy.checkHintsPlay(fillBlanksAppSettingsData.hints); + // success displayed in question bar cy.checkStepStatus(id, false); @@ -469,6 +491,9 @@ describe('Play Fill In The Blanks', () => { const data = shorterAppData.data; checkCorrection(splitSentence(data.text), false); + // hints should be displayed + cy.checkHintsPlay(fillBlanksAppSettingsData.hints); + // success displayed in question bar cy.checkStepStatus(id, false); @@ -500,6 +525,9 @@ describe('Play Fill In The Blanks', () => { cy.visit('/'); cy.get(dataCyWrapper(buildQuestionStepCy(id))).click(); + // hints should be displayed + cy.checkHintsPlay(fillBlanksAppSettingsData.hints); + // we do not check correction: nothing matches // but we want to know that the app didn't crash diff --git a/cypress/e2e/play/multipleChoices.cy.ts b/cypress/e2e/play/multipleChoices.cy.ts index 1fbb61b5..1cdd70c3 100644 --- a/cypress/e2e/play/multipleChoices.cy.ts +++ b/cypress/e2e/play/multipleChoices.cy.ts @@ -1,6 +1,9 @@ import { Context } from '@graasp/sdk'; -import { MultipleChoicesAppSettingData } from '../../../src/components/types/types'; +import { + AppSettingData, + MultipleChoicesAppSettingData, +} from '../../../src/components/types/types'; import { APP_SETTING_NAMES, QuestionType } from '../../../src/config/constants'; import { EXPLANATION_PLAY_CY, @@ -28,6 +31,8 @@ const { data } = QUESTION_APP_SETTINGS.find( const id = data.questionId; const { choices } = data as MultipleChoicesAppSettingData; +const multipleChoiceAppSettingsData = APP_SETTINGS[0].data as AppSettingData; + // click on choices -> become selected const clickSelection = (selection: number[]) => { // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -124,6 +129,7 @@ describe('Play Multiple Choices', () => { dataCyWrapper(buildMultipleChoicesButtonCy(idx, false)) ).should('be.visible'); }); + cy.checkHintsPlay(null); cy.checkExplanationPlay(null); checkInputDisabled([], false); cy.checkNumberOfAttemptsProgression({ @@ -141,6 +147,9 @@ describe('Play Multiple Choices', () => { // verify all choices styles checkCorrection(selection); + // hints should be hidden + cy.checkHintsPlay(null); + // error displayed in question bar cy.checkStepStatus(id, false); checkInputDisabled(selection, true); @@ -164,6 +173,9 @@ describe('Play Multiple Choices', () => { checkCorrection(selection); + // hints should be hidden + cy.checkHintsPlay(null); + // success displayed in question bar cy.checkStepStatus(id, true); checkInputDisabled(selection, true); @@ -218,6 +230,8 @@ describe('Play Multiple Choices', () => { ); checkCorrection(selection); + // hints should be hidden + cy.checkHintsPlay(null); checkInputDisabled(selection, true); cy.checkNumberOfAttemptsProgression({ numberOfAttempts: NUMBER_OF_ATTEMPTS, @@ -256,6 +270,8 @@ describe('Play Multiple Choices', () => { // verify all choices styles checkCorrection(selection, false); + cy.checkHintsPlay(multipleChoiceAppSettingsData.hints); + // error displayed in question bar cy.checkStepStatus(id, false); checkInputDisabled(selection, false); @@ -278,6 +294,7 @@ describe('Play Multiple Choices', () => { cy.get(dataCyWrapper(PLAY_VIEW_SUBMIT_BUTTON_CY)).click(); checkCorrection(selection); + cy.checkHintsPlay(null); // success displayed in question bar cy.checkStepStatus(id, true); @@ -336,6 +353,7 @@ describe('Play Multiple Choices', () => { ); checkCorrection(selection, false); + cy.checkHintsPlay(multipleChoiceAppSettingsData.hints); checkInputDisabled(selection, false); cy.checkNumberOfAttemptsProgression({ numberOfAttempts: NUMBER_OF_ATTEMPTS, diff --git a/cypress/e2e/play/playView.cy.ts b/cypress/e2e/play/playView.cy.ts index 0527adcc..2aabc074 100644 --- a/cypress/e2e/play/playView.cy.ts +++ b/cypress/e2e/play/playView.cy.ts @@ -1,7 +1,6 @@ import { Context, PermissionLevel } from '@graasp/sdk'; import { - EXPLANATION_PLAY_CY, PLAY_VIEW_EMPTY_QUIZ_CY, PLAY_VIEW_QUESTION_TITLE_CY, QUESTION_BAR_CY, @@ -55,7 +54,8 @@ describe('Play View', () => { expect($el.attr('class').toLowerCase()).not.to.contain('success'); }); - cy.get(`${dataCyWrapper(EXPLANATION_PLAY_CY)}`).should('not.exist'); + cy.checkHintsPlay(null); + cy.checkExplanationPlay(null); }); describe('Play View', () => { diff --git a/cypress/e2e/play/slider.cy.ts b/cypress/e2e/play/slider.cy.ts index 31d0f96e..5eedfb01 100644 --- a/cypress/e2e/play/slider.cy.ts +++ b/cypress/e2e/play/slider.cy.ts @@ -1,6 +1,9 @@ import { Context } from '@graasp/sdk'; -import { SliderAppDataData } from '../../../src/components/types/types'; +import { + AppSettingData, + SliderAppDataData, +} from '../../../src/components/types/types'; import { APP_SETTING_NAMES, QuestionType } from '../../../src/config/constants'; import { PLAY_VIEW_QUESTION_TITLE_CY, @@ -25,6 +28,8 @@ const { data } = QUESTION_APP_SETTINGS.find( const id = data.questionId; +const sliderAppSettingsData = APP_SETTINGS[2].data as AppSettingData; + const checkCorrection = (responseData: Pick) => { // cannot check slider value because we cannot move it // cy.get(`${dataCyWrapper(PLAY_VIEW_SLIDER_CY)} input`).should('have.value', responseData.value) @@ -93,6 +98,7 @@ describe('Slider', () => { 'not.have.value', data.value ); + cy.checkHintsPlay(null); cy.checkExplanationPlay(null); cy.checkNumberOfAttemptsProgression({ numberOfAttempts: 1, @@ -108,6 +114,7 @@ describe('Slider', () => { // verify all choices styles checkCorrection({ value: 60 }); + cy.checkHintsPlay(null); // error display in question bar // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -124,6 +131,7 @@ describe('Slider', () => { ).click(); cy.get(dataCyWrapper(buildQuestionStepCy(id))).click(); checkCorrection({ value: 60 }); + cy.checkHintsPlay(null); // error displayed in question bar cy.checkStepStatus(id, false); @@ -165,6 +173,7 @@ describe('Slider', () => { it('Show saved question', () => { checkCorrection(appData.data); + cy.checkHintsPlay(null); cy.checkNumberOfAttemptsProgression({ numberOfAttempts: 1, @@ -198,10 +207,12 @@ describe('Slider', () => { cy.get(dataCyWrapper(buildQuestionStepCy(id))).click(); }); - it('Incorrect app data', () => { + it.only('Incorrect app data', () => { // difficult to move slider, so we don't do it + cy.checkHintsPlay(null); cy.get(dataCyWrapper(PLAY_VIEW_SUBMIT_BUTTON_CY)).click(); + cy.checkHintsPlay(sliderAppSettingsData.hints); // error displayed in question bar cy.checkStepStatus(id, false); @@ -215,7 +226,9 @@ describe('Slider', () => { checkInputDisabled(false); cy.get(dataCyWrapper(PLAY_VIEW_SUBMIT_BUTTON_CY)).click(); + cy.checkHintsPlay(sliderAppSettingsData.hints); cy.get(dataCyWrapper(PLAY_VIEW_SUBMIT_BUTTON_CY)).click(); + cy.checkHintsPlay(null); // verify all choices styles checkCorrection({ value: 60 }); @@ -280,6 +293,7 @@ describe('Slider', () => { it('Show saved question', () => { checkCorrection(appData.data); + cy.checkHintsPlay(null); cy.checkNumberOfAttemptsProgression({ numberOfAttempts: NUMBER_OF_ATTEMPTS, diff --git a/cypress/e2e/play/textInput.cy.ts b/cypress/e2e/play/textInput.cy.ts index 767159ae..245086aa 100644 --- a/cypress/e2e/play/textInput.cy.ts +++ b/cypress/e2e/play/textInput.cy.ts @@ -1,6 +1,6 @@ import { Context } from '@graasp/sdk'; -import { TextAppDataData } from '../../../src/components/types/types'; +import { AppSettingData, TextAppDataData } from '../../../src/components/types/types'; import { APP_SETTING_NAMES, QuestionType } from '../../../src/config/constants'; import { PLAY_VIEW_QUESTION_TITLE_CY, @@ -27,6 +27,8 @@ const id = data.questionId; const { text } = data as TextAppDataData; +const textAppSettingsData = APP_SETTINGS[1].data as AppSettingData; + const getPlayViewTextInputCy = (isCorrect: boolean) => buildPlayViewTextInputCy(isCorrect); @@ -122,6 +124,7 @@ describe('Play Text Input', () => { 'be.empty' ); + cy.checkHintsPlay(null); cy.checkExplanationPlay(null); }); @@ -130,6 +133,7 @@ describe('Play Text Input', () => { checkAnswerAndHeaderStatus(true); + cy.checkHintsPlay(null); cy.checkExplanationPlay(data.explanation); checkInputDisabled(true, true); @@ -143,6 +147,7 @@ describe('Play Text Input', () => { checkAnswerAndHeaderStatus(false); + cy.checkHintsPlay(null); cy.checkExplanationPlay(data.explanation); checkInputDisabled(true, false); @@ -153,6 +158,8 @@ describe('Play Text Input', () => { checkAnswerAndHeaderStatus(true); + cy.checkHintsPlay(null); + checkInputDisabled(true, true); }); @@ -161,6 +168,8 @@ describe('Play Text Input', () => { checkAnswerAndHeaderStatus(true); + cy.checkHintsPlay(null); + checkInputDisabled(true, true); }); @@ -169,6 +178,8 @@ describe('Play Text Input', () => { checkAnswerAndHeaderStatus(true); + cy.checkHintsPlay(null); + checkInputDisabled(true, true); }); @@ -177,6 +188,8 @@ describe('Play Text Input', () => { checkAnswerAndHeaderStatus(true); + cy.checkHintsPlay(null); + checkInputDisabled(true, true); }); }); @@ -214,6 +227,7 @@ describe('Play Text Input', () => { isCorrect: true, }); + cy.checkHintsPlay(null); cy.checkExplanationPlay(data.explanation); }); @@ -229,6 +243,7 @@ describe('Play Text Input', () => { isCorrect: false, }); explanationShouldNotBeVisible(); + cy.checkHintsPlay(textAppSettingsData.hints); const incorrectAnswer = 'still incorrect answer'; submitAnswer(incorrectAnswer, true); @@ -237,6 +252,7 @@ describe('Play Text Input', () => { checkAnswerAndHeaderStatus(false); checkTextValueEquals(incorrectAnswer, false); explanationShouldNotBeVisible(); + cy.checkHintsPlay(textAppSettingsData.hints); submitAnswer('still not correct', true); checkAnswerAndHeaderStatus(false); @@ -247,6 +263,7 @@ describe('Play Text Input', () => { isCorrect: false, }); cy.checkExplanationPlay(data.explanation); + cy.checkHintsPlay(null); }); it('Incorrect, but then correct app data', () => { @@ -262,6 +279,7 @@ describe('Play Text Input', () => { isCorrect: false, }); explanationShouldNotBeVisible(); + cy.checkHintsPlay(textAppSettingsData.hints); submitAnswer(text, true); checkAnswerAndHeaderStatus(true); @@ -272,6 +290,7 @@ describe('Play Text Input', () => { isCorrect: true, }); cy.checkExplanationPlay(data.explanation); + cy.checkHintsPlay(null); }); }); }); @@ -317,6 +336,7 @@ describe('Play Text Input', () => { checkTextValueEquals(incorrectAppData.data.text, false); cy.checkExplanationPlay(data.explanation); + cy.checkHintsPlay(null); }); }); @@ -341,6 +361,7 @@ describe('Play Text Input', () => { checkTextValueEquals(incorrectAppData.data.text, false); explanationShouldNotBeVisible(); + cy.checkHintsPlay(textAppSettingsData.hints); checkInputDisabled(false, false); checkAnswerAndHeaderStatus(false); @@ -348,6 +369,7 @@ describe('Play Text Input', () => { checkAnswerAndHeaderStatus(true); checkInputDisabled(true, true); cy.checkExplanationPlay(data.explanation); + cy.checkHintsPlay(null); }); }); @@ -373,6 +395,7 @@ describe('Play Text Input', () => { checkAnswerAndHeaderStatus(true); checkInputDisabled(true, true); cy.checkExplanationPlay(data.explanation); + cy.checkHintsPlay(null); }); }); }); diff --git a/cypress/fixtures/appSettings.ts b/cypress/fixtures/appSettings.ts index d0fc7315..af98b286 100644 --- a/cypress/fixtures/appSettings.ts +++ b/cypress/fixtures/appSettings.ts @@ -49,6 +49,7 @@ export const MULTIPLE_CHOICES_APP_SETTING: QuestionDataAppSetting = { }, ], explanation: 'my explanation for multiple choice', + hints: 'my hints for multiple choice', }, }; @@ -63,6 +64,7 @@ export const TEXT_INPUT_APP_SETTING: QuestionDataAppSetting = { question: 'My text input question', text: 'my text input', explanation: 'my explanation for text input', + hints: 'my hints for text input', }, }; @@ -79,6 +81,7 @@ export const SLIDER_APP_SETTING: QuestionDataAppSetting = { max: 110, value: 30, explanation: 'my explanation for slider', + hints: 'my hints for slider', }, }; @@ -93,6 +96,7 @@ export const FILL_BLANKS_SETTING: QuestionDataAppSetting = { question: 'My fill in the blanks question', text: 'Lorem dolor sit amet, consectetur adipiscing elit. ut fermentum nulla, sed sem.', explanation: 'my explanation for fill in the blanks', + hints: 'my hints for fill in the blanks', }, }; @@ -152,6 +156,7 @@ export const CAPITAL_FRANCE_SETTING = { }, ], explanation: 'Paris is the capital of France.', + hints: 'Think about the iconic Eiffel Tower.' }, name: APP_SETTING_NAMES.QUESTION, }; @@ -179,6 +184,7 @@ export const BABY_CAT_SETTING = { data: { questionId: 'id61', question: 'What is a baby cat called?', + hints: 'It is like "kitchen"', type: QuestionType.TEXT_INPUT, text: 'kitten', }, diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index b7be238c..9c70ca3e 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -7,6 +7,8 @@ import { CREATE_VIEW_ERROR_ALERT_CY, EXPLANATION_CY, EXPLANATION_PLAY_CY, + HINTS_CY, + HINTS_PLAY_CY, NAVIGATION_ANALYTICS_BUTTON_CY, NAVIGATION_RESULT_BUTTON_CY, NUMBER_OF_ATTEMPTS_CIRCULAR_PROGRESSION_CY, @@ -65,29 +67,47 @@ Cypress.Commands.add('checkStepStatus', (id, isCorrect) => { }); }); -Cypress.Commands.add('checkExplanationPlay', (explanation) => { - if (explanation) { - cy.get(`${dataCyWrapper(EXPLANATION_PLAY_CY)}`).should( - 'contain', - explanation - ); +const checkTextFieldPlay = (value: string, dataCy: string) => { + if (value) { + cy.get(`${dataCyWrapper(dataCy)}`).should('contain', value); } else { - cy.get(`${dataCyWrapper(EXPLANATION_PLAY_CY)}`).should('not.exist'); + cy.get(`${dataCyWrapper(dataCy)}`).should('not.exist'); } +}; + +const checkField = (value: string, dataCy: string) => { + cy.get(`${dataCyWrapper(dataCy)} textarea:first`).should('have.value', value); +}; + +const fillTextArea = (value: string, dataCy: string) => { + cy.get(`${dataCyWrapper(dataCy)} textarea:first`).clear(); + if (value.length) { + cy.get(`${dataCyWrapper(dataCy)} textarea:first`).type(value); + } +}; + +Cypress.Commands.add('checkExplanationPlay', (explanation) => { + checkTextFieldPlay(explanation, EXPLANATION_PLAY_CY); }); Cypress.Commands.add('checkExplanationField', (explanation) => { - cy.get(`${dataCyWrapper(EXPLANATION_CY)} textarea:first`).should( - 'have.value', - explanation - ); + checkField(explanation, EXPLANATION_CY); }); Cypress.Commands.add('fillExplanation', (explanation) => { - cy.get(`${dataCyWrapper(EXPLANATION_CY)} textarea:first`).clear(); - if (explanation.length) { - cy.get(`${dataCyWrapper(EXPLANATION_CY)} textarea:first`).type(explanation); - } + fillTextArea(explanation, EXPLANATION_CY); +}); + +Cypress.Commands.add('checkHintsPlay', (hints) => { + checkTextFieldPlay(hints, HINTS_PLAY_CY); +}); + +Cypress.Commands.add('checkHintsField', (hints) => { + checkField(hints, HINTS_CY); +}); + +Cypress.Commands.add('fillHints', (hints) => { + fillTextArea(hints, HINTS_CY); }); /** @@ -222,9 +242,7 @@ Cypress.Commands.add( if (severity === 'warning') { expect($el.attr('class').toLowerCase()).to.contain(MUI_ALERT_WARNING); } else if (severity === 'error') { - expect($el.attr('class').toLowerCase()).to.contain( - MUI_ALERT_ERROR - ); + expect($el.attr('class').toLowerCase()).to.contain(MUI_ALERT_ERROR); } }); } else { diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 75202f52..15c77578 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -47,6 +47,12 @@ declare global { fillExplanation(explanation: string): Chainable; + checkHintsPlay(hints: string): Chainable; + + checkHintsField(hints: string): Chainable; + + fillHints(hints: string): Chainable; + /** * Command to check that the progression of attempts is displayed correctly. * Also checks that the number of attempts are styled correctly diff --git a/src/components/create/CreateView.tsx b/src/components/create/CreateView.tsx index 3d139f40..9a4fffc0 100644 --- a/src/components/create/CreateView.tsx +++ b/src/components/create/CreateView.tsx @@ -12,7 +12,10 @@ import { CREATE_VIEW_DELETE_BUTTON_CY, CREATE_VIEW_ERROR_ALERT_CY, CREATE_VIEW_SAVE_BUTTON_CY, + EXPLANATION_CY, + HINTS_CY, } from '../../config/selectors'; +import { QUIZ_TRANSLATIONS } from '../../langs/constants'; import { QuizContext } from '../context/QuizContext'; import { isDifferent, validateQuestionData } from '../context/utilities'; import { @@ -21,7 +24,6 @@ import { } from '../types/types'; import { QuestionData } from '../types/types'; import CreateQuestionTopBar from './CreateQuestionTopBar'; -import Explanation from './Explanation'; import FillInTheBlanks from './FillInTheBlanks'; import MultipleChoices from './MultipleChoices'; import NumberOfAttempts from './NumberOfAttempts'; @@ -29,6 +31,7 @@ import QuestionTitle from './QuestionTitle'; import QuestionTypeSelect from './QuestionTypeSelect'; import Slider from './Slider'; import TextInput from './TextInput'; +import TitleDescriptionTextField from './TitleDescriptionTextField'; type ValidationSeverity = 'warning' | 'error'; @@ -46,7 +49,7 @@ const CreateView = () => { const [errorMessage, setErrorMessage] = useState(); const [isSubmitted, setIsSubmitted] = useState(false); - // Reset is submitted when currentIdx changed to + // Reset is submitted when currentIdx changed to // display errorMessage with the correct severity. useEffect(() => { setIsSubmitted(false); @@ -204,7 +207,28 @@ const CreateView = () => { - { + setNewData({ + ...newData, + hints, + }); + }} + dataCy={HINTS_CY} + /> + + + + { setNewData({ @@ -212,6 +236,7 @@ const CreateView = () => { explanation, }); }} + dataCy={EXPLANATION_CY} /> {errorMessage && ( diff --git a/src/components/create/Explanation.tsx b/src/components/create/Explanation.tsx deleted file mode 100644 index 69ac0545..00000000 --- a/src/components/create/Explanation.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { TextField, Typography } from '@mui/material'; - -import { EXPLANATION_CY } from '../../config/selectors'; - -type Props = { value?: string; onChange: (s: string) => void }; - -const Explanation = ({ value: explanation, onChange }: Props) => { - const { t } = useTranslation(); - - const value = useMemo(() => explanation ?? '', [explanation]); - - return ( - <> - {t('Explanation')} - { - - {t( - 'Type here an explanation that will be displayed after an answer is submitted' - )} - - } - onChange(t.target.value)} - multiline - /> - - ); -}; - -export default Explanation; diff --git a/src/components/create/TitleDescriptionTextField.tsx b/src/components/create/TitleDescriptionTextField.tsx new file mode 100644 index 00000000..8afba12b --- /dev/null +++ b/src/components/create/TitleDescriptionTextField.tsx @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; + +import { TextField, Typography } from '@mui/material'; + +type Props = { + title: string; + subTitle: string; + label: string; + value?: string; + dataCy: string; + onChange: (s: string) => void; +}; + +const TitleDescriptionTextField = ({ + title, + subTitle, + label, + value, + dataCy, + onChange, +}: Props) => { + const textFieldValue = useMemo(() => value ?? '', [value]); + + return ( + <> + {title} + { + + {subTitle} + + } + onChange(t.target.value)} + multiline + /> + + ); +}; + +export default TitleDescriptionTextField; diff --git a/src/components/play/PlayHints.tsx b/src/components/play/PlayHints.tsx new file mode 100644 index 00000000..efa71dee --- /dev/null +++ b/src/components/play/PlayHints.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from 'react-i18next'; + +import { Alert, AlertTitle } from '@mui/material'; + +import { HINTS_PLAY_CY } from '../../config/selectors'; +import { QUIZ_TRANSLATIONS } from '../../langs/constants'; + +type Props = { + hints: string | undefined; + maxAttempts: number; + currentAttempts: number; + isCorrect: boolean; +}; + +export const PlayHints = ({ + hints, + maxAttempts, + currentAttempts, + isCorrect, +}: Props) => { + const { t } = useTranslation(); + + if ( + hints && + !isCorrect && + currentAttempts > 0 && + currentAttempts < maxAttempts + ) { + return ( + + {t(QUIZ_TRANSLATIONS.HINTS_ALERT_TITLE)} + {hints} + + ); + } + + return null; +}; + +export default PlayHints; diff --git a/src/components/play/PlayView.tsx b/src/components/play/PlayView.tsx index 30d610b0..33774c0b 100644 --- a/src/components/play/PlayView.tsx +++ b/src/components/play/PlayView.tsx @@ -33,6 +33,7 @@ import { } from '../types/types'; import PlayExplanation from './PlayExplanation'; import PlayFillInTheBlanks from './PlayFillInTheBlanks'; +import PlayHints from './PlayHints'; import PlayMultipleChoices from './PlayMultipleChoices'; import PlaySlider from './PlaySlider'; import PlayTextInput from './PlayTextInput'; @@ -255,6 +256,14 @@ const PlayView = () => { } })()} + + + `playViewTextInput-${isCorrect}`; export const EXPLANATION_CY = 'explanation'; export const EXPLANATION_PLAY_CY = 'explanationPlay'; +export const HINTS_CY = 'hints'; +export const HINTS_PLAY_CY = 'hintsPlay'; export const buildFillBlanksAnswerId = (id: number) => `fillBlanksAnswer-${id}`; export const buildBlankedTextWordCy = (id: number) => `fillBlankedTextWord-${id}`; diff --git a/src/langs/constants.ts b/src/langs/constants.ts index deabb6d8..a472df15 100644 --- a/src/langs/constants.ts +++ b/src/langs/constants.ts @@ -1,7 +1,11 @@ export const QUIZ_TRANSLATIONS = { - NO_RESPONSE_FOR_NOW: 'NO_RESPONSE_FOR_NOW', - NO_DATA_FOR_GENERAL_CHARTS: 'NO_DATA_FOR_GENERAL_CHARTS', - ATTEMPTS_PROGRESS_NUMBER_OF_ATTEMPTS: 'ATTEMPTS_PROGRESS_NUMBER_OF_ATTEMPTS', - CREATE_VIEW_NUMBER_OF_ATTEMPTS: 'CREATE_VIEW_NUMBER_OF_ATTEMPTS', - MULTIPLE_CHOICE_NOT_CORRECT: 'MULTIPLE_CHOICE_NOT_CORRECT', -} as const \ No newline at end of file + NO_RESPONSE_FOR_NOW: 'NO_RESPONSE_FOR_NOW', + NO_DATA_FOR_GENERAL_CHARTS: 'NO_DATA_FOR_GENERAL_CHARTS', + ATTEMPTS_PROGRESS_NUMBER_OF_ATTEMPTS: 'ATTEMPTS_PROGRESS_NUMBER_OF_ATTEMPTS', + CREATE_VIEW_NUMBER_OF_ATTEMPTS: 'CREATE_VIEW_NUMBER_OF_ATTEMPTS', + MULTIPLE_CHOICE_NOT_CORRECT: 'MULTIPLE_CHOICE_NOT_CORRECT', + HINTS_TITLE: 'HINTS_TITLE', + HINTS_SUB_TITLE: 'HINTS_SUB_TITLE', + HINTS_LABEL: 'HINTS_LABEL', + HINTS_ALERT_TITLE: 'HINTS_ALERT_TITLE', +} as const; diff --git a/src/langs/en.ts b/src/langs/en.ts index 40e8e3f9..788d9146 100644 --- a/src/langs/en.ts +++ b/src/langs/en.ts @@ -83,5 +83,9 @@ export default { ATTEMPTS_PROGRESS_NUMBER_OF_ATTEMPTS: 'Number of attempts', CREATE_VIEW_NUMBER_OF_ATTEMPTS: 'Number of attempts', MULTIPLE_CHOICE_NOT_CORRECT: 'The answer is incorrect or not completely correct.', + HINTS_TITLE: 'Hints', + HINTS_SUB_TITLE: 'Enter here the hints that will be displayed if the answer is incorrect, to help the student', + HINTS_LABEL: 'Hints', + HINTS_ALERT_TITLE: 'Do you need some hints ?' }, }; diff --git a/src/langs/fr.ts b/src/langs/fr.ts index a659a1ac..39dcd5c6 100644 --- a/src/langs/fr.ts +++ b/src/langs/fr.ts @@ -78,9 +78,16 @@ export default { 'Error, chart type unknown': 'Erreur, type de graphique inconnu', blank: 'vide', NO_RESPONSE_FOR_NOW: "Il n'y a pas encore de réponse.", - NO_DATA_FOR_GENERAL_CHARTS: 'Aucune donnée n\'a été trouvé pour les graphiques.', + NO_DATA_FOR_GENERAL_CHARTS: + "Aucune donnée n'a été trouvé pour les graphiques.", ATTEMPTS_PROGRESS_NUMBER_OF_ATTEMPTS: 'Nombre de tentatives', CREATE_VIEW_NUMBER_OF_ATTEMPTS: 'Nombre de tentatives', - MULTIPLE_CHOICE_NOT_CORRECT: "La réponse est incorrecte ou n'est pas entièrement correcte.", + MULTIPLE_CHOICE_NOT_CORRECT: + "La réponse est incorrecte ou n'est pas entièrement correcte.", + HINTS_TITLE: 'Indices', + HINTS_SUB_TITLE: + "Saisissez ici les indices qui s'afficheront si la réponse est incorrecte, afin d'aider l'étudiant", + HINTS_LABEL: 'Indices', + HINTS_ALERT_TITLE: "Avez-vous besoins d'indices ?" }, };