diff --git a/cypress/e2e/Admin/create/fillBlanks.cy.ts b/cypress/e2e/Admin/create/fillBlanks.cy.ts index 665ebfab..8536a5a9 100644 --- a/cypress/e2e/Admin/create/fillBlanks.cy.ts +++ b/cypress/e2e/Admin/create/fillBlanks.cy.ts @@ -9,7 +9,6 @@ import i18n from '../../../../src/config/i18n'; import { CREATE_QUESTION_SELECT_TYPE_CY, CREATE_QUESTION_TITLE_CY, - CREATE_VIEW_ERROR_ALERT_CY, CREATE_VIEW_SAVE_BUTTON_CY, FILL_BLANKS_TEXT_FIELD_CY, QUESTION_BAR_ADD_NEW_BUTTON_CLASSNAME, @@ -61,6 +60,8 @@ const fillBlanksQuestion = ( // save if (shouldSave) { cy.get(dataCyWrapper(CREATE_VIEW_SAVE_BUTTON_CY)).click(); + } else { + cy.get(dataCyWrapper(CREATE_VIEW_SAVE_BUTTON_CY)).should('be.disabled'); } }; @@ -81,11 +82,10 @@ describe('Fill in the Blanks', () => { // empty text const new1 = { ...newFillBlanksData, text: '' }; - fillBlanksQuestion(new1); - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should( - 'contain', - t(FAILURE_MESSAGES.FILL_BLANKS_EMPTY_TEXT) - ); + fillBlanksQuestion(new1, { shouldSave: false }); + cy.checkErrorMessage({ + errorMessage: t(FAILURE_MESSAGES.FILL_BLANKS_EMPTY_TEXT), + }); // faulty text const new2 = { @@ -93,14 +93,12 @@ describe('Fill in the Blanks', () => { text: 'my ', }; fillBlanksQuestion(new2, { shouldSave: false }); - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should( - 'contain', - t(FAILURE_MESSAGES.FILL_BLANKS_UNMATCHING_TAGS) - ); + cy.checkErrorMessage({ + errorMessage: t(FAILURE_MESSAGES.FILL_BLANKS_UNMATCHING_TAGS), + }); fillBlanksQuestion(newFillBlanksData); - - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should('not.exist'); + cy.checkErrorMessage({}); }); describe('Display saved settings', () => { diff --git a/cypress/e2e/Admin/create/multipleChoices.cy.ts b/cypress/e2e/Admin/create/multipleChoices.cy.ts index 40ed2fb2..b1f9d033 100644 --- a/cypress/e2e/Admin/create/multipleChoices.cy.ts +++ b/cypress/e2e/Admin/create/multipleChoices.cy.ts @@ -10,7 +10,6 @@ import i18n from '../../../../src/config/i18n'; import { CREATE_QUESTION_SELECT_TYPE_CY, CREATE_QUESTION_TITLE_CY, - CREATE_VIEW_ERROR_ALERT_CY, CREATE_VIEW_SAVE_BUTTON_CY, MULTIPLE_CHOICES_ADD_ANSWER_BUTTON_CY, MULTIPLE_CHOICES_ANSWER_CORRECTNESS_CLASSNAME, @@ -24,7 +23,10 @@ import { buildQuestionStepCy, dataCyWrapper, } from '../../../../src/config/selectors'; -import { APP_SETTINGS, QUESTION_APP_SETTINGS } from '../../../fixtures/appSettings'; +import { + APP_SETTINGS, + QUESTION_APP_SETTINGS, +} from '../../../fixtures/appSettings'; const t = i18n.t; @@ -129,6 +131,8 @@ export const fillMultipleChoiceQuestion = ( // save if (shouldSave) { cy.get(dataCyWrapper(CREATE_VIEW_SAVE_BUTTON_CY)).click(); + } else { + cy.get(dataCyWrapper(CREATE_VIEW_SAVE_BUTTON_CY)).should('be.disabled'); } }; @@ -147,11 +151,8 @@ describe('Multiple Choices', () => { // no question text const new1 = { ...newMultipleChoiceData, question: '' }; - fillMultipleChoiceQuestion(new1); - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should( - 'contain', - t(FAILURE_MESSAGES.EMPTY_QUESTION) - ); + fillMultipleChoiceQuestion(new1, { shouldSave: false }); + cy.checkErrorMessage({ errorMessage: t(FAILURE_MESSAGES.EMPTY_QUESTION) }); // empty answer const new2 = { @@ -162,10 +163,9 @@ describe('Multiple Choices', () => { ], }; fillMultipleChoiceQuestion(new2, { shouldSave: false }); - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should( - 'contain', - t(FAILURE_MESSAGES.MULTIPLE_CHOICES_EMPTY_CHOICE) - ); + cy.checkErrorMessage({ + errorMessage: t(FAILURE_MESSAGES.MULTIPLE_CHOICES_EMPTY_CHOICE), + }); // no correct answer const new3 = { @@ -177,13 +177,12 @@ describe('Multiple Choices', () => { ], }; fillMultipleChoiceQuestion(new3, { shouldSave: false }); - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should( - 'contain', - t(FAILURE_MESSAGES.MULTIPLE_CHOICES_CORRECT_ANSWER) - ); + cy.checkErrorMessage({ + errorMessage: t(FAILURE_MESSAGES.MULTIPLE_CHOICES_CORRECT_ANSWER), + }); fillMultipleChoiceQuestion(newMultipleChoiceData); - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should('not.exist'); + cy.checkErrorMessage({}); cy.checkExplanationField(newMultipleChoiceData.explanation); }); diff --git a/cypress/e2e/Admin/create/slider.cy.ts b/cypress/e2e/Admin/create/slider.cy.ts index 0420a157..2c1f8f64 100644 --- a/cypress/e2e/Admin/create/slider.cy.ts +++ b/cypress/e2e/Admin/create/slider.cy.ts @@ -19,7 +19,10 @@ import { buildQuestionStepCy, dataCyWrapper, } from '../../../../src/config/selectors'; -import { APP_SETTINGS, QUESTION_APP_SETTINGS } from '../../../fixtures/appSettings'; +import { + APP_SETTINGS, + QUESTION_APP_SETTINGS, +} from '../../../fixtures/appSettings'; const t = i18n.t; @@ -80,6 +83,8 @@ const fillSliderQuestion = ( // save if (shouldSave) { cy.get(dataCyWrapper(CREATE_VIEW_SAVE_BUTTON_CY)).click(); + } else { + cy.get(dataCyWrapper(CREATE_VIEW_SAVE_BUTTON_CY)).should('be.disabled'); } }; @@ -101,25 +106,23 @@ describe('Slider', () => { // empty min const new1 = { ...newSliderData, min: null } as SliderValues; const dataValue = (data as SliderAppDataData).value; - fillSliderQuestion(new1, dataValue); - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should( - 'contain', - t(FAILURE_MESSAGES.SLIDER_UNDEFINED_MIN_MAX) - ); + fillSliderQuestion(new1, dataValue, { shouldSave: false }); + cy.checkErrorMessage({ + errorMessage: t(FAILURE_MESSAGES.SLIDER_UNDEFINED_MIN_MAX), + }); + // empty max const new2 = { ...newSliderData, max: null } as SliderValues; fillSliderQuestion(new2, new1.value, { shouldSave: false }); - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should( - 'contain', - t(FAILURE_MESSAGES.SLIDER_UNDEFINED_MIN_MAX) - ); + cy.checkErrorMessage({ + errorMessage: t(FAILURE_MESSAGES.SLIDER_UNDEFINED_MIN_MAX), + }); // // min higher than max const new3 = { ...newSliderData, min: 100, max: 30 }; fillSliderQuestion(new3, new2.value, { shouldSave: false }); - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should( - 'contain', - t(FAILURE_MESSAGES.SLIDER_MIN_SMALLER_THAN_MAX) - ); + cy.checkErrorMessage({ + errorMessage: t(FAILURE_MESSAGES.SLIDER_MIN_SMALLER_THAN_MAX), + }); fillSliderQuestion(newSliderData, new3.value); cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should('not.exist'); diff --git a/cypress/e2e/Admin/create/textInput.cy.ts b/cypress/e2e/Admin/create/textInput.cy.ts index 70f77d77..217ee720 100644 --- a/cypress/e2e/Admin/create/textInput.cy.ts +++ b/cypress/e2e/Admin/create/textInput.cy.ts @@ -2,19 +2,25 @@ import { Context, PermissionLevel } from '@graasp/sdk'; import { APP_SETTING_NAMES, + FAILURE_MESSAGES, QuestionType, } from '../../../../src/config/constants'; +import i18n from '../../../../src/config/i18n'; import { CREATE_QUESTION_SELECT_TYPE_CY, CREATE_QUESTION_TITLE_CY, - CREATE_VIEW_ERROR_ALERT_CY, CREATE_VIEW_SAVE_BUTTON_CY, QUESTION_BAR_ADD_NEW_BUTTON_CLASSNAME, TEXT_INPUT_FIELD_CY, buildQuestionStepCy, dataCyWrapper, } from '../../../../src/config/selectors'; -import { APP_SETTINGS, QUESTION_APP_SETTINGS } from '../../../fixtures/appSettings'; +import { + APP_SETTINGS, + QUESTION_APP_SETTINGS, +} from '../../../fixtures/appSettings'; + +const t = i18n.t; const newTextInputData = { question: 'new question text', @@ -55,6 +61,8 @@ const fillTextInputQuestion = ( // save if (shouldSave) { cy.get(dataCyWrapper(CREATE_VIEW_SAVE_BUTTON_CY)).click(); + } else { + cy.get(dataCyWrapper(CREATE_VIEW_SAVE_BUTTON_CY)).should('be.disabled'); } }; @@ -78,9 +86,10 @@ describe('Text Input', () => { ...newTextInputData, text: '', }; - fillTextInputQuestion(new1); - - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should('not.exist'); + fillTextInputQuestion(new1, { shouldSave: false }); + cy.checkErrorMessage({ + errorMessage: t(FAILURE_MESSAGES.TEXT_INPUT_NOT_EMPTY), + }); }); it('Start with empty data and save question with non-empty response', () => { @@ -98,7 +107,7 @@ describe('Text Input', () => { cy.switchQuestionType(QuestionType.TEXT_INPUT); fillTextInputQuestion(newTextInputData); - cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should('not.exist'); + cy.checkErrorMessage({}); }); describe('Display saved settings', () => { diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 8122a145..b7be238c 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -4,6 +4,7 @@ import { Context, PermissionLevel } from '@graasp/sdk'; import { API_HOST } from '../../src/config/constants'; import { CREATE_QUESTION_SELECT_TYPE_CY, + CREATE_VIEW_ERROR_ALERT_CY, EXPLANATION_CY, EXPLANATION_PLAY_CY, NAVIGATION_ANALYTICS_BUTTON_CY, @@ -198,3 +199,36 @@ Cypress.Commands.add( } } ); + +Cypress.Commands.add( + 'checkErrorMessage', + ({ + errorMessage, + severity = 'warning', + }: { + errorMessage?: string; + severity?: 'error' | 'warning'; + } = {}) => { + const MUI_ALERT_WARNING = 'MuiAlert-standardWarning'.toLowerCase(); + const MUI_ALERT_ERROR = 'MuiAlert-standardError'.toLowerCase(); + + if (errorMessage) { + cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should( + 'contain', + errorMessage + ); + + cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).then(($el) => { + 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 + ); + } + }); + } else { + cy.get(dataCyWrapper(CREATE_VIEW_ERROR_ALERT_CY)).should('not.exist'); + } + } +); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 1ee97a4d..75202f52 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -65,6 +65,14 @@ declare global { currentAttempts: number; isCorrect?: boolean; }): Chainable; + + checkErrorMessage({ + errorMessage, + severity, + }: { + errorMessage?: string; + severity?: 'error' | 'warning'; + }): Chainable; } } } diff --git a/src/components/context/utilities.ts b/src/components/context/utilities.ts index 6de0b58d..0797e4d4 100644 --- a/src/components/context/utilities.ts +++ b/src/components/context/utilities.ts @@ -250,11 +250,11 @@ export const validateQuestionData = (data: QuestionData) => { break; // enable following lines to prevent empty correct answer - // case QuestionType.TEXT_INPUT: - // if (!data?.text?.length) { - // throw FAILURE_MESSAGES.TEXT_INPUT_NOT_EMPTY; - // } - // break; + case QuestionType.TEXT_INPUT: + if (!data?.text?.length) { + throw FAILURE_MESSAGES.TEXT_INPUT_NOT_EMPTY; + } + break; default: return true; } diff --git a/src/components/create/CreateView.tsx b/src/components/create/CreateView.tsx index 6e567a2c..3d139f40 100644 --- a/src/components/create/CreateView.tsx +++ b/src/components/create/CreateView.tsx @@ -30,28 +30,42 @@ import QuestionTypeSelect from './QuestionTypeSelect'; import Slider from './Slider'; import TextInput from './TextInput'; +type ValidationSeverity = 'warning' | 'error'; + +type ValidationMessage = { + msg: string; + severity: ValidationSeverity; +}; + const CreateView = () => { const { t } = useTranslation(); - const { currentQuestion, deleteQuestion, saveQuestion } = + const { currentQuestion, deleteQuestion, saveQuestion, currentIdx } = useContext(QuizContext); const [newData, setNewData] = useState(currentQuestion.data); - const [errorMessage, setErrorMessage] = useState(); + const [errorMessage, setErrorMessage] = useState(); const [isSubmitted, setIsSubmitted] = useState(false); + // Reset is submitted when currentIdx changed to + // display errorMessage with the correct severity. + useEffect(() => { + setIsSubmitted(false); + }, [currentIdx]); + useEffect(() => { setNewData(currentQuestion.data as QuestionData); }, [currentQuestion]); // validate data to enable save useEffect(() => { - if (isSubmitted) { - try { - validateQuestionData(newData); - setErrorMessage(null); - } catch (e) { - setErrorMessage(e as string); - } + try { + validateQuestionData(newData); + setErrorMessage(null); + } catch (e) { + setErrorMessage({ + msg: e as string, + severity: isSubmitted ? 'error' : 'warning', + }); } }, [newData, isSubmitted]); @@ -62,7 +76,10 @@ const CreateView = () => { setErrorMessage(null); saveQuestion(newData); } catch (e) { - setErrorMessage(e as string); + setErrorMessage({ + msg: e as string, + severity: 'error', + }); } }; @@ -199,8 +216,11 @@ const CreateView = () => { {errorMessage && ( - - {t(errorMessage)} + + {t(errorMessage.msg)} )} diff --git a/src/config/constants.ts b/src/config/constants.ts index 2aa64bcc..ce18c6ba 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -113,7 +113,7 @@ export const FAILURE_MESSAGES = { TEXT_INPUT_NOT_EMPTY: 'TEXT_INPUT_NOT_EMPTY', FILL_BLANKS_EMPTY_TEXT: 'FILL_BLANKS_EMPTY_TEXT', FILL_BLANKS_UNMATCHING_TAGS: 'FILL_BLANKS_UNMATCHING_TAGS', -}; +} as const; export const DEFAULT_LANG = 'en';