Skip to content

Commit dcea20d

Browse files
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. [CORE-173]
1 parent 95c954b commit dcea20d

File tree

7 files changed

+284
-8
lines changed

7 files changed

+284
-8
lines changed

src/components/Exercise.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ describe('Exercise', () => {
142142
numberOfQuestions: 1,
143143
scrollToQuestion: 1,
144144
hasMultipleAttempts: false,
145+
hasFeedback: true,
145146
onAnswerChange: () => null,
146147
onAnswerSave: () => null,
147148
onNextStep: () => null,

src/components/Exercise.stories.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ const exerciseWithQuestionStatesProps = (): ExerciseWithQuestionStatesProps => {
130130
apiIsPending: false
131131
}
132132
},
133+
hasFeedback: true,
133134
}};
134135

135136
type TextResizerValue = -2 | -1 | 0 | 1 | 2 | 3;
@@ -181,6 +182,34 @@ export const Default = () => {
181182
)
182183
};
183184

185+
export const DefaultWithoutFeedback = () => {
186+
const [selectedAnswerId, setSelectedAnswerId] = useState<number>(0);
187+
const [apiIsPending, setApiIsPending] = useState(false)
188+
const [isCompleted, setIsCompleted] = useState(false)
189+
const props = exerciseWithQuestionStatesProps();
190+
props.hasFeedback = false;
191+
props.questionStates['1'].answer_id = selectedAnswerId;
192+
props.questionStates['1'].apiIsPending = apiIsPending;
193+
props.questionStates['1'].is_completed = isCompleted;
194+
props.questionStates['1'].canAnswer = !isCompleted;
195+
return (
196+
<Exercise
197+
{...props}
198+
onAnswerChange={(a: Omit<Answer, 'id'> & { id: number, question_id: number }) => {
199+
setSelectedAnswerId(a.id)
200+
}}
201+
onAnswerSave={() => {
202+
setApiIsPending(true);
203+
setTimeout(() => {
204+
setApiIsPending(false)
205+
setIsCompleted(true)
206+
}, 1000)
207+
}}
208+
onNextStep={(idx) => console.log(`Next step: ${idx}`)}
209+
/>
210+
)
211+
};
212+
184213
export const DeprecatedStepData = () => <Exercise {...exerciseWithStepDataProps} />;
185214

186215
export const CompleteWithFeedback = () => {
@@ -210,6 +239,34 @@ export const CompleteWithFeedback = () => {
210239
return <TextResizerProvider><Exercise {...props} /></TextResizerProvider>;
211240
};
212241

242+
export const CompleteWithoutFeedback = () => {
243+
const props: ExerciseWithQuestionStatesProps = {
244+
...exerciseWithQuestionStatesProps(),
245+
246+
questionStates: {
247+
'1': {
248+
available_points: '1.0',
249+
is_completed: true,
250+
answer_id_order: ['1', '2'],
251+
answer_id: 1,
252+
free_response: 'Free response',
253+
feedback_html: '',
254+
correct_answer_id: '',
255+
correct_answer_feedback_html: '',
256+
attempts_remaining: 0,
257+
attempt_number: 1,
258+
incorrectAnswerId: 0,
259+
canAnswer: false,
260+
needsSaved: false,
261+
apiIsPending: false
262+
}
263+
},
264+
hasFeedback: false,
265+
};
266+
267+
return <TextResizerProvider><Exercise {...props} /></TextResizerProvider>;
268+
};
269+
213270
export const IncorrectWithFeedbackAndSolution = () => {
214271
const props: ExerciseWithQuestionStatesProps = { ...exerciseWithQuestionStatesProps() };
215272
props.questionStates = {

src/components/Exercise.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export interface ExerciseBaseProps {
138138
* - A topic icon linking to the relevant textbook location
139139
*/
140140
exerciseIcons?: ExerciseIcons;
141+
hasFeedback?: boolean;
141142
}
142143

143144
export interface ExerciseWithStepDataProps extends ExerciseBaseProps {

src/components/ExerciseQuestion.spec.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe('ExerciseQuestion', () => {
5050
displaySolution: false,
5151
available_points: '1.0',
5252
exercise_uid: '',
53+
hasFeedback: true,
5354
}
5455
});
5556

@@ -125,6 +126,21 @@ describe('ExerciseQuestion', () => {
125126
expect(tree).toMatchSnapshot();
126127
});
127128

129+
it('renders Submit & continue button', () => {
130+
const tree = renderer.create(
131+
<ExerciseQuestion {...props}
132+
choicesEnabled={true}
133+
incorrectAnswerId='2'
134+
canAnswer={true}
135+
needsSaved={true}
136+
answer_id='1'
137+
canUpdateCurrentStep={false}
138+
hasFeedback={false}
139+
/>
140+
).toJSON();
141+
expect(tree).toMatchSnapshot();
142+
});
143+
128144
it('renders continue button (unused?)', () => {
129145
const tree = renderer.create(
130146
<ExerciseQuestion {...props}
@@ -197,4 +213,27 @@ describe('ExerciseQuestion', () => {
197213

198214
expect(mockFn).toHaveBeenCalledWith(0);
199215
});
216+
217+
it('passes question index on submit button click when there is not feedback', () => {
218+
const mockFn = jest.fn();
219+
220+
// This combination of props should never happen: `is_completed` should not
221+
// be true at the same time as `needsSaved`. This combination allows the
222+
// test to work correctly without simulating waiting for api calls
223+
const tree = renderer.create(
224+
<ExerciseQuestion
225+
{...props}
226+
needsSaved={true}
227+
canAnswer={true}
228+
hasFeedback={false}
229+
is_completed={true}
230+
onNextStep={mockFn}
231+
/>
232+
);
233+
renderer.act(() => {
234+
tree.root.findByType(SaveButton).props.onClick();
235+
});
236+
237+
expect(mockFn).toHaveBeenCalledWith(0);
238+
});
200239
});

src/components/ExerciseQuestion.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ const props = {
5151
needsSaved: true,
5252
canUpdateCurrentStep: true,
5353
attempt_number: 0,
54-
apiIsPending: false
54+
apiIsPending: false,
55+
hasFeedback: false
5556
};
5657

5758
export const Default = () => <ExerciseQuestion {...props} />;

src/components/ExerciseQuestion.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface ExerciseQuestionProps {
3737
free_response?: string;
3838
show_all_feedback?: boolean;
3939
tableFeedbackEnabled?: boolean;
40+
hasFeedback?: ExerciseBaseProps['hasFeedback'];
4041
}
4142

4243
const AttemptsRemaining = ({ count }: { count: number }) => {
@@ -56,15 +57,17 @@ const PublishedComments = ({ published_comments }: { published_comments?: string
5657
}
5758

5859
export const SaveButton = (props: {
59-
disabled: boolean, isWaiting: boolean, attempt_number: number
60+
disabled: boolean, isWaiting: boolean, attempt_number: number, willContinue: boolean
6061
} & React.ComponentPropsWithoutRef<'button'>) => (
6162
<Button
6263
{...props}
6364
waitingText="Saving…"
6465
isWaiting={props.isWaiting}
6566
data-test-id="submit-answer-btn"
6667
>
67-
{props.attempt_number == 0 ? 'Submit' : 'Re-submit'}
68+
{props.willContinue
69+
? 'Submit & continue'
70+
: (props.attempt_number == 0 ? 'Submit' : 'Re-submit')}
6871
</Button>
6972
);
7073

@@ -93,9 +96,18 @@ export const ExerciseQuestion = React.forwardRef((props: ExerciseQuestionProps,
9396
is_completed, correct_answer_id, incorrectAnswerId, choicesEnabled, questionNumber,
9497
answer_id, hasMultipleAttempts, attempts_remaining, published_comments, detailedSolution,
9598
canAnswer, needsSaved, attempt_number, apiIsPending, onAnswerSave, onNextStep, canUpdateCurrentStep,
96-
displaySolution, available_points, free_response, show_all_feedback, tableFeedbackEnabled
99+
displaySolution, available_points, free_response, show_all_feedback, tableFeedbackEnabled,
100+
hasFeedback
97101
} = props;
98102

103+
const [shouldContinue, setShouldContinue] = React.useState(false)
104+
React.useEffect(() => {
105+
if (shouldContinue && is_completed) {
106+
onNextStep(questionNumber - 1);
107+
setShouldContinue(false);
108+
}
109+
}, [onNextStep, questionNumber, shouldContinue, is_completed]);
110+
99111
return (
100112
<div data-test-id="student-exercise-question">
101113
<Question
@@ -133,12 +145,18 @@ export const ExerciseQuestion = React.forwardRef((props: ExerciseQuestionProps,
133145
{detailedSolution && (<div><strong>Detailed solution:</strong> <Content html={detailedSolution} /></div>)}
134146
</div>
135147
<div className="controls">
136-
{canAnswer && needsSaved ?
148+
{(canAnswer && needsSaved) || shouldContinue ?
137149
<SaveButton
138-
disabled={apiIsPending || !answer_id}
139-
isWaiting={apiIsPending}
150+
disabled={apiIsPending || !answer_id || shouldContinue}
151+
isWaiting={apiIsPending || shouldContinue}
140152
attempt_number={attempt_number}
141-
onClick={() => onAnswerSave(numberfyId(question.id))}
153+
onClick={() => {
154+
onAnswerSave(numberfyId(question.id));
155+
if (!hasFeedback) {
156+
setShouldContinue(true);
157+
}
158+
}}
159+
willContinue={!hasFeedback}
142160
/> :
143161
<NextButton onClick={() => onNextStep(questionNumber - 1)} canUpdateCurrentStep={canUpdateCurrentStep} />}
144162
</div>

src/components/__snapshots__/ExerciseQuestion.spec.tsx.snap

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,165 @@ exports[`ExerciseQuestion renders Save button 1`] = `
470470
</div>
471471
`;
472472

473+
exports[`ExerciseQuestion renders Submit & continue button 1`] = `
474+
<div
475+
data-test-id="student-exercise-question"
476+
>
477+
<div
478+
className="sc-dkzDqf cYzaBK openstax-question step-card-body has-incorrect-answer"
479+
data-question-number={1}
480+
data-test-id="question"
481+
>
482+
<div
483+
className="question-stem"
484+
dangerouslySetInnerHTML={
485+
Object {
486+
"__html": "Is this a question?",
487+
}
488+
}
489+
data-question-number={1}
490+
/>
491+
<div
492+
className="answers-table"
493+
>
494+
<div
495+
className="openstax-answer"
496+
>
497+
<section
498+
className="answers-answer answer-selected"
499+
>
500+
<input
501+
checked={true}
502+
className="answer-input-box"
503+
disabled={false}
504+
id="1-option-0"
505+
name="1-options"
506+
onChange={[Function]}
507+
type="radio"
508+
/>
509+
<label
510+
className="answer-label"
511+
htmlFor="1-option-0"
512+
>
513+
<span
514+
className="answer-letter-wrapper"
515+
>
516+
<button
517+
aria-label="Selected Choice A:"
518+
className="answer-letter"
519+
data-test-id="answer-choice-A"
520+
disabled={false}
521+
onClick={[Function]}
522+
>
523+
A
524+
</button>
525+
</span>
526+
<div
527+
className="answer-answer"
528+
>
529+
<span
530+
className="answer-content"
531+
dangerouslySetInnerHTML={
532+
Object {
533+
"__html": "True",
534+
}
535+
}
536+
/>
537+
</div>
538+
</label>
539+
</section>
540+
</div>
541+
<div
542+
className="openstax-answer"
543+
>
544+
<section
545+
className="answers-answer answer-incorrect"
546+
>
547+
<input
548+
checked={false}
549+
className="answer-input-box"
550+
disabled={false}
551+
id="1-option-1"
552+
name="1-options"
553+
onChange={[Function]}
554+
type="radio"
555+
/>
556+
<label
557+
className="answer-label"
558+
htmlFor="1-option-1"
559+
>
560+
<span
561+
className="answer-letter-wrapper"
562+
>
563+
<button
564+
aria-label="Choice B:"
565+
className="answer-letter"
566+
data-test-id="answer-choice-B"
567+
disabled={true}
568+
onClick={[Function]}
569+
>
570+
B
571+
</button>
572+
</span>
573+
<div
574+
className="answer-answer"
575+
>
576+
<div
577+
className="sc-gsnTZi jsjSLp"
578+
>
579+
Incorrect
580+
Answer
581+
</div>
582+
<span
583+
className="answer-content"
584+
dangerouslySetInnerHTML={
585+
Object {
586+
"__html": "False",
587+
}
588+
}
589+
/>
590+
</div>
591+
</label>
592+
</section>
593+
</div>
594+
</div>
595+
</div>
596+
<div
597+
className="sc-hKMtZM eTQwo step-card-footer"
598+
>
599+
<div
600+
className="step-card-footer-inner"
601+
>
602+
<div
603+
className="points"
604+
role="status"
605+
>
606+
<strong>
607+
Points:
608+
1.0
609+
</strong>
610+
<span
611+
className="attempts-left"
612+
/>
613+
614+
</div>
615+
<div
616+
className="controls"
617+
>
618+
<button
619+
className="sc-bczRLJ fLTvYy"
620+
data-test-id="submit-answer-btn"
621+
disabled={false}
622+
onClick={[Function]}
623+
>
624+
Submit & continue
625+
</button>
626+
</div>
627+
</div>
628+
</div>
629+
</div>
630+
`;
631+
473632
exports[`ExerciseQuestion renders all attempts remaining 1`] = `
474633
<div
475634
data-test-id="student-exercise-question"

0 commit comments

Comments
 (0)