Skip to content

Commit c39841b

Browse files
HHHindawyv-i-s-h-n-u-psHossam Hindawy
authored
No ref/new prop to jump to question (#254)
* reset quiz context on props change * fix bug on re render * modify session storage to store multiple quiz data * revert unwanted changes * remove unused imports * update tests * fix test * added more getters * update text * removed unwanted changes * added a jump to question event * fix issue with jump logic * added tests for new hooks * added documentation * corrected jump logic, addressed comments * added tests * customise click event on single select question * Add disabled state + Disable button for invalid ids * Fix missing callbacks * Guard empty callbacks --------- Co-authored-by: vishnu <[email protected]> Co-authored-by: Hossam Hindawy <[email protected]>
1 parent 278a02f commit c39841b

26 files changed

+661
-65
lines changed

spec/components/CioQuiz/reducerTestCases.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,47 @@ export const apiReducerCases = [
9797
matchedOptions: [],
9898
},
9999
},
100+
{
101+
initialState: {
102+
...apiInitialState,
103+
quizRequestState: 'SUCCESS',
104+
quizResults: {
105+
...results,
106+
quiz_selected_options: [
107+
{
108+
attribute: null,
109+
has_attribute: false,
110+
id: 1,
111+
is_matched: true,
112+
value: 'VALUE',
113+
},
114+
{
115+
attribute: null,
116+
has_attribute: true,
117+
id: 4,
118+
is_matched: false,
119+
value: 'VALUE',
120+
},
121+
],
122+
},
123+
quizCurrentQuestion: undefined,
124+
selectedOptionsWithAttributes: ['VALUE'],
125+
matchedOptions: [],
126+
},
127+
action: {
128+
type: QuizAPIActionTypes.JUMP_TO_QUESTION,
129+
payload: {
130+
questionId: 2,
131+
},
132+
},
133+
expected: {
134+
...apiInitialState,
135+
quizRequestState: 'SUCCESS',
136+
quizResults: apiInitialState.quizResults,
137+
selectedOptionsWithAttributes: apiInitialState.selectedOptionsWithAttributes,
138+
matchedOptions: undefined,
139+
},
140+
},
100141
{
101142
initialState: apiInitialState,
102143
action: { type: 'unknown' },
@@ -330,6 +371,62 @@ export const localReducerCases = [
330371
},
331372
},
332373
},
374+
{
375+
initialState: {
376+
...localInitialState,
377+
answerInputs: {
378+
'1': {
379+
type: QuestionTypes.SingleSelect,
380+
value: [{ id: 1 }],
381+
},
382+
'2': {
383+
type: QuestionTypes.OpenText,
384+
value: 'true',
385+
},
386+
'3': {
387+
type: QuestionTypes.SingleSelect,
388+
value: [{ id: 1 }],
389+
},
390+
},
391+
prevAnswerInputs: {
392+
'1': {
393+
type: QuestionTypes.SingleSelect,
394+
value: [{ id: 1 }],
395+
},
396+
'2': {
397+
type: QuestionTypes.OpenText,
398+
value: 'true',
399+
},
400+
'3': {
401+
type: QuestionTypes.SingleSelect,
402+
value: [{ id: 1 }],
403+
},
404+
},
405+
answers: [[1], ['true'], [1]],
406+
isQuizCompleted: true,
407+
},
408+
action: {
409+
type: QuestionTypes.JumpToQuestion,
410+
payload: { questionId: 2 },
411+
},
412+
expected: {
413+
...localInitialState,
414+
isQuizCompleted: false,
415+
answers: [[1]],
416+
answerInputs: {
417+
'1': {
418+
type: QuestionTypes.SingleSelect,
419+
value: [{ id: 1 }],
420+
},
421+
},
422+
prevAnswerInputs: {
423+
'1': {
424+
type: QuestionTypes.SingleSelect,
425+
value: [{ id: 1 }],
426+
},
427+
},
428+
},
429+
},
333430
{
334431
initialState: {
335432
...localInitialState,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { renderHookServerSide } from '../../../__tests__/utils.server';
2+
import useJumpToQuestionButtonProps from '../../../../src/hooks/usePropsGetters/useJumpToQuestionButtonProps';
3+
import { QuizAPIReducerState } from '../../../../src/components/CioQuiz/quizApiReducer';
4+
import { QuizLocalReducerState } from '../../../../src/components/CioQuiz/quizLocalReducer';
5+
6+
describe('Testing Hook (server): useJumpToQuestionButtonProps', () => {
7+
it('initializes without errors and returns a function that provides button props', () => {
8+
const jumpToQuestionMock = jest.fn();
9+
const quizApiStateMock = {
10+
quizCurrentQuestion: {
11+
next_question: { id: '2' },
12+
isCoverQuestion: false,
13+
},
14+
} as unknown as QuizAPIReducerState;
15+
16+
const quizLocalStateMock = {
17+
prevAnswerInputs: {
18+
1: { type: 'open', value: '' },
19+
},
20+
} as unknown as QuizLocalReducerState;
21+
22+
const { result } = renderHookServerSide(
23+
() => useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiStateMock, quizLocalStateMock),
24+
{
25+
initialProps: {},
26+
}
27+
);
28+
29+
expect(typeof result).toBe('function');
30+
31+
const buttonProps = result();
32+
expect(buttonProps).toHaveProperty('className');
33+
expect(buttonProps).toHaveProperty('type');
34+
expect(buttonProps).toHaveProperty('onClick');
35+
expect(typeof buttonProps.onClick).toBe('function');
36+
});
37+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { renderHook, act } from '@testing-library/react';
2+
import useJumpToQuestionButtonProps from '../../../../src/hooks/usePropsGetters/useJumpToQuestionButtonProps';
3+
import { QuizAPIReducerState } from '../../../../src/components/CioQuiz/quizApiReducer';
4+
import { QuizLocalReducerState } from '../../../../src/components/CioQuiz/quizLocalReducer';
5+
6+
describe('Testing Hook (client): useJumpToQuestionButtonProps', () => {
7+
const mockEvent = { preventDefault: jest.fn() } as unknown as React.MouseEvent<HTMLElement>;
8+
const jumpToQuestionMock = jest.fn();
9+
10+
it('returns button props with disabled class if no answer is provided', () => {
11+
const quizApiState = {
12+
quizCurrentQuestion: {
13+
next_question: { id: '2' },
14+
isCoverQuestion: false,
15+
},
16+
} as unknown as QuizAPIReducerState;
17+
18+
const quizLocalStateMock = {
19+
prevAnswerInputs: {
20+
1: { type: 'open', value: '' },
21+
},
22+
} as unknown as QuizLocalReducerState;
23+
24+
const { result } = renderHook(() =>
25+
useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState, quizLocalStateMock)
26+
);
27+
28+
expect(result.current(2).className).toContain('disabled');
29+
});
30+
31+
it('returns button props with disabled class if an invalid question ID is provided', () => {
32+
const quizApiState = {
33+
quizCurrentQuestion: {
34+
next_question: { id: '2' },
35+
isCoverQuestion: false,
36+
},
37+
} as unknown as QuizAPIReducerState;
38+
39+
const quizLocalStateMock = {
40+
prevAnswerInputs: {
41+
1: { type: 'open', value: '' },
42+
},
43+
} as unknown as QuizLocalReducerState;
44+
45+
const { result } = renderHook(() =>
46+
useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState, quizLocalStateMock)
47+
);
48+
49+
expect(result.current(-1).className).toContain('disabled');
50+
});
51+
52+
it('returns button props without disabled class if an answer is provided', () => {
53+
const quizApiState = {
54+
quizCurrentQuestion: {
55+
next_question: { id: '3' },
56+
isCoverQuestion: false,
57+
},
58+
} as unknown as QuizAPIReducerState;
59+
60+
const quizLocalStateMock = {
61+
prevAnswerInputs: {
62+
1: { type: 'open', value: '' },
63+
2: { type: 'open', value: '' },
64+
},
65+
} as unknown as QuizLocalReducerState;
66+
67+
const { result } = renderHook(() =>
68+
useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState, quizLocalStateMock)
69+
);
70+
71+
expect(result.current(1).className).not.toContain('disabled');
72+
});
73+
74+
it('calls jumpToQuestion function when button is clicked', () => {
75+
const quizApiState = {
76+
quizCurrentQuestion: {
77+
next_question: { id: '3' },
78+
isCoverQuestion: false,
79+
},
80+
} as unknown as QuizAPIReducerState;
81+
82+
const quizLocalStateMock = {
83+
prevAnswerInputs: {
84+
1: { type: 'open', value: '' },
85+
2: { type: 'open', value: '' },
86+
},
87+
} as unknown as QuizLocalReducerState;
88+
89+
const { result } = renderHook(() =>
90+
useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState, quizLocalStateMock)
91+
);
92+
93+
expect(typeof result.current(1).onClick).toBe('function');
94+
95+
act(() => {
96+
result.current(1).onClick(mockEvent);
97+
});
98+
99+
expect(jumpToQuestionMock).toHaveBeenCalled();
100+
});
101+
});

spec/hooks/usePropsGetters/useSelectInputProps/useSelectInputProps.test.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ describe('Testing Hook (client): useSelectInputProps', () => {
2424
});
2525

2626
const setupHook = (questionData) =>
27-
renderHook(() => useSelectInputProps(quizAnswerChangedMock, nextQuestionMock, questionData));
27+
renderHook(() =>
28+
useSelectInputProps(quizAnswerChangedMock, nextQuestionMock, questionData, answerInputs)
29+
);
2830

2931
it('correctly toggles selected class on click', () => {
3032
const { result } = setupHook(currentQuestionData);
@@ -80,6 +82,25 @@ describe('Testing Hook (client): useSelectInputProps', () => {
8082
expect(nextQuestionMock).not.toHaveBeenCalled();
8183
});
8284

85+
it('does not advance when configured not to', () => {
86+
currentQuestionData.type = 'single';
87+
const { result } = renderHook(() =>
88+
useSelectInputProps(
89+
quizAnswerChangedMock,
90+
nextQuestionMock,
91+
currentQuestionData,
92+
answerInputs,
93+
false
94+
)
95+
);
96+
97+
act(() => {
98+
result.current(currentQuestionData.options[0]).onClick(mockEvent);
99+
});
100+
expect(result.current(currentQuestionData.options[0]).className).toContain('selected');
101+
expect(nextQuestionMock).not.toHaveBeenCalled();
102+
});
103+
83104
it('allows toggling options off in a multiple select question', () => {
84105
currentQuestionData.type = 'multiple';
85106
const { result } = setupHook(currentQuestionData);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { renderHookServerSide } from '../../../__tests__/utils.server';
2+
import useJumpToQuestion from '../../../../src/hooks/useQuizEvents/useJumpToQuestion';
3+
import { QuizAPIReducerState } from '../../../../src/components/CioQuiz/quizApiReducer';
4+
5+
describe('Testing Hook (server): useJumpToQuestion', () => {
6+
it('initializes without throwing errors', async () => {
7+
const dispatchLocalStateMock = jest.fn();
8+
const dispatchApiStateMock = jest.fn();
9+
const quizApiStateMock: QuizAPIReducerState = {
10+
quizCurrentQuestion: {
11+
id: '1',
12+
next_question: {
13+
id: '2',
14+
type: 'singleChoice',
15+
},
16+
},
17+
} as unknown as QuizAPIReducerState;
18+
19+
const executeHook = () =>
20+
renderHookServerSide(
21+
() =>
22+
useJumpToQuestion({
23+
quizApiState: quizApiStateMock,
24+
dispatchLocalState: dispatchLocalStateMock,
25+
dispatchApiState: dispatchApiStateMock,
26+
}),
27+
{
28+
initialProps: {},
29+
}
30+
);
31+
32+
expect(executeHook).not.toThrow();
33+
});
34+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { renderHook, act } from '@testing-library/react';
2+
3+
import useJumpToQuestion from '../../../../src/hooks/useQuizEvents/useJumpToQuestion';
4+
import { QuizAPIReducerState } from '../../../../src/components/CioQuiz/quizApiReducer';
5+
import { QuestionTypes, QuizAPIActionTypes } from '../../../../src/components/CioQuiz/actions';
6+
7+
describe('Testing Hook (client): useJumpToQuestion', () => {
8+
const dispatchLocalStateMock = jest.fn();
9+
const dispatchApiStateMock = jest.fn();
10+
const quizApiStateMock = {
11+
quizCurrentQuestion: {
12+
id: 2,
13+
next_question: {
14+
id: 3,
15+
type: 'singleChoice',
16+
},
17+
},
18+
} as unknown as QuizAPIReducerState;
19+
20+
beforeEach(() => {
21+
jest.clearAllMocks();
22+
});
23+
24+
it('does not call dispatchLocalState and dispatchApiState', () => {
25+
const { result } = renderHook(() =>
26+
useJumpToQuestion({
27+
quizApiState: quizApiStateMock,
28+
dispatchLocalState: dispatchLocalStateMock,
29+
dispatchApiState: dispatchApiStateMock,
30+
})
31+
);
32+
33+
act(() => {
34+
result.current(2);
35+
});
36+
37+
expect(dispatchLocalStateMock).not.toHaveBeenCalled();
38+
expect(dispatchApiStateMock).not.toHaveBeenCalled();
39+
});
40+
41+
it('calls dispatchLocalState and dispatchApiState correctly', () => {
42+
const { result } = renderHook(() =>
43+
useJumpToQuestion({
44+
quizApiState: quizApiStateMock,
45+
dispatchLocalState: dispatchLocalStateMock,
46+
dispatchApiState: dispatchApiStateMock,
47+
})
48+
);
49+
50+
act(() => {
51+
result.current(1);
52+
});
53+
54+
expect(dispatchLocalStateMock).toHaveBeenCalledWith({
55+
type: QuestionTypes.JumpToQuestion,
56+
payload: { questionId: 1 },
57+
});
58+
59+
expect(dispatchApiStateMock).toHaveBeenCalledWith({
60+
type: QuizAPIActionTypes.JUMP_TO_QUESTION,
61+
payload: { questionId: 1 },
62+
});
63+
});
64+
});

0 commit comments

Comments
 (0)