From 1c2f02bea1c3164641d27d8ead5a3ed218a86c37 Mon Sep 17 00:00:00 2001 From: TylerZeroMaster <33585550+TylerZeroMaster@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:46:52 -0500 Subject: [PATCH] Save and continue when there is no feedback (#76) * Save and continue when there is no feedback Why do it this way? `onAnswerSave` is non-blocking and non-awaitable. Consequently, calling `onAnswerSave` in the onClick of the submit button does not work because this will advance to the next question before the current one is finished saving. We know that the api is done saving when `is_completed` becomes true. When `hasFeedback` is falsy and the submit button is clicked, `shouldContinue` is set to `true`. If both `shouldContinue` and `is_completed` are true, then the answer has been submitted and saved and it is okay to advance. * Update version to 1.2.0 [CORE-173] --- package.json | 2 +- src/components/Exercise.spec.tsx | 1 + src/components/Exercise.stories.tsx | 57 +++++++ src/components/Exercise.tsx | 1 + src/components/ExerciseQuestion.spec.tsx | 39 +++++ src/components/ExerciseQuestion.stories.tsx | 3 +- src/components/ExerciseQuestion.tsx | 32 +++- .../ExerciseQuestion.spec.tsx.snap | 159 ++++++++++++++++++ 8 files changed, 285 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 056c01f..4e9b12b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@openstax/assessment-components", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "source": "./src/index.ts", "types": "./dist/src/index.d.ts", diff --git a/src/components/Exercise.spec.tsx b/src/components/Exercise.spec.tsx index b3530a0..aff0bcd 100644 --- a/src/components/Exercise.spec.tsx +++ b/src/components/Exercise.spec.tsx @@ -142,6 +142,7 @@ describe('Exercise', () => { numberOfQuestions: 1, scrollToQuestion: 1, hasMultipleAttempts: false, + hasFeedback: true, onAnswerChange: () => null, onAnswerSave: () => null, onNextStep: () => null, diff --git a/src/components/Exercise.stories.tsx b/src/components/Exercise.stories.tsx index d1aeb86..ca6f65f 100644 --- a/src/components/Exercise.stories.tsx +++ b/src/components/Exercise.stories.tsx @@ -130,6 +130,7 @@ const exerciseWithQuestionStatesProps = (): ExerciseWithQuestionStatesProps => { apiIsPending: false } }, + hasFeedback: true, }}; type TextResizerValue = -2 | -1 | 0 | 1 | 2 | 3; @@ -181,6 +182,34 @@ export const Default = () => { ) }; +export const DefaultWithoutFeedback = () => { + const [selectedAnswerId, setSelectedAnswerId] = useState(0); + const [apiIsPending, setApiIsPending] = useState(false) + const [isCompleted, setIsCompleted] = useState(false) + const props = exerciseWithQuestionStatesProps(); + props.hasFeedback = false; + props.questionStates['1'].answer_id = selectedAnswerId; + props.questionStates['1'].apiIsPending = apiIsPending; + props.questionStates['1'].is_completed = isCompleted; + props.questionStates['1'].canAnswer = !isCompleted; + return ( + & { id: number, question_id: number }) => { + setSelectedAnswerId(a.id) + }} + onAnswerSave={() => { + setApiIsPending(true); + setTimeout(() => { + setApiIsPending(false) + setIsCompleted(true) + }, 1000) + }} + onNextStep={(idx) => console.log(`Next step: ${idx}`)} + /> + ) +}; + export const DeprecatedStepData = () => ; export const CompleteWithFeedback = () => { @@ -210,6 +239,34 @@ export const CompleteWithFeedback = () => { return ; }; +export const CompleteWithoutFeedback = () => { + const props: ExerciseWithQuestionStatesProps = { + ...exerciseWithQuestionStatesProps(), + + questionStates: { + '1': { + available_points: '1.0', + is_completed: true, + answer_id_order: ['1', '2'], + answer_id: 1, + free_response: 'Free response', + feedback_html: '', + correct_answer_id: '', + correct_answer_feedback_html: '', + attempts_remaining: 0, + attempt_number: 1, + incorrectAnswerId: 0, + canAnswer: false, + needsSaved: false, + apiIsPending: false + } + }, + hasFeedback: false, + }; + + return ; +}; + export const IncorrectWithFeedbackAndSolution = () => { const props: ExerciseWithQuestionStatesProps = { ...exerciseWithQuestionStatesProps() }; props.questionStates = { diff --git a/src/components/Exercise.tsx b/src/components/Exercise.tsx index 2df0332..0bf6995 100644 --- a/src/components/Exercise.tsx +++ b/src/components/Exercise.tsx @@ -138,6 +138,7 @@ export interface ExerciseBaseProps { * - A topic icon linking to the relevant textbook location */ exerciseIcons?: ExerciseIcons; + hasFeedback?: boolean; } export interface ExerciseWithStepDataProps extends ExerciseBaseProps { diff --git a/src/components/ExerciseQuestion.spec.tsx b/src/components/ExerciseQuestion.spec.tsx index 248a64d..0910149 100644 --- a/src/components/ExerciseQuestion.spec.tsx +++ b/src/components/ExerciseQuestion.spec.tsx @@ -50,6 +50,7 @@ describe('ExerciseQuestion', () => { displaySolution: false, available_points: '1.0', exercise_uid: '', + hasFeedback: true, } }); @@ -125,6 +126,21 @@ describe('ExerciseQuestion', () => { expect(tree).toMatchSnapshot(); }); + it('renders Submit & continue button', () => { + const tree = renderer.create( + + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); + it('renders continue button (unused?)', () => { const tree = renderer.create( { expect(mockFn).toHaveBeenCalledWith(0); }); + + it('passes question index on submit button click when there is not feedback', () => { + const mockFn = jest.fn(); + + // This combination of props should never happen: `is_completed` should not + // be true at the same time as `needsSaved`. This combination allows the + // test to work correctly without simulating waiting for api calls + const tree = renderer.create( + + ); + renderer.act(() => { + tree.root.findByType(SaveButton).props.onClick(); + }); + + expect(mockFn).toHaveBeenCalledWith(0); + }); }); diff --git a/src/components/ExerciseQuestion.stories.tsx b/src/components/ExerciseQuestion.stories.tsx index 8360861..0a92d2e 100644 --- a/src/components/ExerciseQuestion.stories.tsx +++ b/src/components/ExerciseQuestion.stories.tsx @@ -51,7 +51,8 @@ const props = { needsSaved: true, canUpdateCurrentStep: true, attempt_number: 0, - apiIsPending: false + apiIsPending: false, + hasFeedback: false }; export const Default = () => ; diff --git a/src/components/ExerciseQuestion.tsx b/src/components/ExerciseQuestion.tsx index 6148917..a4117eb 100644 --- a/src/components/ExerciseQuestion.tsx +++ b/src/components/ExerciseQuestion.tsx @@ -37,6 +37,7 @@ export interface ExerciseQuestionProps { free_response?: string; show_all_feedback?: boolean; tableFeedbackEnabled?: boolean; + hasFeedback?: ExerciseBaseProps['hasFeedback']; } const AttemptsRemaining = ({ count }: { count: number }) => { @@ -56,7 +57,7 @@ const PublishedComments = ({ published_comments }: { published_comments?: string } export const SaveButton = (props: { - disabled: boolean, isWaiting: boolean, attempt_number: number + disabled: boolean, isWaiting: boolean, attempt_number: number, willContinue: boolean } & React.ComponentPropsWithoutRef<'button'>) => ( ); @@ -93,9 +96,18 @@ export const ExerciseQuestion = React.forwardRef((props: ExerciseQuestionProps, is_completed, correct_answer_id, incorrectAnswerId, choicesEnabled, questionNumber, answer_id, hasMultipleAttempts, attempts_remaining, published_comments, detailedSolution, canAnswer, needsSaved, attempt_number, apiIsPending, onAnswerSave, onNextStep, canUpdateCurrentStep, - displaySolution, available_points, free_response, show_all_feedback, tableFeedbackEnabled + displaySolution, available_points, free_response, show_all_feedback, tableFeedbackEnabled, + hasFeedback } = props; + const [shouldContinue, setShouldContinue] = React.useState(false) + React.useEffect(() => { + if (shouldContinue && is_completed) { + onNextStep(questionNumber - 1); + setShouldContinue(false); + } + }, [onNextStep, questionNumber, shouldContinue, is_completed]); + return (
Detailed solution:
)}
- {canAnswer && needsSaved ? + {(canAnswer && needsSaved) || shouldContinue ? onAnswerSave(numberfyId(question.id))} + onClick={() => { + onAnswerSave(numberfyId(question.id)); + if (!hasFeedback) { + setShouldContinue(true); + } + }} + willContinue={!hasFeedback} /> : onNextStep(questionNumber - 1)} canUpdateCurrentStep={canUpdateCurrentStep} />}
diff --git a/src/components/__snapshots__/ExerciseQuestion.spec.tsx.snap b/src/components/__snapshots__/ExerciseQuestion.spec.tsx.snap index c1112bd..fde358f 100644 --- a/src/components/__snapshots__/ExerciseQuestion.spec.tsx.snap +++ b/src/components/__snapshots__/ExerciseQuestion.spec.tsx.snap @@ -470,6 +470,165 @@ exports[`ExerciseQuestion renders Save button 1`] = ` `; +exports[`ExerciseQuestion renders Submit & continue button 1`] = ` +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+ + Points: + 1.0 + + + +
+
+ +
+
+
+
+`; + exports[`ExerciseQuestion renders all attempts remaining 1`] = `