From 9072bb66b7cc36cdd3f7d76e1170bf0f05b162dc Mon Sep 17 00:00:00 2001 From: Jesus Balderrama Date: Fri, 19 Dec 2025 12:38:19 -0600 Subject: [PATCH] feat: better validation for NumericalInput problem editor (#2615) * feat(form): add validation to NumericalInput to accept only numeric values * style(format): fix spaces and update message to camelCase * fix(content): update text for clarity Co-authored-by: Kyle McCormick * feat(validation): validation added to numeric input with new endpoint to see if is a valid math expression * fix(content): change in input validation to use react query instead of redux * fix(content): change in types to avoid ci errors * fix(content): remove unnecessary code after changing to react query * fix(content): change numeric input validation path to new url and loader added * feat: returning data in camelcase, improve UI in validation * feat: tests added to problem editor --------- Co-authored-by: Kyle McCormick --- .../AnswerWidget/AnswerOption.jsx | 35 ++++++--- .../AnswerWidget/AnswerOption.test.tsx | 13 ++++ .../EditProblemView/AnswerWidget/messages.js | 5 ++ .../ProblemEditor/data/apiHooks.test.tsx | 69 ++++++++++++++++++ .../containers/ProblemEditor/data/apiHooks.ts | 19 +++++ src/editors/data/images/numericalInput.png | Bin 7320 -> 2710 bytes src/editors/data/services/cms/api.ts | 7 ++ src/editors/data/services/cms/urls.ts | 4 + 8 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 src/editors/containers/ProblemEditor/data/apiHooks.test.tsx create mode 100644 src/editors/containers/ProblemEditor/data/apiHooks.ts diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx index 897ff07b0..1420125ea 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx @@ -19,6 +19,7 @@ import * as hooks from './hooks'; import { ProblemTypeKeys } from '../../../../../data/constants/problem'; import ExpandableTextArea from '../../../../../sharedComponents/ExpandableTextArea'; import { answerRangeFormatRegex } from '../../../data/OLXParser'; +import { useValidateInputBlock } from '../../../data/apiHooks'; const AnswerOption = ({ answer, @@ -32,7 +33,6 @@ const AnswerOption = ({ const isLibrary = useSelector(selectors.app.isLibrary); const learningContextId = useSelector(selectors.app.learningContextId); const blockId = useSelector(selectors.app.blockId); - const removeAnswer = hooks.removeAnswer({ answer, dispatch }); const setAnswer = hooks.setAnswer({ answer, hasSingleAnswer, dispatch }); const setAnswerTitle = hooks.setAnswerTitle({ @@ -44,6 +44,7 @@ const AnswerOption = ({ const setSelectedFeedback = hooks.setSelectedFeedback({ answer, hasSingleAnswer, dispatch }); const setUnselectedFeedback = hooks.setUnselectedFeedback({ answer, hasSingleAnswer, dispatch }); const { isFeedbackVisible, toggleFeedback } = hooks.useFeedback(answer); + const { data = { isValid: true }, mutate } = useValidateInputBlock(); const staticRootUrl = isLibrary ? `${getConfig().STUDIO_BASE_URL}/library_assets/blocks/${blockId}/` @@ -69,17 +70,31 @@ const AnswerOption = ({ /> ); } + if (problemType !== ProblemTypeKeys.NUMERIC || !answer.isAnswerRange) { return ( - + + { + setAnswerTitle(e); + if (problemType === ProblemTypeKeys.NUMERIC) { + mutate(e.target.value); + } + }} + placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)} + + /> + {(!data?.isValid ?? true) && ( + + + + )} + ); } // Return Answer Range View diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.tsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.tsx index a97a6998c..8afbea7a5 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.tsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.tsx @@ -3,6 +3,7 @@ import { render, screen, initializeMocks } from '@src/testUtils'; import { selectors } from '@src/editors/data/redux'; import AnswerOption from './AnswerOption'; import * as hooks from './hooks'; +import * as reactQueryHooks from '../../../data/apiHooks'; const { problem } = selectors; @@ -101,4 +102,16 @@ describe('AnswerOption', () => { expect(screen.getByText(answerRange.title)).toBeInTheDocument(); expect(screen.getByRole('textbox')).toBeInTheDocument(); }); + + test('shows numeric error feedback when data.isValid is false', () => { + // Mock useValidateInputBlock to simulate invalid state + // @ts-ignore-next-line + jest.spyOn(reactQueryHooks, 'useValidateInputBlock').mockReturnValue({ data: { isValid: false } }); + jest.spyOn(problem, 'problemType').mockReturnValue('numericalresponse'); + const myProps = { ...props, answer: { ...answerWithOnlyFeedback, isAnswerRange: false } }; + render(); + expect( + screen.getByText('Error: This input type only supports numeric answers. Did you mean to make a Text input or Math expression input problem?'), + ).toBeInTheDocument(); + }); }); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js index d69bc876d..c0dcec252 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js @@ -82,6 +82,11 @@ const messages = defineMessages({ defaultMessage: 'Error: Invalid range format. Use brackets or parentheses with values separated by a comma.', description: 'Error text describing wrong format of answer ranges', }, + answerNumericErrorText: { + id: 'authoring.answerwidget.answer.answerNumericErrorText', + defaultMessage: 'Error: This input type only supports numeric answers. Did you mean to make a Text input or Math expression input problem?', + description: 'Error message when user provides wrong format', + }, }); export default messages; diff --git a/src/editors/containers/ProblemEditor/data/apiHooks.test.tsx b/src/editors/containers/ProblemEditor/data/apiHooks.test.tsx new file mode 100644 index 000000000..bc72c1f26 --- /dev/null +++ b/src/editors/containers/ProblemEditor/data/apiHooks.test.tsx @@ -0,0 +1,69 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import api from '@src/editors/data/services/cms/api'; +import { useValidateInputBlock } from './apiHooks'; + +// Mock external dependencies +jest.mock('@edx/frontend-platform'); +jest.mock('@src/editors/data/services/cms/api', () => ({ + validateBlockNumericInput: jest.fn(), +})); + +const mockedCamelCaseObject = jest.mocked(camelCaseObject); +const mockedGetConfig = jest.mocked(getConfig); +const mockedValidateBlockNumericInput = jest.mocked(api.validateBlockNumericInput); + +// Test wrapper component +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + const wrapper = ({ children }) => ( + + {children} + + ); + return wrapper; +}; + +describe('useValidateInputBlock', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockedGetConfig.mockReturnValue({ + STUDIO_BASE_URL: 'http://studio.local.openedx.io:8001', + }); + }); + + test('should return camelCase data on successful API call', async () => { + const mockResponse = { + data: { is_valid: true, result: 'success' }, + } as any; + const mockCamelCaseResult = { isValid: true, result: 'success' }; + + mockedValidateBlockNumericInput.mockResolvedValue(Promise.resolve(mockResponse)); + mockedCamelCaseObject.mockReturnValue(mockCamelCaseResult); + + const { result } = renderHook(() => useValidateInputBlock(), { + wrapper: createWrapper(), + }); + + const testFormula = 'x + 1'; + result.current.mutate(testFormula); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(mockedValidateBlockNumericInput).toHaveBeenCalledWith({ + studioEndpointUrl: 'http://studio.local.openedx.io:8001', + data: { formula: testFormula }, + }); + expect(mockedCamelCaseObject).toHaveBeenCalledWith(mockResponse.data); + expect(result.current.data).toEqual({ isValid: true, result: 'success' }); + }); +}); diff --git a/src/editors/containers/ProblemEditor/data/apiHooks.ts b/src/editors/containers/ProblemEditor/data/apiHooks.ts new file mode 100644 index 000000000..610ffa847 --- /dev/null +++ b/src/editors/containers/ProblemEditor/data/apiHooks.ts @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import api from '@src/editors/data/services/cms/api'; + +const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; + +export const useValidateInputBlock = () => useMutation({ + mutationFn: async (title : string) => { + try { + const res = await api.validateBlockNumericInput({ studioEndpointUrl: `${getApiBaseUrl()}`, data: { formula: title } }); + return camelCaseObject(res.data); + } catch (err: any) { + return { + isValid: false, + error: err.response?.data?.error ?? 'Unknown error', + }; + } + }, +}); diff --git a/src/editors/data/images/numericalInput.png b/src/editors/data/images/numericalInput.png index 535631d3b6d16fb23e6bf1ce89cbd6a788fef9c3..b4a285a5f90e2ac2ea374d20d04633021350847a 100644 GIT binary patch literal 2710 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 O0S~o}w6Gesk^ci1ntuua literal 7320 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 W$6nltB;&xrz!f74L&_!RhyMf1A(Te| diff --git a/src/editors/data/services/cms/api.ts b/src/editors/data/services/cms/api.ts index ec4cd7d76..aceb01c76 100644 --- a/src/editors/data/services/cms/api.ts +++ b/src/editors/data/services/cms/api.ts @@ -390,6 +390,13 @@ export const apiMethods = { }) => get( urls.handlerUrl({ studioEndpointUrl, blockId, handlerName }), ), + validateBlockNumericInput: ({ + studioEndpointUrl, + data, + }) => post( + urls.validateNumericInputUrl({ studioEndpointUrl }), + data, + ), }; export default apiMethods; diff --git a/src/editors/data/services/cms/urls.ts b/src/editors/data/services/cms/urls.ts index 6da1e2203..28398055f 100644 --- a/src/editors/data/services/cms/urls.ts +++ b/src/editors/data/services/cms/urls.ts @@ -123,3 +123,7 @@ export const courseVideos = (({ studioEndpointUrl, learningContextId }) => ( export const handlerUrl = (({ studioEndpointUrl, blockId, handlerName }) => ( `${studioEndpointUrl}/api/xblock/v2/xblocks/${blockId}/handler_url/${handlerName}/` )) satisfies UrlFunction; + +export const validateNumericInputUrl = (({ studioEndpointUrl }) => ( + `${studioEndpointUrl}/api/contentstore/v2/validate/numerical-input/` +)) satisfies UrlFunction;