From dd5ccc0f6e4f3ab9fa062a585483aa2d6f264adc Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 7 Nov 2024 15:45:58 +0530 Subject: [PATCH 01/20] reset quiz context on props change --- spec/hooks/usePrevious/usePrevious.test.tsx | 6 ++++-- src/hooks/usePrevious.ts | 23 +++++++++------------ src/hooks/useQuiz.ts | 2 +- src/hooks/useQuizState/useQuizApiState.ts | 6 ++++++ 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/spec/hooks/usePrevious/usePrevious.test.tsx b/spec/hooks/usePrevious/usePrevious.test.tsx index 9c2cc594..a0ceb95c 100644 --- a/spec/hooks/usePrevious/usePrevious.test.tsx +++ b/spec/hooks/usePrevious/usePrevious.test.tsx @@ -25,11 +25,13 @@ describe(`${usePrevious.name}`, () => { expect(container).toHaveTextContent('prevX=10'); }); - it('does not change the previous value if rendered again with the same value', () => { + it('changes the previous value if rendered again with the same value', () => { const { container, rerender } = render(); rerender(); - rerender(); expect(container).toHaveTextContent('x=11'); expect(container).toHaveTextContent('prevX=10'); + rerender(); + expect(container).toHaveTextContent('x=11'); + expect(container).toHaveTextContent('prevX=11'); }); }); diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts index 97e75a95..f834f0fa 100644 --- a/src/hooks/usePrevious.ts +++ b/src/hooks/usePrevious.ts @@ -1,14 +1,11 @@ -import { useRef } from 'react'; - -export default function usePrevious(value: Type): Type | undefined { - const latest = useRef(value); - const previous = useRef(); - - if (latest.current !== value) { - previous.current = latest.current; - } - - latest.current = value; - - return previous.current; +import { useEffect, useRef } from 'react'; + +function usePrevious(value: T) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; } + +export default usePrevious; diff --git a/src/hooks/useQuiz.ts b/src/hooks/useQuiz.ts index 35ae2fe4..1da8b0de 100644 --- a/src/hooks/useQuiz.ts +++ b/src/hooks/useQuiz.ts @@ -50,13 +50,13 @@ const useQuiz: UseQuiz = (quizOptions) => { useEffect(() => { if (quizId === prevQuizId) return; if (!prevQuizId) return; + resetQuizSessionStorageState(quizSessionStorageState.key); dispatchLocalState({ type: QuestionTypes.Reset, }); dispatchApiState({ type: QuizAPIActionTypes.RESET_QUIZ, }); - resetQuizSessionStorageState(quizSessionStorageState.key); // eslint-disable-next-line react-hooks/exhaustive-deps }, [quizId, prevQuizId]); diff --git a/src/hooks/useQuizState/useQuizApiState.ts b/src/hooks/useQuizState/useQuizApiState.ts index 0ab152a8..6d6ebff8 100644 --- a/src/hooks/useQuizState/useQuizApiState.ts +++ b/src/hooks/useQuizState/useQuizApiState.ts @@ -19,6 +19,7 @@ import { } from '../../services'; import { IQuizProps } from '../../types'; import useQueryParams from '../useQueryParams'; +import usePrevious from '../usePrevious'; type UseQuizApiState = ( quizOptions: IQuizProps, @@ -103,8 +104,13 @@ const useQuizApiState: UseQuizApiState = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [quizApiState.resultsConfig]); + const prevQuizId = usePrevious(quizId); + useEffect(() => { (async () => { + // If quizId is the same as the previous quizId, wait for next render after reset + if (!!prevQuizId && quizId !== prevQuizId) return; + dispatchApiState({ type: QuizAPIActionTypes.SET_IS_LOADING, }); From 1db8d1f28abcce24ec4357c161169681de6e7e1b Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 7 Nov 2024 22:46:43 +0530 Subject: [PATCH 02/20] fix bug on re render --- src/hooks/useQuiz.ts | 2 +- src/hooks/useQuizState/useQuizApiState.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useQuiz.ts b/src/hooks/useQuiz.ts index 1da8b0de..b49ef49e 100644 --- a/src/hooks/useQuiz.ts +++ b/src/hooks/useQuiz.ts @@ -50,7 +50,7 @@ const useQuiz: UseQuiz = (quizOptions) => { useEffect(() => { if (quizId === prevQuizId) return; if (!prevQuizId) return; - resetQuizSessionStorageState(quizSessionStorageState.key); + resetQuizSessionStorageState(quizSessionStorageState.key)(); dispatchLocalState({ type: QuestionTypes.Reset, }); diff --git a/src/hooks/useQuizState/useQuizApiState.ts b/src/hooks/useQuizState/useQuizApiState.ts index 6d6ebff8..0869e479 100644 --- a/src/hooks/useQuizState/useQuizApiState.ts +++ b/src/hooks/useQuizState/useQuizApiState.ts @@ -165,6 +165,7 @@ const useQuizApiState: UseQuizApiState = ( }, [ cioClient, quizId, + prevQuizId, quizLocalState.answers, resultsPageOptions?.numResultsToDisplay, isSharedResultsQuery, From 1ee8c117325c5dc5792c9ab33921b94819fc6d7a Mon Sep 17 00:00:00 2001 From: vishnu Date: Fri, 8 Nov 2024 00:07:39 +0530 Subject: [PATCH 03/20] modify session storage to store multiple quiz data --- src/hooks/useQuiz.ts | 20 +++++++++---- src/hooks/useQuizEvents/index.ts | 10 +++++-- .../useQuizEvents/useHydrateQuizLocalState.ts | 8 +++-- src/hooks/useQuizState/index.ts | 1 + .../useQuizState/useSessionStorageState.ts | 21 +++++++++++--- .../Component/ComponentStories.stories.tsx | 29 +++++++++++++++++++ src/utils.tsx | 14 +++++---- 7 files changed, 83 insertions(+), 20 deletions(-) diff --git a/src/hooks/useQuiz.ts b/src/hooks/useQuiz.ts index b49ef49e..739e7990 100644 --- a/src/hooks/useQuiz.ts +++ b/src/hooks/useQuiz.ts @@ -8,7 +8,7 @@ import useQuizEvents from './useQuizEvents'; import useQuizState from './useQuizState'; import usePrevious from './usePrevious'; import { QuestionTypes, QuizAPIActionTypes } from '../components/CioQuiz/actions'; -import { resetQuizSessionStorageState } from '../utils'; +import { getStateFromSessionStorage, resetQuizSessionStorageState } from '../utils'; const useQuiz: UseQuiz = (quizOptions) => { const { apiKey, cioJsClient, primaryColor, resultsPageOptions } = quizOptions; @@ -40,7 +40,7 @@ const useQuiz: UseQuiz = (quizOptions) => { useEffect(() => { if (skipToResults) quizEvents.hydrateQuiz(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [quizOptions.quizId]); const { quizId } = quizOptions; const { dispatchApiState, dispatchLocalState } = quizState; @@ -50,10 +50,18 @@ const useQuiz: UseQuiz = (quizOptions) => { useEffect(() => { if (quizId === prevQuizId) return; if (!prevQuizId) return; - resetQuizSessionStorageState(quizSessionStorageState.key)(); - dispatchLocalState({ - type: QuestionTypes.Reset, - }); + + const quizData = getStateFromSessionStorage(quizSessionStorageState.key); + if (quizData && quizData[quizId]) { + dispatchLocalState({ + type: QuestionTypes.Hydrate, + payload: quizData[quizId], + }); + } else { + dispatchLocalState({ + type: QuestionTypes.Reset, + }); + } dispatchApiState({ type: QuizAPIActionTypes.RESET_QUIZ, }); diff --git a/src/hooks/useQuizEvents/index.ts b/src/hooks/useQuizEvents/index.ts index f796fc23..014c5182 100644 --- a/src/hooks/useQuizEvents/index.ts +++ b/src/hooks/useQuizEvents/index.ts @@ -69,9 +69,14 @@ const useQuizEvents: UseQuizEvents = (quizOptions, cioClient, quizState) => { // Quiz results loaded event useQuizResultsLoaded(cioClient, quizApiState, onQuizResultsLoaded); + const resetSessionStorageState = resetQuizSessionStorageState( + quizSessionStorageState.key, + quizOptions.quizId + ); + // Quiz reset const resetQuiz = useQuizResetClick({ - resetQuizSessionStorageState: resetQuizSessionStorageState(quizSessionStorageState.key), + resetQuizSessionStorageState: resetSessionStorageState, dispatchLocalState, dispatchApiState, quizResults: quizApiState.quizResults, @@ -79,6 +84,7 @@ const useQuizEvents: UseQuizEvents = (quizOptions, cioClient, quizState) => { // Quiz rehydrate const hydrateQuizLocalState = useHydrateQuizLocalState( + quizOptions.quizId, quizSessionStorageState.key, dispatchLocalState ); @@ -93,7 +99,7 @@ const useQuizEvents: UseQuizEvents = (quizOptions, cioClient, quizState) => { skipQuestion, resetQuiz, hydrateQuiz: hydrateQuizLocalState, - resetSessionStorageState: resetQuizSessionStorageState(quizSessionStorageState.key), + resetSessionStorageState, }; }; diff --git a/src/hooks/useQuizEvents/useHydrateQuizLocalState.ts b/src/hooks/useQuizEvents/useHydrateQuizLocalState.ts index 580a1e25..53693336 100644 --- a/src/hooks/useQuizEvents/useHydrateQuizLocalState.ts +++ b/src/hooks/useQuizEvents/useHydrateQuizLocalState.ts @@ -4,18 +4,20 @@ import { QuizEventsReturn } from '../../types'; import { getStateFromSessionStorage } from '../../utils'; const useHydrateQuizLocalState = ( + quizId: string, quizSessionStorageStateKey: string, dispatchLocalState: React.Dispatch ): QuizEventsReturn.NextQuestion => { const sessionStorageQuizState = getStateFromSessionStorage(quizSessionStorageStateKey); const hydrateQuizLocalStateHandler = useCallback(() => { - if (sessionStorageQuizState) { + const quizData = sessionStorageQuizState?.[quizId]; + if (quizData) { dispatchLocalState({ type: QuestionTypes.Hydrate, - payload: sessionStorageQuizState, + payload: quizData, }); } - }, [dispatchLocalState, sessionStorageQuizState]); + }, [dispatchLocalState, quizId, sessionStorageQuizState]); return hydrateQuizLocalStateHandler; }; diff --git a/src/hooks/useQuizState/index.ts b/src/hooks/useQuizState/index.ts index 7250b379..49230edf 100644 --- a/src/hooks/useQuizState/index.ts +++ b/src/hooks/useQuizState/index.ts @@ -27,6 +27,7 @@ const useQuizState: UseQuizState = (quizOptions, cioClient) => { // Quiz Session Storage state const { skipToResults, quizSessionStorageStateKey, hasSessionStorageState } = useSessionStorageState( + quizOptions.quizId, quizLocalState, sessionStateOptions, enableHydration === undefined ? true : enableHydration diff --git a/src/hooks/useQuizState/useSessionStorageState.ts b/src/hooks/useQuizState/useSessionStorageState.ts index b674daa1..3b9129e8 100644 --- a/src/hooks/useQuizState/useSessionStorageState.ts +++ b/src/hooks/useQuizState/useSessionStorageState.ts @@ -3,30 +3,43 @@ import { getStateFromSessionStorage } from '../../utils'; import { SessionStateOptions } from '../../types'; import { QuizLocalReducerState } from '../../components/CioQuiz/quizLocalReducer'; import { quizSessionStateKey } from '../../constants'; +import usePrevious from '../usePrevious'; const useSessionStorageState = ( + quizId: string, quizLocalState?: QuizLocalReducerState, sessionStateOptions?: SessionStateOptions, enableHydration?: boolean ) => { const quizSessionStorageStateKey = sessionStateOptions?.sessionStateKey || quizSessionStateKey; + + const prevQuizId = usePrevious(quizId); + // Save state to session storage useEffect(() => { // don't save state if initial state + if (quizId !== prevQuizId) return; if (enableHydration && quizLocalState?.answers?.length) { - window?.sessionStorage?.setItem(quizSessionStorageStateKey, JSON.stringify(quizLocalState)); + const data = getStateFromSessionStorage(quizSessionStorageStateKey); + const dataToSave = { + ...data, + [quizId]: quizLocalState, + }; + window?.sessionStorage?.setItem(quizSessionStorageStateKey, JSON.stringify(dataToSave)); } - }, [quizLocalState, quizSessionStorageStateKey, enableHydration]); + }, [quizLocalState, quizSessionStorageStateKey, enableHydration, quizId, prevQuizId]); + + const quizData = getStateFromSessionStorage(quizSessionStorageStateKey); const skipToResults = !!enableHydration && - !!getStateFromSessionStorage(quizSessionStorageStateKey)?.isQuizCompleted && + !!quizData?.[quizId]?.isQuizCompleted && !sessionStateOptions?.showSessionModalOnResults; return { skipToResults, quizSessionStorageStateKey, - hasSessionStorageState: () => getStateFromSessionStorage(quizSessionStorageStateKey) !== null, + hasSessionStorageState: () => !!quizData && !!quizData[quizId], }; }; diff --git a/src/stories/Quiz/Component/ComponentStories.stories.tsx b/src/stories/Quiz/Component/ComponentStories.stories.tsx index 19562693..0769162f 100644 --- a/src/stories/Quiz/Component/ComponentStories.stories.tsx +++ b/src/stories/Quiz/Component/ComponentStories.stories.tsx @@ -40,14 +40,43 @@ export const BasicUsage: Story = { }, }; +const quizzes = [ + { + apiKey: 'key_1tigFZoUEs7Ygkww', + quizId: 'find-your-perfect-dining-room-set', + name: 'Dining', + }, + { + apiKey: 'key_1tigFZoUEs7Ygkww', + quizId: 'find-your-sofa-v4', + name: 'Sofa', + }, + { + apiKey: 'key_n4SkMH5PFWLdStQZ', + quizId: 'coffee-quiz', + name: 'Coffee', + }, +]; + function RenderInASmallContainerTemplate(args: IQuizProps) { const [favorites, setFavorites] = useState([]); + const [quiz, setQuiz] = useState(quizzes[0]); return (
+ {quizzes.map((q) => ( +
setQuiz(q)}> + + {q.name} +
+ ))} +
+
{quiz.name}
() => { - window?.sessionStorage?.removeItem(quizStateKey); +export const resetQuizSessionStorageState = (quizStateKey: string, quizId: string) => () => { + const quizData = getStateFromSessionStorage(quizStateKey); + if (quizData) { + const updatedData = { ...quizData, [quizId]: null }; + window?.sessionStorage?.setItem(quizStateKey, JSON.stringify(updatedData)); + } }; /* istanbul ignore next */ @@ -192,9 +196,9 @@ export function rgbToHsl(r: number, g: number, b: number) { export function convertPrimaryColorsToString(primaryColorStyles: PrimaryColorStyles) { return `{ - --primary-color-h: ${primaryColorStyles['--primary-color-h']}; - --primary-color-s: ${primaryColorStyles['--primary-color-s']}; - --primary-color-l: ${primaryColorStyles['--primary-color-l']}; + --primary-color-h: ${primaryColorStyles['--primary-color-h']}; + --primary-color-s: ${primaryColorStyles['--primary-color-s']}; + --primary-color-l: ${primaryColorStyles['--primary-color-l']}; }`; } From a12304529113474c419d043428aff627172a1ca5 Mon Sep 17 00:00:00 2001 From: vishnu Date: Fri, 8 Nov 2024 00:08:08 +0530 Subject: [PATCH 04/20] revert unwanted changes --- .../Component/ComponentStories.stories.tsx | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/src/stories/Quiz/Component/ComponentStories.stories.tsx b/src/stories/Quiz/Component/ComponentStories.stories.tsx index 0769162f..19562693 100644 --- a/src/stories/Quiz/Component/ComponentStories.stories.tsx +++ b/src/stories/Quiz/Component/ComponentStories.stories.tsx @@ -40,43 +40,14 @@ export const BasicUsage: Story = { }, }; -const quizzes = [ - { - apiKey: 'key_1tigFZoUEs7Ygkww', - quizId: 'find-your-perfect-dining-room-set', - name: 'Dining', - }, - { - apiKey: 'key_1tigFZoUEs7Ygkww', - quizId: 'find-your-sofa-v4', - name: 'Sofa', - }, - { - apiKey: 'key_n4SkMH5PFWLdStQZ', - quizId: 'coffee-quiz', - name: 'Coffee', - }, -]; - function RenderInASmallContainerTemplate(args: IQuizProps) { const [favorites, setFavorites] = useState([]); - const [quiz, setQuiz] = useState(quizzes[0]); return (
- {quizzes.map((q) => ( -
setQuiz(q)}> - - {q.name} -
- ))} -
-
{quiz.name}
Date: Fri, 8 Nov 2024 00:08:44 +0530 Subject: [PATCH 05/20] remove unused imports --- src/hooks/useQuiz.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useQuiz.ts b/src/hooks/useQuiz.ts index 739e7990..0f383ffe 100644 --- a/src/hooks/useQuiz.ts +++ b/src/hooks/useQuiz.ts @@ -8,7 +8,7 @@ import useQuizEvents from './useQuizEvents'; import useQuizState from './useQuizState'; import usePrevious from './usePrevious'; import { QuestionTypes, QuizAPIActionTypes } from '../components/CioQuiz/actions'; -import { getStateFromSessionStorage, resetQuizSessionStorageState } from '../utils'; +import { getStateFromSessionStorage } from '../utils'; const useQuiz: UseQuiz = (quizOptions) => { const { apiKey, cioJsClient, primaryColor, resultsPageOptions } = quizOptions; From 414255e9f1a5c6275177744470407545ddd0cf00 Mon Sep 17 00:00:00 2001 From: vishnu Date: Fri, 8 Nov 2024 00:42:45 +0530 Subject: [PATCH 06/20] update tests --- .../useHydrateQuizLocalState.server.test.tsx | 3 +- .../useHydrateQuizLocalState.test.tsx | 2 +- .../useQuizApiState/useQuizApiState.test.tsx | 1 + .../useSessionStorageState.server.test.tsx | 7 ++-- .../useSessionStorageState.test.tsx | 23 ++++++++++--- spec/utils/utils.test.ts | 34 +++++++++++++++---- .../useQuizState/useSessionStorageState.ts | 2 +- 7 files changed, 57 insertions(+), 15 deletions(-) diff --git a/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.server.test.tsx b/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.server.test.tsx index 2727007e..bd4d2050 100644 --- a/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.server.test.tsx +++ b/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.server.test.tsx @@ -9,7 +9,8 @@ describe('Testing Hook (server): useHydrateQuizLocalState', () => { let hookExecutionResult; expect(() => { const { result } = renderHookServerSide( - () => useHydrateQuizLocalState(quizSessionStorageStateKey, dispatchLocalStateMock), + () => + useHydrateQuizLocalState('quizId', quizSessionStorageStateKey, dispatchLocalStateMock), { initialProps: {}, } diff --git a/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.test.tsx b/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.test.tsx index 964c0a27..d3a2e496 100644 --- a/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.test.tsx +++ b/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.test.tsx @@ -26,7 +26,7 @@ describe('Testing Hook (client): useHydrateQuizLocalState', () => { sessionStorageMock.getItem.mockReturnValue(JSON.stringify(quizSessionStorageStateKey)); const { result } = renderHook(() => - useHydrateQuizLocalState(quizSessionStorageStateKey, dispatchLocalStateMock) + useHydrateQuizLocalState('quizId', quizSessionStorageStateKey, dispatchLocalStateMock) ); act(() => { diff --git a/spec/hooks/useQuizState/useQuizApiState/useQuizApiState.test.tsx b/spec/hooks/useQuizState/useQuizApiState/useQuizApiState.test.tsx index d204f945..c1d51db9 100644 --- a/spec/hooks/useQuizState/useQuizApiState/useQuizApiState.test.tsx +++ b/spec/hooks/useQuizState/useQuizApiState/useQuizApiState.test.tsx @@ -3,6 +3,7 @@ import { mockConstructorIOClient } from '../../../__tests__/utils'; import useQuizApiState from '../../../../src/hooks/useQuizState/useQuizApiState'; import { getQuizResults } from '../../../../src/services'; import { QUIZ_VERSION_ID, QUIZ_ID } from '../../../__tests__/constants'; +import * as usePrevious from '../../../../src/hooks/usePrevious'; jest.mock('../../../../src/services', () => ({ getNextQuestion: jest.fn().mockResolvedValue({ diff --git a/spec/hooks/useQuizState/useSessionStorageState/useSessionStorageState.server.test.tsx b/spec/hooks/useQuizState/useSessionStorageState/useSessionStorageState.server.test.tsx index 7946d051..3f5b29d3 100644 --- a/spec/hooks/useQuizState/useSessionStorageState/useSessionStorageState.server.test.tsx +++ b/spec/hooks/useQuizState/useSessionStorageState/useSessionStorageState.server.test.tsx @@ -24,10 +24,13 @@ describe('Testing Hook (server): useSessionStorageState', () => { quizSessionId: QUIZ_SESSION_ID, }; + const quizId = 'quizId'; + it('does not throw when rendered server-side', () => { expect(() => { renderHookServerSide( - () => useSessionStorageState(mockState, { sessionStateKey: QUIZ_SESSION_KEY }, false), + () => + useSessionStorageState(quizId, mockState, { sessionStateKey: QUIZ_SESSION_KEY }, false), { initialProps: {}, } @@ -37,7 +40,7 @@ describe('Testing Hook (server): useSessionStorageState', () => { it('return correct data on server-side when enableHydration equals false', () => { const { result } = renderHookServerSide( - () => useSessionStorageState(mockState, { sessionStateKey: QUIZ_SESSION_KEY }, false), + () => useSessionStorageState(quizId, mockState, { sessionStateKey: QUIZ_SESSION_KEY }, false), { initialProps: {}, } diff --git a/spec/hooks/useQuizState/useSessionStorageState/useSessionStorageState.test.tsx b/spec/hooks/useQuizState/useSessionStorageState/useSessionStorageState.test.tsx index c8bbe516..bcf304b4 100644 --- a/spec/hooks/useQuizState/useSessionStorageState/useSessionStorageState.test.tsx +++ b/spec/hooks/useQuizState/useSessionStorageState/useSessionStorageState.test.tsx @@ -39,22 +39,37 @@ describe('Testing Hook (client): useSessionStorageState', () => { quizVersionId: 'version1', quizSessionId: 'session1', }; + const quizId = 'quizId'; it('should set item in sessionStorage when conditions are met', () => { - renderHook(() => useSessionStorageState(mockState, { sessionStateKey: 'testKey' }, true)); + renderHook(() => + useSessionStorageState(quizId, mockState, { sessionStateKey: 'testKey' }, true) + ); - expect(mockSetItem).toHaveBeenCalledWith('testKey', JSON.stringify(mockState)); + expect(mockSetItem).toHaveBeenCalledWith('testKey', JSON.stringify({ [quizId]: mockState })); }); it('should retrieve "skipToResults" correctly based on sessionStorage', () => { const { result } = renderHook(() => - useSessionStorageState(mockState, { sessionStateKey: 'testKey' }, true) + useSessionStorageState(quizId, mockState, { sessionStateKey: 'testKey' }, true) ); act(() => { - window.sessionStorage.setItem('testKey', JSON.stringify(mockState)); + window.sessionStorage.setItem('testKey', JSON.stringify({ [quizId]: mockState })); }); expect(result.current.skipToResults).toBe(mockState.isQuizCompleted); }); + + it('should return "hasSessionStorageState" correctly based on sessionStorage', () => { + const { result } = renderHook(() => + useSessionStorageState(quizId, mockState, { sessionStateKey: 'testKey' }, true) + ); + + act(() => { + window.sessionStorage.setItem('testKey', JSON.stringify({ [quizId]: mockState })); + }); + + expect(result.current.hasSessionStorageState()).toBe(false); + }); }); diff --git a/spec/utils/utils.test.ts b/spec/utils/utils.test.ts index 6c140516..6a6ff2eb 100644 --- a/spec/utils/utils.test.ts +++ b/spec/utils/utils.test.ts @@ -21,9 +21,9 @@ describe('convertPrimaryColorsToString', () => { '--primary-color-l': '0', }); expect(result).toEqual(`{ - --primary-color-h: 0; - --primary-color-s: 0; - --primary-color-l: 0; + --primary-color-h: 0; + --primary-color-s: 0; + --primary-color-l: 0; }`); }); }); @@ -229,10 +229,32 @@ describe('resetQuizSessionStorageState', () => { }); it('removes the specified key from sessionStorage', () => { - window.sessionStorage.removeItem = jest.fn(); + window.sessionStorage.setItem = jest.fn(); + + const mockData = { QUIZ_ID_1: { answer: '42' } }; + + jest.spyOn(window.sessionStorage, 'getItem').mockReturnValue(JSON.stringify(mockData)); - const reset = resetQuizSessionStorageState('quizState'); + const reset = resetQuizSessionStorageState('quizState', 'QUIZ_ID_1'); reset(); - expect(window.sessionStorage.removeItem).toHaveBeenCalledWith('quizState'); + expect(window.sessionStorage.setItem).toHaveBeenCalledWith( + 'quizState', + JSON.stringify({ QUIZ_ID_1: null }) + ); + }); + + it('does not modify other quiz data from session', () => { + window.sessionStorage.setItem = jest.fn(); + + const mockData = { QUIZ_ID_2: { answer: '42' } }; + + jest.spyOn(window.sessionStorage, 'getItem').mockReturnValue(JSON.stringify(mockData)); + + const reset = resetQuizSessionStorageState('quizState', 'QUIZ_ID_1'); + reset(); + expect(window.sessionStorage.setItem).toHaveBeenCalledWith( + 'quizState', + JSON.stringify({ ...mockData, QUIZ_ID_1: null }) + ); }); }); diff --git a/src/hooks/useQuizState/useSessionStorageState.ts b/src/hooks/useQuizState/useSessionStorageState.ts index 3b9129e8..a35275cc 100644 --- a/src/hooks/useQuizState/useSessionStorageState.ts +++ b/src/hooks/useQuizState/useSessionStorageState.ts @@ -18,7 +18,7 @@ const useSessionStorageState = ( // Save state to session storage useEffect(() => { // don't save state if initial state - if (quizId !== prevQuizId) return; + if (prevQuizId && quizId !== prevQuizId) return; if (enableHydration && quizLocalState?.answers?.length) { const data = getStateFromSessionStorage(quizSessionStorageStateKey); const dataToSave = { From 16b25cc7e85c1da7af4518c46903b243825190b6 Mon Sep 17 00:00:00 2001 From: vishnu Date: Fri, 8 Nov 2024 00:47:51 +0530 Subject: [PATCH 07/20] fix test --- .../useHydrateQuizLocalState.test.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.test.tsx b/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.test.tsx index d3a2e496..ef5948e5 100644 --- a/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.test.tsx +++ b/spec/hooks/useQuizEvents/useHydrateQuizLocalState/useHydrateQuizLocalState.test.tsx @@ -3,6 +3,7 @@ import useHydrateQuizLocalState from '../../../../src/hooks/useQuizEvents/useHyd import { QuestionTypes } from '../../../../src/components/CioQuiz/actions'; describe('Testing Hook (client): useHydrateQuizLocalState', () => { + const quizId = 'quizId'; const quizSessionStorageStateKey = 'quizState'; const sessionStorageMock = { getItem: jest.fn(), @@ -23,10 +24,12 @@ describe('Testing Hook (client): useHydrateQuizLocalState', () => { it('correctly hydrates quiz local state from sessionStorage', () => { const dispatchLocalStateMock = jest.fn(); - sessionStorageMock.getItem.mockReturnValue(JSON.stringify(quizSessionStorageStateKey)); + sessionStorageMock.getItem.mockReturnValue( + JSON.stringify({ [quizId]: quizSessionStorageStateKey }) + ); const { result } = renderHook(() => - useHydrateQuizLocalState('quizId', quizSessionStorageStateKey, dispatchLocalStateMock) + useHydrateQuizLocalState(quizId, quizSessionStorageStateKey, dispatchLocalStateMock) ); act(() => { From c390b16d56f7ce5c2429dcc665b87e11cd719d93 Mon Sep 17 00:00:00 2001 From: vishnu Date: Mon, 9 Dec 2024 21:32:21 +0530 Subject: [PATCH 08/20] added more getters --- src/components/Results/Results.tsx | 7 ++++--- src/types.ts | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Results/Results.tsx b/src/components/Results/Results.tsx index 7285bd3c..6fe56e86 100644 --- a/src/components/Results/Results.tsx +++ b/src/components/Results/Results.tsx @@ -15,13 +15,14 @@ function Results(props: ResultsProps) { renderResultCard, } = props; - const { state, getAddToCartButtonProps, getAddToFavoritesButtonProps } = useContext(QuizContext); - const getters = { getAddToCartButtonProps, getAddToFavoritesButtonProps }; + const { state, getAddToCartButtonProps, getAddToFavoritesButtonProps, getQuizResultLinkProps } = + useContext(QuizContext); + const getters = { getAddToCartButtonProps, getAddToFavoritesButtonProps, getQuizResultLinkProps }; return (
{state?.quiz?.results?.response?.results?.map((result, index) => renderResultCard ? ( - renderResultCard(result, getters) + renderResultCard(result, getters, index) ) : ( JSX.Element; } From 4bf09ab7900c5d7156a3e5168dedab7fd8e0b440 Mon Sep 17 00:00:00 2001 From: vishnu Date: Wed, 24 Sep 2025 00:39:35 +0530 Subject: [PATCH 09/20] update text --- src/stories/Quiz/Hooks/Docs/markdown/EventsDocs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stories/Quiz/Hooks/Docs/markdown/EventsDocs.md b/src/stories/Quiz/Hooks/Docs/markdown/EventsDocs.md index 3134008d..5023f1b6 100644 --- a/src/stories/Quiz/Hooks/Docs/markdown/EventsDocs.md +++ b/src/stories/Quiz/Hooks/Docs/markdown/EventsDocs.md @@ -15,4 +15,4 @@ | addToCart | `function(e: React.MouseEvent, item, price) => void` | Action event to trigger add to cart click events | | addToFavorites | `function(e: React.MouseEvent, item, price) => void` | Action event trigger add to favorites click events | | hydrateQuiz | `function() => void` | Action event to hydrate the quiz with saved state in session storage on reload | - | quizAnswerChanged | `function(payload: string \| string[] ) => void` | Action event to trigger add to cart click events | + | quizAnswerChanged | `function(payload: string \| options[] ) => void` | Action event to change an answer to a question | From cf319526ae80e2fd94b0c13a3843b9360ee027c4 Mon Sep 17 00:00:00 2001 From: vishnu Date: Wed, 24 Sep 2025 00:42:11 +0530 Subject: [PATCH 10/20] removed unwanted changes --- spec/hooks/usePrevious/usePrevious.test.tsx | 37 ------------------- .../useQuizApiState/useQuizApiState.test.tsx | 1 - src/hooks/usePrevious.ts | 11 ------ src/hooks/useQuizState/useQuizApiState.ts | 7 ---- .../useQuizState/useSessionStorageState.ts | 1 - 5 files changed, 57 deletions(-) delete mode 100644 spec/hooks/usePrevious/usePrevious.test.tsx delete mode 100644 src/hooks/usePrevious.ts diff --git a/spec/hooks/usePrevious/usePrevious.test.tsx b/spec/hooks/usePrevious/usePrevious.test.tsx deleted file mode 100644 index a0ceb95c..00000000 --- a/spec/hooks/usePrevious/usePrevious.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import usePrevious from '../../../src/hooks/usePrevious'; - -describe(`${usePrevious.name}`, () => { - function Component({ x }: { x: number }) { - const prevX = usePrevious(x); - return ( - <> - x={x};prevX={prevX} - - ); - } - - it('starts by having the previous value undefined', () => { - const { container } = render(); - expect(container).toHaveTextContent('x=10'); - expect(container).toHaveTextContent('prevX='); - }); - - it('changes the previous value when rendered again with a different value', () => { - const { container, rerender } = render(); - rerender(); - expect(container).toHaveTextContent('x=11'); - expect(container).toHaveTextContent('prevX=10'); - }); - - it('changes the previous value if rendered again with the same value', () => { - const { container, rerender } = render(); - rerender(); - expect(container).toHaveTextContent('x=11'); - expect(container).toHaveTextContent('prevX=10'); - rerender(); - expect(container).toHaveTextContent('x=11'); - expect(container).toHaveTextContent('prevX=11'); - }); -}); diff --git a/spec/hooks/useQuizState/useQuizApiState/useQuizApiState.test.tsx b/spec/hooks/useQuizState/useQuizApiState/useQuizApiState.test.tsx index cd7dfa32..da3496a0 100644 --- a/spec/hooks/useQuizState/useQuizApiState/useQuizApiState.test.tsx +++ b/spec/hooks/useQuizState/useQuizApiState/useQuizApiState.test.tsx @@ -3,7 +3,6 @@ import { mockConstructorIOClient } from '../../../__tests__/utils'; import useQuizApiState from '../../../../src/hooks/useQuizState/useQuizApiState'; import { getQuizResults } from '../../../../src/services'; import { QUIZ_VERSION_ID, QUIZ_ID } from '../../../__tests__/constants'; -import * as usePrevious from '../../../../src/hooks/usePrevious'; jest.mock('../../../../src/services', () => ({ getNextQuestion: jest.fn().mockResolvedValue({ diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts deleted file mode 100644 index f834f0fa..00000000 --- a/src/hooks/usePrevious.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useEffect, useRef } from 'react'; - -function usePrevious(value: T) { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }); - return ref.current; -} - -export default usePrevious; diff --git a/src/hooks/useQuizState/useQuizApiState.ts b/src/hooks/useQuizState/useQuizApiState.ts index 6c3b1d06..d5ce5d7b 100644 --- a/src/hooks/useQuizState/useQuizApiState.ts +++ b/src/hooks/useQuizState/useQuizApiState.ts @@ -19,7 +19,6 @@ import { } from '../../services'; import { IQuizProps } from '../../types'; import useQueryParams from '../useQueryParams'; -import usePrevious from '../usePrevious'; import { isFunction } from '../../utils'; type UseQuizApiState = ( @@ -133,13 +132,8 @@ const useQuizApiState: UseQuizApiState = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [quizApiState.resultsConfig]); - const prevQuizId = usePrevious(quizId); - useEffect(() => { (async () => { - // If quizId is the same as the previous quizId, wait for next render after reset - if (!!prevQuizId && quizId !== prevQuizId) return; - dispatchApiState({ type: QuizAPIActionTypes.SET_IS_LOADING, }); @@ -194,7 +188,6 @@ const useQuizApiState: UseQuizApiState = ( }, [ cioClient, quizId, - prevQuizId, quizLocalState.answers, resultsPageOptions?.numResultsToDisplay, isSharedResultsQuery, diff --git a/src/hooks/useQuizState/useSessionStorageState.ts b/src/hooks/useQuizState/useSessionStorageState.ts index 97fdbff0..16ef99e3 100644 --- a/src/hooks/useQuizState/useSessionStorageState.ts +++ b/src/hooks/useQuizState/useSessionStorageState.ts @@ -3,7 +3,6 @@ import { getStateFromSessionStorage } from '../../utils'; import { SessionStateOptions } from '../../types'; import { QuizLocalReducerState } from '../../components/CioQuiz/quizLocalReducer'; import { quizSessionStateKey } from '../../constants'; -import usePrevious from '../usePrevious'; const useSessionStorageState = ( quizId: string, From bdfb0965e371d13075dea9f1c026f2fb3883a708 Mon Sep 17 00:00:00 2001 From: vishnu Date: Wed, 24 Sep 2025 13:59:58 +0530 Subject: [PATCH 11/20] added a jump to question event --- src/components/CioQuiz/actions.ts | 11 ++- src/components/CioQuiz/context.ts | 2 + src/components/CioQuiz/index.tsx | 2 + src/components/CioQuiz/quizApiReducer.ts | 8 ++ src/components/CioQuiz/quizLocalReducer.ts | 74 +++++++++---------- src/hooks/usePropsGetters/index.ts | 9 +++ .../useJumpToQuestionButtonProps.ts | 32 ++++++++ src/hooks/useQuizEvents/index.ts | 9 +++ src/hooks/useQuizEvents/useJumpToQuestion.ts | 49 ++++++++++++ src/hooks/useQuizEvents/useQuizResetClick.ts | 2 +- src/types.ts | 10 +++ 11 files changed, 165 insertions(+), 43 deletions(-) create mode 100644 src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts create mode 100644 src/hooks/useQuizEvents/useJumpToQuestion.ts diff --git a/src/components/CioQuiz/actions.ts b/src/components/CioQuiz/actions.ts index a5d5ee90..1b3b90f7 100644 --- a/src/components/CioQuiz/actions.ts +++ b/src/components/CioQuiz/actions.ts @@ -23,6 +23,7 @@ export enum QuestionTypes { Reset = 'reset', Hydrate = 'hydrate', Complete = 'complete', + JumpToQuestion = 'jump_to_question', } export interface QuestionAnswer { @@ -54,6 +55,7 @@ export type ActionAnswerQuestion = | Action | Action | Action + | Action | Action>; // API actions @@ -63,6 +65,7 @@ export enum QuizAPIActionTypes { SET_QUIZ_RESULTS, SET_CURRENT_QUESTION, RESET_QUIZ, + JUMP_TO_QUESTION, SET_QUIZ_SHARED_RESULTS, SET_QUIZ_RESULTS_CONFIG, SET_QUIZ_RESULTS_CONFIG_ERROR, @@ -85,7 +88,10 @@ export type ActionSetCurrentQuestion = Action< QuizAPIActionTypes.SET_CURRENT_QUESTION, { quizCurrentQuestion: NextQuestionResponse; quizSessionId?: string; quizVersionId?: string } >; - +export type ActionJumpToQuestion = Action< + QuizAPIActionTypes.JUMP_TO_QUESTION, + { questionId: number } +>; export type ActionResetQuiz = Action; export type ActionSetQuizResultsConfig = | Action< @@ -100,4 +106,5 @@ export type ActionQuizAPI = | ActionSetCurrentQuestion | ActionResetQuiz | ActionSetQuizSharedResults - | ActionSetQuizResultsConfig; + | ActionSetQuizResultsConfig + | ActionJumpToQuestion; diff --git a/src/components/CioQuiz/context.ts b/src/components/CioQuiz/context.ts index b924fcf2..b9458f45 100644 --- a/src/components/CioQuiz/context.ts +++ b/src/components/CioQuiz/context.ts @@ -17,6 +17,7 @@ import { PrimaryColorStyles, QuizReturnState, GetShareResultsButtonProps, + GetJumpToQuestionButtonProps, } from '../../types'; export interface QuizContextValue { @@ -36,6 +37,7 @@ export interface QuizContextValue { getAddToFavoritesButtonProps: GetAddToFavoritesButtonProps; getQuizResultButtonProps: GetQuizResultButtonProps; getQuizResultLinkProps: GetQuizResultLinkProps; + getJumpToQuestionButtonProps: GetJumpToQuestionButtonProps; primaryColorStyles: PrimaryColorStyles; customClickItemCallback: boolean; customAddToFavoritesCallback: boolean; diff --git a/src/components/CioQuiz/index.tsx b/src/components/CioQuiz/index.tsx index 3216509c..0cc365b8 100644 --- a/src/components/CioQuiz/index.tsx +++ b/src/components/CioQuiz/index.tsx @@ -24,6 +24,7 @@ export default function CioQuiz(props: IQuizProps) { getHydrateQuizButtonProps, getNextQuestionButtonProps, getSkipQuestionButtonProps, + getJumpToQuestionButtonProps, getOpenTextInputProps, getPreviousQuestionButtonProps, getQuizImageProps, @@ -69,6 +70,7 @@ export default function CioQuiz(props: IQuizProps) { getHydrateQuizButtonProps, getNextQuestionButtonProps, getSkipQuestionButtonProps, + getJumpToQuestionButtonProps, getOpenTextInputProps, getPreviousQuestionButtonProps, getQuizImageProps, diff --git a/src/components/CioQuiz/quizApiReducer.ts b/src/components/CioQuiz/quizApiReducer.ts index 9952d2bd..24d923fa 100644 --- a/src/components/CioQuiz/quizApiReducer.ts +++ b/src/components/CioQuiz/quizApiReducer.ts @@ -106,6 +106,14 @@ export default function apiReducer( selectedOptionsWithAttributes: action.payload?.quizResults.attributes, }; } + case QuizAPIActionTypes.JUMP_TO_QUESTION: { + return { + ...state, + quizResults: undefined, + selectedOptionsWithAttributes: undefined, + matchedOptions: undefined, + }; + } case QuizAPIActionTypes.SET_QUIZ_RESULTS_CONFIG: { return { ...state, diff --git a/src/components/CioQuiz/quizLocalReducer.ts b/src/components/CioQuiz/quizLocalReducer.ts index 944f3e43..943d2674 100644 --- a/src/components/CioQuiz/quizLocalReducer.ts +++ b/src/components/CioQuiz/quizLocalReducer.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { AnswerInputState, QuestionOption } from '../../types'; import { ActionAnswerQuestion, QuestionTypes, ActionAnswerInputQuestion } from './actions'; @@ -18,16 +19,6 @@ export const initialState: QuizLocalReducerState = { isQuizCompleted: false, }; -function answerInputReducer(state: AnswerInputState, action: ActionAnswerInputQuestion) { - return { - ...state, - [String(action.payload!.questionId)]: { - type: action.type, - value: action.payload!.input, - }, - }; -} - function handleNextQuestion(state: QuizLocalReducerState) { const { answers, answerInputs } = state; const newAnswers = [...answers]; @@ -63,47 +54,30 @@ function handleNextQuestion(state: QuizLocalReducerState) { }; } +const handleAnswerInput = (state: QuizLocalReducerState, action: ActionAnswerInputQuestion) => ({ + ...state, + answerInputs: { + ...state.answerInputs, + [String(action.payload!.questionId)]: { + type: action.type, + value: action.payload!.input, + }, + }, + isQuizCompleted: false, +}); + export default function quizLocalReducer( state: QuizLocalReducerState, action: ActionAnswerQuestion ): QuizLocalReducerState { switch (action.type) { case QuestionTypes.OpenText: - return { - ...state, - answerInputs: answerInputReducer(state.answerInputs, action), - isQuizCompleted: false, - }; case QuestionTypes.Cover: - return { - ...state, - answerInputs: answerInputReducer(state.answerInputs, action), - isQuizCompleted: false, - }; case QuestionTypes.SingleSelect: - return { - ...state, - answerInputs: answerInputReducer(state.answerInputs, action), - isQuizCompleted: false, - }; case QuestionTypes.MultipleSelect: - return { - ...state, - answerInputs: answerInputReducer(state.answerInputs, action), - isQuizCompleted: false, - }; case QuestionTypes.SingleFilterValue: - return { - ...state, - answerInputs: answerInputReducer(state.answerInputs, action), - isQuizCompleted: false, - }; case QuestionTypes.MultipleFilterValues: - return { - ...state, - answerInputs: answerInputReducer(state.answerInputs, action), - isQuizCompleted: false, - }; + return handleAnswerInput(state, action); case QuestionTypes.Next: { return handleNextQuestion(state); } @@ -145,6 +119,26 @@ export default function quizLocalReducer( }; } + case QuestionTypes.JumpToQuestion: { + const questionId = action.payload?.questionId; + if (questionId === undefined) return state; + const prevAnswerInputs = { ...state.prevAnswerInputs }; + + // Remove all keys greater than questionId from answerInputs + const filteredAnswerInputs: AnswerInputState = {}; + Object.keys(prevAnswerInputs).forEach((key) => { + if (parseInt(key, 10) > questionId) return; + filteredAnswerInputs[key] = prevAnswerInputs[key]; + }); + + return { + ...state, + answerInputs: filteredAnswerInputs, + answers: [...state.answers.slice(0, -1)], + isQuizCompleted: false, + }; + } + case QuestionTypes.Reset: return { ...initialState, diff --git a/src/hooks/usePropsGetters/index.ts b/src/hooks/usePropsGetters/index.ts index ba40515f..9e1b1ca1 100644 --- a/src/hooks/usePropsGetters/index.ts +++ b/src/hooks/usePropsGetters/index.ts @@ -17,6 +17,7 @@ import { GetAddToFavoritesButtonProps, GetSkipQuestionButtonProps, GetShareResultsButtonProps, + GetJumpToQuestionButtonProps, } from '../../types'; import { QuizAPIReducerState } from '../../components/CioQuiz/quizApiReducer'; import { QuizLocalReducerState } from '../../components/CioQuiz/quizLocalReducer'; @@ -27,6 +28,7 @@ import useNextQuestionButtonProps from './useNextQuestionButtonProps'; import usePreviousQuestionButtonProps from './usePreviousQuestionButtonProps'; import useAddToFavoritesButtonProps from './useAddToFavoritesButtonProps'; import useSkipQuestionButtonProps from './useSkipQuestionButtonProps'; +import useJumpToQuestionButtonProps from './useJumpToQuestionButtonProps'; const usePropsGetters = ( quizEvents: QuizEventsReturn, @@ -44,6 +46,7 @@ const usePropsGetters = ( addToCart, addToFavorites, resultClick, + jumpToQuestion, } = quizEvents; const getOpenTextInputProps: GetOpenTextInputProps = useOpenTextInputProps( @@ -76,6 +79,11 @@ const usePropsGetters = ( quizApiState ); + const getJumpToQuestionButtonProps: GetJumpToQuestionButtonProps = useJumpToQuestionButtonProps( + jumpToQuestion, + quizApiState + ); + const getPreviousQuestionButtonProps: GetPreviousQuestionButtonProps = usePreviousQuestionButtonProps(quizApiState, previousQuestion); @@ -182,6 +190,7 @@ const usePropsGetters = ( getQuizResultButtonProps, getQuizResultLinkProps, getSkipQuestionButtonProps, + getJumpToQuestionButtonProps, }; }; diff --git a/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts b/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts new file mode 100644 index 00000000..1d00060e --- /dev/null +++ b/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts @@ -0,0 +1,32 @@ +import { useCallback } from 'react'; +import { QuizAPIReducerState } from '../../components/CioQuiz/quizApiReducer'; +import { GetJumpToQuestionButtonProps, QuizEventsReturn } from '../../types'; + +export default function useNextQuestionButtonProps( + jumpToQuestion: QuizEventsReturn.JumpToQuestion, + quizApiState: QuizAPIReducerState +): GetJumpToQuestionButtonProps { + const getJumpToQuestionButtonProps: GetJumpToQuestionButtonProps = useCallback( + (id: number) => { + const currentQuestionId = quizApiState.quizCurrentQuestion?.next_question?.id; + let buttonDisabled; + if (!currentQuestionId || (currentQuestionId && id >= currentQuestionId)) { + buttonDisabled = true; + } + + return { + className: buttonDisabled ? 'cio-question-cta-button disabled' : 'cio-question-cta-button', + tabIndex: buttonDisabled ? -1 : 0, + 'aria-disabled': buttonDisabled ? 'true' : 'false', + 'aria-describedby': buttonDisabled ? 'next-button-help' : '', + type: 'button', + onClick: () => { + jumpToQuestion(id); + }, + }; + }, + [quizApiState.quizCurrentQuestion, jumpToQuestion] + ); + + return getJumpToQuestionButtonProps; +} diff --git a/src/hooks/useQuizEvents/index.ts b/src/hooks/useQuizEvents/index.ts index 3cda8c89..680fac21 100644 --- a/src/hooks/useQuizEvents/index.ts +++ b/src/hooks/useQuizEvents/index.ts @@ -12,6 +12,7 @@ import useQuizState from '../useQuizState'; import { resetQuizSessionStorageState } from '../../utils'; import useQuizAddToFavorites from './useQuizAddToFavorites'; import useQuizSkipClick from './useQuizSkipClick'; +import useJumpToQuestion from './useJumpToQuestion'; type UseQuizEvents = ( quizOptions: IQuizProps, @@ -82,6 +83,13 @@ const useQuizEvents: UseQuizEvents = (quizOptions, cioClient, quizState) => { quizResults: quizApiState.quizResults, }); + const jumpToQuestion = useJumpToQuestion({ + dispatchLocalState, + dispatchApiState, + quizApiState, + quizLocalState, + }); + // Quiz rehydrate const hydrateQuizLocalState = useHydrateQuizLocalState( quizOptions.quizId, @@ -98,6 +106,7 @@ const useQuizEvents: UseQuizEvents = (quizOptions, cioClient, quizState) => { nextQuestion, skipQuestion, resetQuiz, + jumpToQuestion, hydrateQuiz: hydrateQuizLocalState, resetSessionStorageState, }; diff --git a/src/hooks/useQuizEvents/useJumpToQuestion.ts b/src/hooks/useQuizEvents/useJumpToQuestion.ts new file mode 100644 index 00000000..90be94be --- /dev/null +++ b/src/hooks/useQuizEvents/useJumpToQuestion.ts @@ -0,0 +1,49 @@ +import { useCallback } from 'react'; +import { + ActionAnswerQuestion, + ActionQuizAPI, + QuestionTypes, + QuizAPIActionTypes, +} from '../../components/CioQuiz/actions'; +import { QuizEventsReturn } from '../../types'; +import { QuizAPIReducerState } from '../../components/CioQuiz/quizApiReducer'; +import { QuizLocalReducerState } from '../../components/CioQuiz/quizLocalReducer'; + +type IUseJumpToQuestionProps = { + dispatchLocalState: React.Dispatch; + dispatchApiState: React.Dispatch; + quizApiState: QuizAPIReducerState; + quizLocalState: QuizLocalReducerState; +}; + +const useJumpToQuestion = (props: IUseJumpToQuestionProps): QuizEventsReturn.JumpToQuestion => { + const { dispatchLocalState, dispatchApiState, quizApiState, quizLocalState } = props; + const quizResetClickHandler = useCallback( + (questionId: number) => { + const isComplete = quizLocalState.isQuizCompleted; + const currentQuestionId = quizApiState.quizCurrentQuestion?.id; + + if (isComplete || questionId >= currentQuestionId) { + return; + } + dispatchLocalState({ + type: QuestionTypes.JumpToQuestion, + payload: { questionId }, + }); + dispatchApiState({ + type: QuizAPIActionTypes.JUMP_TO_QUESTION, + payload: { questionId }, + }); + }, + [ + quizLocalState.isQuizCompleted, + quizApiState.quizCurrentQuestion?.id, + dispatchLocalState, + dispatchApiState, + ] + ); + + return quizResetClickHandler; +}; + +export default useJumpToQuestion; diff --git a/src/hooks/useQuizEvents/useQuizResetClick.ts b/src/hooks/useQuizEvents/useQuizResetClick.ts index be8cf321..c57e2362 100644 --- a/src/hooks/useQuizEvents/useQuizResetClick.ts +++ b/src/hooks/useQuizEvents/useQuizResetClick.ts @@ -15,7 +15,7 @@ type IUseQuizResetClickProps = { quizResults?: QuizResultsResponse | QuizSharedResultsData; }; -const useQuizResetClick = (props: IUseQuizResetClickProps): QuizEventsReturn.NextQuestion => { +const useQuizResetClick = (props: IUseQuizResetClickProps): QuizEventsReturn.ResetQuiz => { const { resetQuizSessionStorageState, dispatchLocalState, dispatchApiState, quizResults } = props; const { removeSharedResultsQueryParams, isSharedResultsQuery } = useQueryParams(); const quizResetClickHandler = useCallback(() => { diff --git a/src/types.ts b/src/types.ts index cf6687b3..a7261fe6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -187,6 +187,7 @@ export namespace QuizEventsReturn { export type SkipQuestion = () => void; export type PreviousQuestion = () => void; export type ResetQuiz = () => void; + export type JumpToQuestion = (questionId: number) => void; export type ResultClick = (result: QuizResultDataPartial, position: number) => void; export type AddToCart = ( e: React.MouseEvent, @@ -206,6 +207,7 @@ export namespace QuizEventsReturn { export interface QuizEventsReturn { nextQuestion: QuizEventsReturn.NextQuestion; skipQuestion: QuizEventsReturn.SkipQuestion; + jumpToQuestion: QuizEventsReturn.JumpToQuestion; quizAnswerChanged: QuizEventsReturn.QuizAnswerChanged; previousQuestion: QuizEventsReturn.PreviousQuestion; resetQuiz: QuizEventsReturn.ResetQuiz; @@ -313,12 +315,19 @@ export interface SelectInputProps { key: number | string; } +export interface JumpToQuestionButtonProps { + className: string; + onClick: React.MouseEventHandler; + style?: Record; +} + export type GetOpenTextInputProps = () => OpenTextInputProps; export type GetCoverQuestionProps = () => CoverQuestionProps; export type GetSelectInputProps = (option: QuestionOption) => SelectInputProps; export type GetNextQuestionButtonProps = () => NextQuestionButtonProps; export type GetSkipQuestionButtonProps = () => SkipQuestionButtonProps; export type GetPreviousQuestionButtonProps = () => PreviousQuestionButtonProps; +export type GetJumpToQuestionButtonProps = (id: number) => JumpToQuestionButtonProps; export type GetResetQuizButtonProps = ( stylesType?: 'primary' | 'secondary' ) => ResetQuizButtonProps; @@ -371,6 +380,7 @@ export interface UseQuizReturn { getAddToFavoritesButtonProps: GetAddToFavoritesButtonProps; getQuizResultButtonProps: GetQuizResultButtonProps; getQuizResultLinkProps: GetQuizResultLinkProps; + getJumpToQuestionButtonProps: GetJumpToQuestionButtonProps; primaryColorStyles: PrimaryColorStyles; } From 320e8b88ca7d6ed304d458ff27f78560dd7551b4 Mon Sep 17 00:00:00 2001 From: vishnu Date: Sun, 28 Sep 2025 16:25:09 +0530 Subject: [PATCH 12/20] fix issue with jump logic --- src/components/CioQuiz/quizLocalReducer.ts | 5 ++++- src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/CioQuiz/quizLocalReducer.ts b/src/components/CioQuiz/quizLocalReducer.ts index 943d2674..7d4b1d76 100644 --- a/src/components/CioQuiz/quizLocalReducer.ts +++ b/src/components/CioQuiz/quizLocalReducer.ts @@ -131,10 +131,13 @@ export default function quizLocalReducer( filteredAnswerInputs[key] = prevAnswerInputs[key]; }); + // Calculate the number of questions to keep (questions <= questionId) + const questionsToKeep = Object.keys(filteredAnswerInputs).length; + return { ...state, answerInputs: filteredAnswerInputs, - answers: [...state.answers.slice(0, -1)], + answers: state.answers.slice(0, questionsToKeep), isQuizCompleted: false, }; } diff --git a/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts b/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts index 1d00060e..b8f7ee28 100644 --- a/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts +++ b/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts @@ -18,7 +18,7 @@ export default function useNextQuestionButtonProps( className: buttonDisabled ? 'cio-question-cta-button disabled' : 'cio-question-cta-button', tabIndex: buttonDisabled ? -1 : 0, 'aria-disabled': buttonDisabled ? 'true' : 'false', - 'aria-describedby': buttonDisabled ? 'next-button-help' : '', + 'aria-describedby': buttonDisabled ? 'jump-to-button-help' : '', type: 'button', onClick: () => { jumpToQuestion(id); From 547d1d4d17ec9a6ac9982d1121551050940934f6 Mon Sep 17 00:00:00 2001 From: vishnu Date: Wed, 1 Oct 2025 10:24:02 +0530 Subject: [PATCH 13/20] added tests for new hooks --- ...seJumpToQuestionButtonProps.server.test.ts | 30 ++++++++++ .../useJumpToQuestionButtonProps.test.ts | 59 +++++++++++++++++++ .../useJumpToQuestion.server.test.ts | 34 +++++++++++ .../useJumpToQuestion.test.ts | 44 ++++++++++++++ src/components/CioQuiz/quizLocalReducer.ts | 2 +- .../useJumpToQuestionButtonProps.ts | 4 +- src/hooks/useQuizEvents/index.ts | 1 - src/hooks/useQuizEvents/useJumpToQuestion.ts | 18 ++---- 8 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.server.test.ts create mode 100644 spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.test.ts create mode 100644 spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.server.test.ts create mode 100644 spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.test.ts diff --git a/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.server.test.ts b/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.server.test.ts new file mode 100644 index 00000000..a7dac5bb --- /dev/null +++ b/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.server.test.ts @@ -0,0 +1,30 @@ +import { renderHookServerSide } from '../../../__tests__/utils.server'; +import useJumpToQuestionButtonProps from '../../../../src/hooks/usePropsGetters/useJumpToQuestionButtonProps'; +import { QuizAPIReducerState } from '../../../../src/components/CioQuiz/quizApiReducer'; + +describe('Testing Hook (server): useJumpToQuestionButtonProps', () => { + it('initializes without errors and returns a function that provides button props', () => { + const jumpToQuestionMock = jest.fn(); + const quizApiStateMock = { + quizCurrentQuestion: { + next_question: { id: '2' }, + isCoverQuestion: false, + }, + } as unknown as QuizAPIReducerState; + + const { result } = renderHookServerSide( + () => useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiStateMock), + { + initialProps: {}, + } + ); + + expect(typeof result).toBe('function'); + + const buttonProps = result(); + expect(buttonProps).toHaveProperty('className'); + expect(buttonProps).toHaveProperty('type'); + expect(buttonProps).toHaveProperty('onClick'); + expect(typeof buttonProps.onClick).toBe('function'); + }); +}); diff --git a/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.test.ts b/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.test.ts new file mode 100644 index 00000000..194bd1d5 --- /dev/null +++ b/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.test.ts @@ -0,0 +1,59 @@ +import { renderHook, act } from '@testing-library/react'; +import useJumpToQuestionButtonProps from '../../../../src/hooks/usePropsGetters/useJumpToQuestionButtonProps'; +import { QuizAPIReducerState } from '../../../../src/components/CioQuiz/quizApiReducer'; + +describe('Testing Hook (client): useJumpToQuestionButtonProps', () => { + const mockEvent = { preventDefault: jest.fn() } as unknown as React.MouseEvent; + const jumpToQuestionMock = jest.fn(); + + it('returns button props with disabled class if no answer is provided', () => { + const quizApiState = { + quizCurrentQuestion: { + next_question: { id: '2' }, + isCoverQuestion: false, + }, + } as unknown as QuizAPIReducerState; + + const { result } = renderHook(() => + useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState) + ); + + expect(result.current(2).className).toContain('disabled'); + }); + + it('returns button props without disabled class if an answer is provided', () => { + const quizApiState = { + quizCurrentQuestion: { + next_question: { id: '3' }, + isCoverQuestion: false, + }, + } as unknown as QuizAPIReducerState; + + const { result } = renderHook(() => + useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState) + ); + + expect(result.current(1).className).not.toContain('disabled'); + }); + + it('calls jumpToQuestion function when button is clicked', () => { + const quizApiState = { + quizCurrentQuestion: { + next_question: { id: '3' }, + isCoverQuestion: false, + }, + } as unknown as QuizAPIReducerState; + + const { result } = renderHook(() => + useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState) + ); + + expect(typeof result.current(1).onClick).toBe('function'); + + act(() => { + result.current(1).onClick(mockEvent); + }); + + expect(jumpToQuestionMock).toHaveBeenCalled(); + }); +}); diff --git a/spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.server.test.ts b/spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.server.test.ts new file mode 100644 index 00000000..58621d13 --- /dev/null +++ b/spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.server.test.ts @@ -0,0 +1,34 @@ +import { renderHookServerSide } from '../../../__tests__/utils.server'; +import useJumpToQuestion from '../../../../src/hooks/useQuizEvents/useJumpToQuestion'; +import { QuizAPIReducerState } from '../../../../src/components/CioQuiz/quizApiReducer'; + +describe('Testing Hook (server): useJumpToQuestion', () => { + it('initializes without throwing errors', async () => { + const dispatchLocalStateMock = jest.fn(); + const dispatchApiStateMock = jest.fn(); + const quizApiStateMock: QuizAPIReducerState = { + quizCurrentQuestion: { + id: '1', + next_question: { + id: '2', + type: 'singleChoice', + }, + }, + } as unknown as QuizAPIReducerState; + + const executeHook = () => + renderHookServerSide( + () => + useJumpToQuestion({ + quizApiState: quizApiStateMock, + dispatchLocalState: dispatchLocalStateMock, + dispatchApiState: dispatchApiStateMock, + }), + { + initialProps: {}, + } + ); + + expect(executeHook).not.toThrow(); + }); +}); diff --git a/spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.test.ts b/spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.test.ts new file mode 100644 index 00000000..b333a17c --- /dev/null +++ b/spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.test.ts @@ -0,0 +1,44 @@ +import { renderHook, act } from '@testing-library/react'; + +import useJumpToQuestion from '../../../../src/hooks/useQuizEvents/useJumpToQuestion'; +import { QuizAPIReducerState } from '../../../../src/components/CioQuiz/quizApiReducer'; +import { QuestionTypes, QuizAPIActionTypes } from '../../../../src/components/CioQuiz/actions'; + +describe('Testing Hook (client): useJumpToQuestion', () => { + const dispatchLocalStateMock = jest.fn(); + const dispatchApiStateMock = jest.fn(); + + it('calls dispatchLocalState and dispatchApiState correctly', () => { + const quizApiStateMock = { + quizCurrentQuestion: { + id: '2', + next_question: { + id: '3', + type: 'singleChoice', + }, + }, + } as unknown as QuizAPIReducerState; + + const { result } = renderHook(() => + useJumpToQuestion({ + quizApiState: quizApiStateMock, + dispatchLocalState: dispatchLocalStateMock, + dispatchApiState: dispatchApiStateMock, + }) + ); + + act(() => { + result.current(1); + }); + + expect(dispatchLocalStateMock).toHaveBeenCalledWith({ + type: QuestionTypes.JumpToQuestion, + payload: { questionId: 1 }, + }); + + expect(dispatchApiStateMock).toHaveBeenCalledWith({ + type: QuizAPIActionTypes.JUMP_TO_QUESTION, + payload: { questionId: 1 }, + }); + }); +}); diff --git a/src/components/CioQuiz/quizLocalReducer.ts b/src/components/CioQuiz/quizLocalReducer.ts index 7d4b1d76..4de612d2 100644 --- a/src/components/CioQuiz/quizLocalReducer.ts +++ b/src/components/CioQuiz/quizLocalReducer.ts @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import { AnswerInputState, QuestionOption } from '../../types'; import { ActionAnswerQuestion, QuestionTypes, ActionAnswerInputQuestion } from './actions'; @@ -66,6 +65,7 @@ const handleAnswerInput = (state: QuizLocalReducerState, action: ActionAnswerInp isQuizCompleted: false, }); +// eslint-disable-next-line complexity export default function quizLocalReducer( state: QuizLocalReducerState, action: ActionAnswerQuestion diff --git a/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts b/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts index b8f7ee28..9f8fcb15 100644 --- a/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts +++ b/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import { QuizAPIReducerState } from '../../components/CioQuiz/quizApiReducer'; import { GetJumpToQuestionButtonProps, QuizEventsReturn } from '../../types'; -export default function useNextQuestionButtonProps( +export default function useJumpToQuestionButtonProps( jumpToQuestion: QuizEventsReturn.JumpToQuestion, quizApiState: QuizAPIReducerState ): GetJumpToQuestionButtonProps { @@ -10,7 +10,7 @@ export default function useNextQuestionButtonProps( (id: number) => { const currentQuestionId = quizApiState.quizCurrentQuestion?.next_question?.id; let buttonDisabled; - if (!currentQuestionId || (currentQuestionId && id >= currentQuestionId)) { + if (currentQuestionId && id >= currentQuestionId) { buttonDisabled = true; } diff --git a/src/hooks/useQuizEvents/index.ts b/src/hooks/useQuizEvents/index.ts index 680fac21..6a1e1f38 100644 --- a/src/hooks/useQuizEvents/index.ts +++ b/src/hooks/useQuizEvents/index.ts @@ -87,7 +87,6 @@ const useQuizEvents: UseQuizEvents = (quizOptions, cioClient, quizState) => { dispatchLocalState, dispatchApiState, quizApiState, - quizLocalState, }); // Quiz rehydrate diff --git a/src/hooks/useQuizEvents/useJumpToQuestion.ts b/src/hooks/useQuizEvents/useJumpToQuestion.ts index 90be94be..814928b3 100644 --- a/src/hooks/useQuizEvents/useJumpToQuestion.ts +++ b/src/hooks/useQuizEvents/useJumpToQuestion.ts @@ -7,23 +7,20 @@ import { } from '../../components/CioQuiz/actions'; import { QuizEventsReturn } from '../../types'; import { QuizAPIReducerState } from '../../components/CioQuiz/quizApiReducer'; -import { QuizLocalReducerState } from '../../components/CioQuiz/quizLocalReducer'; type IUseJumpToQuestionProps = { dispatchLocalState: React.Dispatch; dispatchApiState: React.Dispatch; quizApiState: QuizAPIReducerState; - quizLocalState: QuizLocalReducerState; }; const useJumpToQuestion = (props: IUseJumpToQuestionProps): QuizEventsReturn.JumpToQuestion => { - const { dispatchLocalState, dispatchApiState, quizApiState, quizLocalState } = props; - const quizResetClickHandler = useCallback( + const { dispatchLocalState, dispatchApiState, quizApiState } = props; + const quizJumpToQuestionClickHandler = useCallback( (questionId: number) => { - const isComplete = quizLocalState.isQuizCompleted; const currentQuestionId = quizApiState.quizCurrentQuestion?.id; - if (isComplete || questionId >= currentQuestionId) { + if (questionId >= currentQuestionId) { return; } dispatchLocalState({ @@ -35,15 +32,10 @@ const useJumpToQuestion = (props: IUseJumpToQuestionProps): QuizEventsReturn.Jum payload: { questionId }, }); }, - [ - quizLocalState.isQuizCompleted, - quizApiState.quizCurrentQuestion?.id, - dispatchLocalState, - dispatchApiState, - ] + [quizApiState.quizCurrentQuestion?.id, dispatchLocalState, dispatchApiState] ); - return quizResetClickHandler; + return quizJumpToQuestionClickHandler; }; export default useJumpToQuestion; From e835cc74cfa551849fc7c79bc55f6a5c8aa29eda Mon Sep 17 00:00:00 2001 From: vishnu Date: Wed, 1 Oct 2025 10:29:43 +0530 Subject: [PATCH 14/20] added documentation --- .../markdown/QuizQuestionsPropGettersDocs.md | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/stories/Quiz/Hooks/Docs/markdown/QuizQuestionsPropGettersDocs.md b/src/stories/Quiz/Hooks/Docs/markdown/QuizQuestionsPropGettersDocs.md index 52b0b9bf..43ae9062 100644 --- a/src/stories/Quiz/Hooks/Docs/markdown/QuizQuestionsPropGettersDocs.md +++ b/src/stories/Quiz/Hooks/Docs/markdown/QuizQuestionsPropGettersDocs.md @@ -14,9 +14,9 @@ ); ``` - + - ##### `getSelectInputProps` - + This method should be applied to an element of type `
  • ` or `` on quiz questions of type SelectQuestion. This handles selection state, events and styles. @@ -71,7 +71,7 @@ ); ``` - + - ##### `getSkipQuestionButtonProps` This method should be applied to an element of type ` + ); + ``` + - ##### `getQuizImageProps` This method should be applied to an element of type `` or `
    ` on quiz images of any question type. From e62ca24e941320a7faa4591afa7bf146ad7d2f1f Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 2 Oct 2025 09:29:08 +0530 Subject: [PATCH 15/20] corrected jump logic, addressed comments --- spec/components/CioQuiz/reducerTestCases.ts | 97 +++++++++++++++++++ src/components/CioQuiz/quizLocalReducer.ts | 3 +- .../useQuizState/useSessionStorageState.ts | 6 +- .../markdown/QuizQuestionsPropGettersDocs.md | 8 +- 4 files changed, 108 insertions(+), 6 deletions(-) diff --git a/spec/components/CioQuiz/reducerTestCases.ts b/spec/components/CioQuiz/reducerTestCases.ts index c0477e35..4a36f6da 100644 --- a/spec/components/CioQuiz/reducerTestCases.ts +++ b/spec/components/CioQuiz/reducerTestCases.ts @@ -97,6 +97,47 @@ export const apiReducerCases = [ matchedOptions: [], }, }, + { + initialState: { + ...apiInitialState, + quizRequestState: 'SUCCESS', + quizResults: { + ...results, + quiz_selected_options: [ + { + attribute: null, + has_attribute: false, + id: 1, + is_matched: true, + value: 'VALUE', + }, + { + attribute: null, + has_attribute: true, + id: 4, + is_matched: false, + value: 'VALUE', + }, + ], + }, + quizCurrentQuestion: undefined, + selectedOptionsWithAttributes: ['VALUE'], + matchedOptions: [], + }, + action: { + type: QuizAPIActionTypes.JUMP_TO_QUESTION, + payload: { + questionId: 2, + }, + }, + expected: { + ...apiInitialState, + quizRequestState: 'SUCCESS', + quizResults: apiInitialState.quizResults, + selectedOptionsWithAttributes: apiInitialState.selectedOptionsWithAttributes, + matchedOptions: undefined, + }, + }, { initialState: apiInitialState, action: { type: 'unknown' }, @@ -330,6 +371,62 @@ export const localReducerCases = [ }, }, }, + { + initialState: { + ...localInitialState, + answerInputs: { + '1': { + type: QuestionTypes.SingleSelect, + value: [{ id: 1 }], + }, + '2': { + type: QuestionTypes.OpenText, + value: 'true', + }, + '3': { + type: QuestionTypes.SingleSelect, + value: [{ id: 1 }], + }, + }, + prevAnswerInputs: { + '1': { + type: QuestionTypes.SingleSelect, + value: [{ id: 1 }], + }, + '2': { + type: QuestionTypes.OpenText, + value: 'true', + }, + '3': { + type: QuestionTypes.SingleSelect, + value: [{ id: 1 }], + }, + }, + answers: [[1], ['true'], [1]], + isQuizCompleted: true, + }, + action: { + type: QuestionTypes.JumpToQuestion, + payload: { questionId: 2 }, + }, + expected: { + ...localInitialState, + isQuizCompleted: false, + answers: [[1]], + answerInputs: { + '1': { + type: QuestionTypes.SingleSelect, + value: [{ id: 1 }], + }, + }, + prevAnswerInputs: { + '1': { + type: QuestionTypes.SingleSelect, + value: [{ id: 1 }], + }, + }, + }, + }, { initialState: { ...localInitialState, diff --git a/src/components/CioQuiz/quizLocalReducer.ts b/src/components/CioQuiz/quizLocalReducer.ts index 4de612d2..e9cc9043 100644 --- a/src/components/CioQuiz/quizLocalReducer.ts +++ b/src/components/CioQuiz/quizLocalReducer.ts @@ -127,7 +127,7 @@ export default function quizLocalReducer( // Remove all keys greater than questionId from answerInputs const filteredAnswerInputs: AnswerInputState = {}; Object.keys(prevAnswerInputs).forEach((key) => { - if (parseInt(key, 10) > questionId) return; + if (parseInt(key, 10) >= questionId) return; filteredAnswerInputs[key] = prevAnswerInputs[key]; }); @@ -137,6 +137,7 @@ export default function quizLocalReducer( return { ...state, answerInputs: filteredAnswerInputs, + prevAnswerInputs: filteredAnswerInputs, answers: state.answers.slice(0, questionsToKeep), isQuizCompleted: false, }; diff --git a/src/hooks/useQuizState/useSessionStorageState.ts b/src/hooks/useQuizState/useSessionStorageState.ts index 16ef99e3..77ed2209 100644 --- a/src/hooks/useQuizState/useSessionStorageState.ts +++ b/src/hooks/useQuizState/useSessionStorageState.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { getStateFromSessionStorage } from '../../utils'; import { SessionStateOptions } from '../../types'; import { QuizLocalReducerState } from '../../components/CioQuiz/quizLocalReducer'; @@ -11,6 +11,7 @@ const useSessionStorageState = ( enableHydration?: boolean ) => { const quizSessionStorageStateKey = sessionStateOptions?.sessionStateKey || quizSessionStateKey; + const [quizData, setQuizData] = useState(getStateFromSessionStorage(quizSessionStorageStateKey)); // Save state to session storage useEffect(() => { @@ -22,11 +23,10 @@ const useSessionStorageState = ( [quizId]: quizLocalState, }; window?.sessionStorage?.setItem(quizSessionStorageStateKey, JSON.stringify(dataToSave)); + setQuizData(data); } }, [quizLocalState, quizSessionStorageStateKey, enableHydration, quizId]); - const quizData = getStateFromSessionStorage(quizSessionStorageStateKey); - const skipToResults = !!enableHydration && !!quizData?.[quizId]?.isQuizCompleted && diff --git a/src/stories/Quiz/Hooks/Docs/markdown/QuizQuestionsPropGettersDocs.md b/src/stories/Quiz/Hooks/Docs/markdown/QuizQuestionsPropGettersDocs.md index 43ae9062..bf8e0b52 100644 --- a/src/stories/Quiz/Hooks/Docs/markdown/QuizQuestionsPropGettersDocs.md +++ b/src/stories/Quiz/Hooks/Docs/markdown/QuizQuestionsPropGettersDocs.md @@ -106,14 +106,18 @@ This method should be applied to an element of type ` + ); ``` From a9419aa9ef9d86bd5094362126fe9bdaeac80e1e Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 2 Oct 2025 10:34:28 +0530 Subject: [PATCH 16/20] added tests --- .../useJumpToQuestion.test.ts | 40 +++++++++++---- .../useQuizResetClick.server.test.ts | 25 +++++++++ .../useQuizResetClick.test.ts | 51 +++++++++++++++++++ 3 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 spec/hooks/useQuizEvents/useQuizResetClick/useQuizResetClick.server.test.ts create mode 100644 spec/hooks/useQuizEvents/useQuizResetClick/useQuizResetClick.test.ts diff --git a/spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.test.ts b/spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.test.ts index b333a17c..0de58f97 100644 --- a/spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.test.ts +++ b/spec/hooks/useQuizEvents/useJumpToQuestion/useJumpToQuestion.test.ts @@ -7,18 +7,38 @@ import { QuestionTypes, QuizAPIActionTypes } from '../../../../src/components/Ci describe('Testing Hook (client): useJumpToQuestion', () => { const dispatchLocalStateMock = jest.fn(); const dispatchApiStateMock = jest.fn(); - - it('calls dispatchLocalState and dispatchApiState correctly', () => { - const quizApiStateMock = { - quizCurrentQuestion: { - id: '2', - next_question: { - id: '3', - type: 'singleChoice', - }, + const quizApiStateMock = { + quizCurrentQuestion: { + id: 2, + next_question: { + id: 3, + type: 'singleChoice', }, - } as unknown as QuizAPIReducerState; + }, + } as unknown as QuizAPIReducerState; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does not call dispatchLocalState and dispatchApiState', () => { + const { result } = renderHook(() => + useJumpToQuestion({ + quizApiState: quizApiStateMock, + dispatchLocalState: dispatchLocalStateMock, + dispatchApiState: dispatchApiStateMock, + }) + ); + act(() => { + result.current(2); + }); + + expect(dispatchLocalStateMock).not.toHaveBeenCalled(); + expect(dispatchApiStateMock).not.toHaveBeenCalled(); + }); + + it('calls dispatchLocalState and dispatchApiState correctly', () => { const { result } = renderHook(() => useJumpToQuestion({ quizApiState: quizApiStateMock, diff --git a/spec/hooks/useQuizEvents/useQuizResetClick/useQuizResetClick.server.test.ts b/spec/hooks/useQuizEvents/useQuizResetClick/useQuizResetClick.server.test.ts new file mode 100644 index 00000000..61e75334 --- /dev/null +++ b/spec/hooks/useQuizEvents/useQuizResetClick/useQuizResetClick.server.test.ts @@ -0,0 +1,25 @@ +import { renderHookServerSide } from '../../../__tests__/utils.server'; +import useQuizResetClick from '../../../../src/hooks/useQuizEvents/useQuizResetClick'; +import * as factories from '../../../__tests__/factories'; + +describe('Testing Hook (server): useQuizResetClick', () => { + it('initializes without throwing errors', async () => { + const resetQuizSessionStorageStateMock = jest.fn(); + const dispatchLocalStateMock = jest.fn(); + const dispatchApiStateMock = jest.fn(); + + const props: Parameters[0] = { + resetQuizSessionStorageState: resetQuizSessionStorageStateMock, + dispatchLocalState: dispatchLocalStateMock, + dispatchApiState: dispatchApiStateMock, + quizResults: factories.quizResults.build(), + }; + + const executeHook = () => + renderHookServerSide(() => useQuizResetClick(props), { + initialProps: {}, + }); + + expect(executeHook).not.toThrow(); + }); +}); diff --git a/spec/hooks/useQuizEvents/useQuizResetClick/useQuizResetClick.test.ts b/spec/hooks/useQuizEvents/useQuizResetClick/useQuizResetClick.test.ts new file mode 100644 index 00000000..adc58b17 --- /dev/null +++ b/spec/hooks/useQuizEvents/useQuizResetClick/useQuizResetClick.test.ts @@ -0,0 +1,51 @@ +import { act, renderHook } from '@testing-library/react'; + +import * as factories from '../../../__tests__/factories'; + +import useQuizResetClick from '../../../../src/hooks/useQuizEvents/useQuizResetClick'; +import * as useQueryParams from '../../../../src/hooks/useQueryParams'; +import { QuestionTypes, QuizAPIActionTypes } from '../../../../src/components/CioQuiz/actions'; + +describe('Testing Hook (client): useQuizResetClick', () => { + const resetQuizSessionStorageStateMock = jest.fn(); + const dispatchLocalStateMock = jest.fn(); + const dispatchApiStateMock = jest.fn(); + const removeSharedResultsQueryParamsMock = jest.fn(); + const useQueryParamsReturnValue = { + removeSharedResultsQueryParams: removeSharedResultsQueryParamsMock, + isSharedResultsQuery: true, + answers: [], + quizVersionId: '', + queryItems: [], + queryAttributes: [], + }; + const props: Parameters[0] = { + resetQuizSessionStorageState: resetQuizSessionStorageStateMock, + dispatchLocalState: dispatchLocalStateMock, + dispatchApiState: dispatchApiStateMock, + quizResults: factories.quizResults.build(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(useQueryParams, 'default').mockReturnValue(useQueryParamsReturnValue); + }); + + it('should reset the quiz when called', () => { + const { result } = renderHook(() => useQuizResetClick(props)); + + act(() => { + result.current(); + }); + + expect(dispatchLocalStateMock).toHaveBeenCalledWith({ + type: QuestionTypes.Reset, + }); + + expect(dispatchApiStateMock).toHaveBeenCalledWith({ + type: QuizAPIActionTypes.RESET_QUIZ, + }); + expect(resetQuizSessionStorageStateMock).toHaveBeenCalled(); + expect(removeSharedResultsQueryParamsMock).toHaveBeenCalled(); + }); +}); From a2fe67baec7ab1f179b59f21b2ae2d8c6d8dab5f Mon Sep 17 00:00:00 2001 From: vishnu Date: Thu, 2 Oct 2025 13:13:16 +0530 Subject: [PATCH 17/20] customise click event on single select question --- .../useSelectInputProps.test.tsx | 23 ++++++++++++++++++- src/hooks/usePropsGetters/index.ts | 23 +++++++++++++------ .../usePropsGetters/useSelectInputProps.ts | 6 +++-- src/hooks/useQuiz.ts | 7 +++--- src/types.ts | 1 + 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/spec/hooks/usePropsGetters/useSelectInputProps/useSelectInputProps.test.tsx b/spec/hooks/usePropsGetters/useSelectInputProps/useSelectInputProps.test.tsx index 50ffe441..d0209875 100644 --- a/spec/hooks/usePropsGetters/useSelectInputProps/useSelectInputProps.test.tsx +++ b/spec/hooks/usePropsGetters/useSelectInputProps/useSelectInputProps.test.tsx @@ -24,7 +24,9 @@ describe('Testing Hook (client): useSelectInputProps', () => { }); const setupHook = (questionData) => - renderHook(() => useSelectInputProps(quizAnswerChangedMock, nextQuestionMock, questionData)); + renderHook(() => + useSelectInputProps(quizAnswerChangedMock, nextQuestionMock, questionData, answerInputs) + ); it('correctly toggles selected class on click', () => { const { result } = setupHook(currentQuestionData); @@ -80,6 +82,25 @@ describe('Testing Hook (client): useSelectInputProps', () => { expect(nextQuestionMock).not.toHaveBeenCalled(); }); + it('does not advance when configured not to', () => { + currentQuestionData.type = 'single'; + const { result } = renderHook(() => + useSelectInputProps( + quizAnswerChangedMock, + nextQuestionMock, + currentQuestionData, + answerInputs, + false + ) + ); + + act(() => { + result.current(currentQuestionData.options[0]).onClick(mockEvent); + }); + expect(result.current(currentQuestionData.options[0]).className).toContain('selected'); + expect(nextQuestionMock).not.toHaveBeenCalled(); + }); + it('allows toggling options off in a multiple select question', () => { currentQuestionData.type = 'multiple'; const { result } = setupHook(currentQuestionData); diff --git a/src/hooks/usePropsGetters/index.ts b/src/hooks/usePropsGetters/index.ts index 9e1b1ca1..962960e1 100644 --- a/src/hooks/usePropsGetters/index.ts +++ b/src/hooks/usePropsGetters/index.ts @@ -18,6 +18,7 @@ import { GetSkipQuestionButtonProps, GetShareResultsButtonProps, GetJumpToQuestionButtonProps, + QuestionsPageOptions, } from '../../types'; import { QuizAPIReducerState } from '../../components/CioQuiz/quizApiReducer'; import { QuizLocalReducerState } from '../../components/CioQuiz/quizLocalReducer'; @@ -30,12 +31,19 @@ import useAddToFavoritesButtonProps from './useAddToFavoritesButtonProps'; import useSkipQuestionButtonProps from './useSkipQuestionButtonProps'; import useJumpToQuestionButtonProps from './useJumpToQuestionButtonProps'; -const usePropsGetters = ( - quizEvents: QuizEventsReturn, - quizApiState: QuizAPIReducerState, - quizLocalState: QuizLocalReducerState, - favoriteItems?: string[] -) => { +const usePropsGetters = ({ + questionsPageOptions, + favoriteItems, + quizEvents, + quizApiState, + quizLocalState, +}: { + quizEvents: QuizEventsReturn; + quizApiState: QuizAPIReducerState; + quizLocalState: QuizLocalReducerState; + favoriteItems?: string[]; + questionsPageOptions?: QuestionsPageOptions; +}) => { const { quizAnswerChanged, nextQuestion, @@ -65,7 +73,8 @@ const usePropsGetters = ( quizAnswerChanged, nextQuestion, quizApiState.quizCurrentQuestion?.next_question, - quizLocalState.answerInputs + quizLocalState.answerInputs, + questionsPageOptions?.nextQuestionOnSingleSelect ); const getNextQuestionButtonProps: GetNextQuestionButtonProps = useNextQuestionButtonProps( diff --git a/src/hooks/usePropsGetters/useSelectInputProps.ts b/src/hooks/usePropsGetters/useSelectInputProps.ts index 39e0868a..fc629db1 100644 --- a/src/hooks/usePropsGetters/useSelectInputProps.ts +++ b/src/hooks/usePropsGetters/useSelectInputProps.ts @@ -15,7 +15,8 @@ export default function useSelectInputProps( quizAnswerChanged: QuizEventsReturn.QuizAnswerChanged, nextQuestion: QuizEventsReturn.NextQuestion, currentQuestionData?: Nullable, - answerInputs?: AnswerInputState + answerInputs?: AnswerInputState, + nextQuestionOnSingleSelect = true ): GetSelectInputProps { const type: `${QuestionTypes}` | undefined = currentQuestionData?.type; const hasImages = currentQuestionData?.options?.some((option: QuestionOption) => option.images); @@ -112,7 +113,8 @@ export default function useSelectInputProps( if ( (currentQuestionData?.type === QuestionTypes.SingleSelect || currentQuestionData?.type === QuestionTypes.SingleFilterValue) && - singleSelectClicked.current + singleSelectClicked.current && + nextQuestionOnSingleSelect ) { nextQuestion(); } diff --git a/src/hooks/useQuiz.ts b/src/hooks/useQuiz.ts index a5311d1e..db11efab 100644 --- a/src/hooks/useQuiz.ts +++ b/src/hooks/useQuiz.ts @@ -23,12 +23,13 @@ const useQuiz: UseQuiz = (quizOptions) => { // Props getters const { quizApiState, quizLocalState, quizSessionStorageState } = quizState; - const propGetters = usePropsGetters( + const propGetters = usePropsGetters({ quizEvents, quizApiState, quizLocalState, - resultsPageOptions?.favoriteItems - ); + favoriteItems: resultsPageOptions?.favoriteItems, + questionsPageOptions: quizOptions.questionsPageOptions, + }); const primaryColorStyles = usePrimaryColorStyles(primaryColor); diff --git a/src/types.ts b/src/types.ts index a7261fe6..229dddd6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,6 +87,7 @@ export interface ResultsPageOptions { export interface QuestionsPageOptions { skipQuestionButtonText?: string; + nextQuestionOnSingleSelect?: boolean; } export interface SessionStateOptions { From aac2ae126cb4d90a7592cf593669616fbc61fe44 Mon Sep 17 00:00:00 2001 From: Hossam Hindawy Date: Tue, 7 Oct 2025 22:50:50 +0300 Subject: [PATCH 18/20] Add disabled state + Disable button for invalid ids --- ...seJumpToQuestionButtonProps.server.test.ts | 9 +++- .../useJumpToQuestionButtonProps.test.ts | 48 +++++++++++++++++-- src/hooks/usePropsGetters/index.ts | 3 +- .../useJumpToQuestionButtonProps.ts | 21 ++++++-- 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.server.test.ts b/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.server.test.ts index a7dac5bb..5fd5916c 100644 --- a/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.server.test.ts +++ b/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.server.test.ts @@ -1,6 +1,7 @@ import { renderHookServerSide } from '../../../__tests__/utils.server'; import useJumpToQuestionButtonProps from '../../../../src/hooks/usePropsGetters/useJumpToQuestionButtonProps'; import { QuizAPIReducerState } from '../../../../src/components/CioQuiz/quizApiReducer'; +import { QuizLocalReducerState } from '../../../../src/components/CioQuiz/quizLocalReducer'; describe('Testing Hook (server): useJumpToQuestionButtonProps', () => { it('initializes without errors and returns a function that provides button props', () => { @@ -12,8 +13,14 @@ describe('Testing Hook (server): useJumpToQuestionButtonProps', () => { }, } as unknown as QuizAPIReducerState; + const quizLocalStateMock = { + prevAnswerInputs: { + 1: { type: 'open', value: '' }, + }, + } as unknown as QuizLocalReducerState; + const { result } = renderHookServerSide( - () => useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiStateMock), + () => useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiStateMock, quizLocalStateMock), { initialProps: {}, } diff --git a/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.test.ts b/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.test.ts index 194bd1d5..e24a58df 100644 --- a/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.test.ts +++ b/spec/hooks/usePropsGetters/useJumpToQuestionButtonProps/useJumpToQuestionButtonProps.test.ts @@ -1,6 +1,7 @@ import { renderHook, act } from '@testing-library/react'; import useJumpToQuestionButtonProps from '../../../../src/hooks/usePropsGetters/useJumpToQuestionButtonProps'; import { QuizAPIReducerState } from '../../../../src/components/CioQuiz/quizApiReducer'; +import { QuizLocalReducerState } from '../../../../src/components/CioQuiz/quizLocalReducer'; describe('Testing Hook (client): useJumpToQuestionButtonProps', () => { const mockEvent = { preventDefault: jest.fn() } as unknown as React.MouseEvent; @@ -14,13 +15,40 @@ describe('Testing Hook (client): useJumpToQuestionButtonProps', () => { }, } as unknown as QuizAPIReducerState; + const quizLocalStateMock = { + prevAnswerInputs: { + 1: { type: 'open', value: '' }, + }, + } as unknown as QuizLocalReducerState; + const { result } = renderHook(() => - useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState) + useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState, quizLocalStateMock) ); expect(result.current(2).className).toContain('disabled'); }); + it('returns button props with disabled class if an invalid question ID is provided', () => { + const quizApiState = { + quizCurrentQuestion: { + next_question: { id: '2' }, + isCoverQuestion: false, + }, + } as unknown as QuizAPIReducerState; + + const quizLocalStateMock = { + prevAnswerInputs: { + 1: { type: 'open', value: '' }, + }, + } as unknown as QuizLocalReducerState; + + const { result } = renderHook(() => + useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState, quizLocalStateMock) + ); + + expect(result.current(-1).className).toContain('disabled'); + }); + it('returns button props without disabled class if an answer is provided', () => { const quizApiState = { quizCurrentQuestion: { @@ -29,8 +57,15 @@ describe('Testing Hook (client): useJumpToQuestionButtonProps', () => { }, } as unknown as QuizAPIReducerState; + const quizLocalStateMock = { + prevAnswerInputs: { + 1: { type: 'open', value: '' }, + 2: { type: 'open', value: '' }, + }, + } as unknown as QuizLocalReducerState; + const { result } = renderHook(() => - useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState) + useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState, quizLocalStateMock) ); expect(result.current(1).className).not.toContain('disabled'); @@ -44,8 +79,15 @@ describe('Testing Hook (client): useJumpToQuestionButtonProps', () => { }, } as unknown as QuizAPIReducerState; + const quizLocalStateMock = { + prevAnswerInputs: { + 1: { type: 'open', value: '' }, + 2: { type: 'open', value: '' }, + }, + } as unknown as QuizLocalReducerState; + const { result } = renderHook(() => - useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState) + useJumpToQuestionButtonProps(jumpToQuestionMock, quizApiState, quizLocalStateMock) ); expect(typeof result.current(1).onClick).toBe('function'); diff --git a/src/hooks/usePropsGetters/index.ts b/src/hooks/usePropsGetters/index.ts index 962960e1..259c5be7 100644 --- a/src/hooks/usePropsGetters/index.ts +++ b/src/hooks/usePropsGetters/index.ts @@ -90,7 +90,8 @@ const usePropsGetters = ({ const getJumpToQuestionButtonProps: GetJumpToQuestionButtonProps = useJumpToQuestionButtonProps( jumpToQuestion, - quizApiState + quizApiState, + quizLocalState ); const getPreviousQuestionButtonProps: GetPreviousQuestionButtonProps = diff --git a/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts b/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts index 9f8fcb15..027aaea2 100644 --- a/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts +++ b/src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts @@ -1,16 +1,24 @@ import { useCallback } from 'react'; import { QuizAPIReducerState } from '../../components/CioQuiz/quizApiReducer'; import { GetJumpToQuestionButtonProps, QuizEventsReturn } from '../../types'; +import { QuizLocalReducerState } from '../../components/CioQuiz/quizLocalReducer'; export default function useJumpToQuestionButtonProps( jumpToQuestion: QuizEventsReturn.JumpToQuestion, - quizApiState: QuizAPIReducerState + quizApiState: QuizAPIReducerState, + quizLocalState: QuizLocalReducerState ): GetJumpToQuestionButtonProps { const getJumpToQuestionButtonProps: GetJumpToQuestionButtonProps = useCallback( (id: number) => { - const currentQuestionId = quizApiState.quizCurrentQuestion?.next_question?.id; let buttonDisabled; - if (currentQuestionId && id >= currentQuestionId) { + + const currentQuestionId = quizApiState.quizCurrentQuestion?.next_question?.id; + + const answerIds = Object.keys(quizLocalState.prevAnswerInputs); + const isInvalidQuestionId = answerIds && !answerIds.includes(String(id)); + const isCurrentOrFutureQuestionId = currentQuestionId && id >= currentQuestionId; + + if (isInvalidQuestionId || isCurrentOrFutureQuestionId) { buttonDisabled = true; } @@ -19,13 +27,18 @@ export default function useJumpToQuestionButtonProps( tabIndex: buttonDisabled ? -1 : 0, 'aria-disabled': buttonDisabled ? 'true' : 'false', 'aria-describedby': buttonDisabled ? 'jump-to-button-help' : '', + disabled: !!buttonDisabled, type: 'button', onClick: () => { jumpToQuestion(id); }, }; }, - [quizApiState.quizCurrentQuestion, jumpToQuestion] + [ + quizLocalState.prevAnswerInputs, + quizApiState.quizCurrentQuestion?.next_question?.id, + jumpToQuestion, + ] ); return getJumpToQuestionButtonProps; From 6b0ab77ec529b3dc926d35d36807196c310ddccf Mon Sep 17 00:00:00 2001 From: Hossam Hindawy Date: Tue, 7 Oct 2025 23:22:05 +0300 Subject: [PATCH 19/20] Fix missing callbacks --- src/stories/Quiz/Examples/RetrievingAnswers.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stories/Quiz/Examples/RetrievingAnswers.tsx b/src/stories/Quiz/Examples/RetrievingAnswers.tsx index 2aa3d3a1..9bf5a532 100644 --- a/src/stories/Quiz/Examples/RetrievingAnswers.tsx +++ b/src/stories/Quiz/Examples/RetrievingAnswers.tsx @@ -18,6 +18,9 @@ export default function RetrievingAnswersStory() { apiKey, quizId, resultsPageOptions, + callbacks: { + onAddToCartClick: () => {}, + }, }; const quizHook = useQuiz(quizProps); From 642bc36244a7a70923cefc2f94cc2465f294a1d3 Mon Sep 17 00:00:00 2001 From: Hossam Hindawy Date: Tue, 7 Oct 2025 23:28:45 +0300 Subject: [PATCH 20/20] Guard empty callbacks --- src/hooks/useQuizState/useQuizApiState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useQuizState/useQuizApiState.ts b/src/hooks/useQuizState/useQuizApiState.ts index d5ce5d7b..0a2ff1ea 100644 --- a/src/hooks/useQuizState/useQuizApiState.ts +++ b/src/hooks/useQuizState/useQuizApiState.ts @@ -125,7 +125,7 @@ const useQuizApiState: UseQuizApiState = ( return; } - const { onQuizResultsConfigLoaded } = callbacks; + const { onQuizResultsConfigLoaded } = callbacks || {}; if (onQuizResultsConfigLoaded && isFunction(onQuizResultsConfigLoaded)) { onQuizResultsConfigLoaded(quizApiState.resultsConfig, quizApiState.metadata); }