From 767d4b446bda679209b8caf34ac0bef948ba3098 Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Fri, 4 Jul 2025 12:39:30 -0600 Subject: [PATCH 1/2] test: deprecate react-test-utils --- .../Unit/hooks/useIFrameBehavior.test.js | 221 ++++++++---------- ...IFrameBehavior.js => useIFrameBehavior.ts} | 40 ++-- .../sequence/Unit/hooks/useModalIFrameData.js | 12 +- .../Unit/hooks/useModalIFrameData.test.js | 68 +++--- .../Unit/hooks/useShouldDisplayHonorCode.js | 8 +- .../hooks/useShouldDisplayHonorCode.test.js | 77 ++---- 6 files changed, 185 insertions(+), 241 deletions(-) rename src/courseware/course/sequence/Unit/hooks/{useIFrameBehavior.js => useIFrameBehavior.ts} (81%) diff --git a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js index 3cafa3ab74..3d7d226cc1 100644 --- a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js +++ b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js @@ -1,7 +1,6 @@ -import React from 'react'; import { useDispatch } from 'react-redux'; +import { renderHook } from '@testing-library/react'; -import { getEffects, mockUseKeyedState } from '@edx/react-unit-test-utils'; import { logError } from '@edx/frontend-platform/logging'; import { getConfig } from '@edx/frontend-platform'; @@ -13,7 +12,7 @@ import { useSequenceNavigationMetadata } from '@src/courseware/course/sequence/s import { messageTypes } from '../constants'; -import useIFrameBehavior, { stateKeys } from './useIFrameBehavior'; +import useIFrameBehavior, { iframeBehaviorState } from './useIFrameBehavior'; const mockNavigate = jest.fn(); @@ -25,7 +24,6 @@ jest.mock('@edx/frontend-platform/analytics'); jest.mock('react', () => ({ ...jest.requireActual('react'), - useEffect: jest.fn(), useCallback: jest.fn((cb, prereqs) => ({ cb, prereqs })), })); @@ -34,13 +32,6 @@ jest.mock('react-redux', () => ({ useSelector: jest.fn(), })); -jest.mock('lodash', () => ({ - ...jest.requireActual('lodash'), - throttle: jest.fn((fn) => fn), -})); - -jest.mock('./useLoadBearingHook', () => jest.fn()); - jest.mock('@edx/frontend-platform/logging', () => ({ logError: jest.fn(), })); @@ -65,8 +56,6 @@ jest.mock('react-router-dom', () => ({ jest.mock('@src/courseware/course/sequence/sequence-navigation/hooks'); useSequenceNavigationMetadata.mockReturnValue({ isLastUnit: false, nextLink: '/next-unit-link' }); -const state = mockUseKeyedState(stateKeys); - const props = { elementId: 'test-element-id', id: 'test-id', @@ -104,82 +93,79 @@ const stateVals = { windowTopOffset: 32, }; +const setIframeHeight = jest.fn(); +const setHasLoaded = jest.fn(); +const setShowError = jest.fn(); +const setWindowTopOffset = jest.fn(); + +const mockState = (state) => { + const { iframeHeight, hasLoaded, showError, windowTopOffset } = state; + if ('iframeHeight' in state) jest.spyOn(iframeBehaviorState, 'iframeHeight').mockImplementation(() => [iframeHeight, setIframeHeight]); + if ('hasLoaded' in state) jest.spyOn(iframeBehaviorState, 'hasLoaded').mockImplementation(() => [hasLoaded, setHasLoaded]); + if ('showError' in state) jest.spyOn(iframeBehaviorState, 'showError').mockImplementation(() => [showError, setShowError]); + if ('windowTopOffset' in state) jest.spyOn(iframeBehaviorState, 'windowTopOffset').mockImplementation(() => [windowTopOffset, setWindowTopOffset]); +}; + describe('useIFrameBehavior hook', () => { - let hook; beforeEach(() => { jest.clearAllMocks(); - state.mock(); global.document.getElementById = mockGetElementById; global.window.addEventListener = jest.fn(); global.window.removeEventListener = jest.fn(); global.window.innerHeight = 800; }); - afterEach(() => { - state.resetVals(); - }); describe('behavior', () => { it('initializes iframe height to 0 and error/loaded values to false', () => { - hook = useIFrameBehavior(props); - state.expectInitializedWith(stateKeys.iframeHeight, 0); - state.expectInitializedWith(stateKeys.hasLoaded, false); - state.expectInitializedWith(stateKeys.showError, false); - state.expectInitializedWith(stateKeys.windowTopOffset, null); + mockState(defaultStateVals); + const { result } = renderHook(() => useIFrameBehavior(props)); + + expect(result.current.iframeHeight).toBe(0); + expect(result.current.showError).toBe(false); + expect(result.current.hasLoaded).toBe(false); }); describe('effects - on frame change', () => { let oldGetElement; beforeEach(() => { global.window ??= Object.create(window); Object.defineProperty(window, 'location', { value: {}, writable: true }); - state.mockVals(stateVals); oldGetElement = document.getElementById; document.getElementById = mockGetElementById; + mockState(defaultStateVals); }); afterEach(() => { - state.resetVals(); + jest.clearAllMocks(); document.getElementById = oldGetElement; }); it('does not post url hash if the window does not have one', () => { - hook = useIFrameBehavior(props); - const cb = getEffects([ - props.id, - props.onLoaded, - testIFrameHeight, - true, - ], React)[0]; - cb(); + window.location.hash = ''; + renderHook(() => useIFrameBehavior(props)); expect(postMessage).not.toHaveBeenCalled(); }); it('posts url hash if the window has one', () => { window.location.hash = testHash; - hook = useIFrameBehavior(props); - const cb = getEffects([ - props.id, - props.onLoaded, - testIFrameHeight, - true, - ], React)[0]; - cb(); + renderHook(() => useIFrameBehavior(props)); expect(postMessage).toHaveBeenCalledWith({ hashName: testHash }, config.LMS_BASE_URL); }); }); describe('event listener', () => { it('calls eventListener with prepared callback', () => { - state.mockVals(stateVals); - hook = useIFrameBehavior(props); + mockState(stateVals); + renderHook(() => useIFrameBehavior(props)); const [call] = useEventListener.mock.calls; expect(call[0]).toEqual('message'); expect(call[1].prereqs).toEqual([ props.id, props.onLoaded, - state.values.hasLoaded, - state.setState.hasLoaded, - state.values.iframeHeight, - state.setState.iframeHeight, - state.values.windowTopOffset, - state.setState.windowTopOffset, + stateVals.hasLoaded, + setHasLoaded, + stateVals.iframeHeight, + setIframeHeight, + stateVals.windowTopOffset, + setWindowTopOffset, ]); }); describe('resize message', () => { + const height = 23; const resizeMessage = (height = 23) => ({ data: { type: messageTypes.resize, payload: { height } }, }); @@ -189,63 +175,60 @@ describe('useIFrameBehavior hook', () => { const testSetIFrameHeight = (height = 23) => { const { cb } = useEventListener.mock.calls[0][1]; cb(resizeMessage(height)); - expect(state.setState.iframeHeight).toHaveBeenCalledWith(height); - }; - const testOnlySetsHeight = () => { - it('sets iframe height with payload height', () => { - testSetIFrameHeight(); - }); - it('does not set hasLoaded', () => { - expect(state.setState.hasLoaded).not.toHaveBeenCalled(); - }); + expect(setIframeHeight).toHaveBeenCalledWith(height); }; describe('hasLoaded', () => { - beforeEach(() => { - state.mockVals({ ...defaultStateVals, hasLoaded: true }); - hook = useIFrameBehavior(props); - }); - testOnlySetsHeight(); - }); - describe('iframeHeight is not 0', () => { - beforeEach(() => { - state.mockVals({ ...defaultStateVals, hasLoaded: true }); - hook = useIFrameBehavior(props); + it('sets iframe height with payload height', () => { + mockState({ ...defaultStateVals, hasLoaded: true }); + renderHook(() => useIFrameBehavior(props)); + const { cb } = useEventListener.mock.calls[0][1]; + cb(resizeMessage(height)); + expect(setIframeHeight).toHaveBeenCalledWith(0); + expect(setIframeHeight).toHaveBeenCalledWith(height); }); - testOnlySetsHeight(); }); describe('payload height is 0', () => { - beforeEach(() => { hook = useIFrameBehavior(props); }); - testOnlySetsHeight(0); + it('sets iframe height with payload height', () => { + mockState(defaultStateVals); + renderHook(() => useIFrameBehavior(props)); + const { cb } = useEventListener.mock.calls[0][1]; + cb(resizeMessage(0)); + expect(setIframeHeight).toHaveBeenCalledWith(0); + expect(setIframeHeight).not.toHaveBeenCalledWith(height); + }); }); describe('payload is present but uninitialized', () => { + beforeEach(() => { + mockState(defaultStateVals); + }); it('sets iframe height with payload height', () => { - hook = useIFrameBehavior(props); + renderHook(() => useIFrameBehavior(props)); testSetIFrameHeight(); }); it('sets hasLoaded and calls onLoaded', () => { - hook = useIFrameBehavior(props); + renderHook(() => useIFrameBehavior(props)); const { cb } = useEventListener.mock.calls[0][1]; cb(resizeMessage()); - expect(state.setState.hasLoaded).toHaveBeenCalledWith(true); + expect(setHasLoaded).toHaveBeenCalledWith(true); expect(props.onLoaded).toHaveBeenCalled(); }); test('onLoaded is optional', () => { - hook = useIFrameBehavior({ ...props, onLoaded: undefined }); + renderHook(() => useIFrameBehavior({ ...props, onLoaded: undefined })); const { cb } = useEventListener.mock.calls[0][1]; cb(resizeMessage()); - expect(state.setState.hasLoaded).toHaveBeenCalledWith(true); + expect(setHasLoaded).toHaveBeenCalledWith(true); }); }); it('scrolls to current window vertical offset if one is set', () => { const windowTopOffset = 32; - state.mockVals({ ...defaultStateVals, windowTopOffset }); - hook = useIFrameBehavior(props); + mockState({ ...defaultStateVals, windowTopOffset }); + renderHook(() => useIFrameBehavior(props)); const { cb } = useEventListener.mock.calls[0][1]; cb(videoFullScreenMessage()); expect(window.scrollTo).toHaveBeenCalledWith(0, windowTopOffset); }); it('does not scroll if towverticalp offset is not set', () => { - hook = useIFrameBehavior(props); + renderHook(() => useIFrameBehavior(props)); const { cb } = useEventListener.mock.calls[0][1]; cb(resizeMessage()); expect(window.scrollTo).not.toHaveBeenCalled(); @@ -259,16 +242,16 @@ describe('useIFrameBehavior hook', () => { }); beforeEach(() => { window.scrollY = scrollY; - hook = useIFrameBehavior(props); + renderHook(() => useIFrameBehavior(props)); [[, { cb }]] = useEventListener.mock.calls; }); it('sets window top offset based on window.scrollY if opening the video', () => { cb(fullScreenMessage(true)); - expect(state.setState.windowTopOffset).toHaveBeenCalledWith(scrollY); + expect(setWindowTopOffset).toHaveBeenCalledWith(scrollY); }); it('sets window top offset to null if closing the video', () => { cb(fullScreenMessage(false)); - expect(state.setState.windowTopOffset).toHaveBeenCalledWith(null); + expect(setWindowTopOffset).toHaveBeenCalledWith(null); }); }); describe('offset message', () => { @@ -280,7 +263,7 @@ describe('useIFrameBehavior hook', () => { document.getElementById = mockGetEl; const oldScrollTo = window.scrollTo; window.scrollTo = jest.fn(); - hook = useIFrameBehavior(props); + renderHook(() => useIFrameBehavior(props)); const { cb } = useEventListener.mock.calls[0][1]; const offset = 99; cb({ data: { offset } }); @@ -293,12 +276,9 @@ describe('useIFrameBehavior hook', () => { }); describe('visibility tracking', () => { it('sets up visibility tracking after iframe has loaded', () => { - state.mockVals({ ...defaultStateVals, hasLoaded: true }); - useIFrameBehavior(props); - - const effects = getEffects([true, props.elementId], React); - expect(effects.length).toEqual(2); - effects[0](); // Execute the visibility tracking effect. + mockState({ ...defaultStateVals, hasLoaded: true }); + + renderHook(() => useIFrameBehavior(props)); expect(global.window.addEventListener).toHaveBeenCalledTimes(2); expect(global.window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); @@ -316,22 +296,18 @@ describe('useIFrameBehavior hook', () => { ); }); it('does not set up visibility tracking before iframe has loaded', () => { - state.mockVals({ ...defaultStateVals, hasLoaded: false }); - useIFrameBehavior(props); - - const effects = getEffects([false, props.elementId], React); - expect(effects).toBeNull(); + window.location.hash = ''; // Avoid posting hash message. + mockState({ ...defaultStateVals, hasLoaded: false }); + renderHook(() => useIFrameBehavior(props)); expect(global.window.addEventListener).not.toHaveBeenCalled(); expect(postMessage).not.toHaveBeenCalled(); }); it('cleans up event listeners on unmount', () => { - state.mockVals({ ...defaultStateVals, hasLoaded: true }); - useIFrameBehavior(props); + mockState({ ...defaultStateVals, hasLoaded: true }); + const { unmount } = renderHook(() => useIFrameBehavior(props)); - const effects = getEffects([true, props.elementId], React); - const cleanup = effects[0](); // Execute the effect and get the cleanup function. - cleanup(); // Call the cleanup function. + unmount(); // Call the cleanup function. expect(global.window.removeEventListener).toHaveBeenCalledTimes(2); expect(global.window.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); @@ -342,14 +318,16 @@ describe('useIFrameBehavior hook', () => { describe('output', () => { describe('handleIFrameLoad', () => { it('sets and logs error if has not loaded', () => { - hook = useIFrameBehavior(props); - hook.handleIFrameLoad(); - expect(state.setState.showError).toHaveBeenCalledWith(true); + mockState(defaultStateVals); + const { result } = renderHook(() => useIFrameBehavior(props)); + result.current.handleIFrameLoad(); + expect(setShowError).toHaveBeenCalledWith(true); expect(logError).toHaveBeenCalled(); }); it('sends track event if has not loaded', () => { - hook = useIFrameBehavior(props); - hook.handleIFrameLoad(); + mockState(defaultStateVals); + const { result } = renderHook(() => useIFrameBehavior(props)); + result.current.handleIFrameLoad(); const eventName = 'edx.bi.error.learning.iframe_load_failed'; const eventProperties = { unitId: props.id, @@ -358,21 +336,22 @@ describe('useIFrameBehavior hook', () => { expect(sendTrackEvent).toHaveBeenCalledWith(eventName, eventProperties); }); it('does not set/log errors if loaded', () => { - state.mockVals({ ...defaultStateVals, hasLoaded: true }); - hook = useIFrameBehavior(props); - hook.handleIFrameLoad(); - expect(state.setState.showError).not.toHaveBeenCalled(); + mockState({ ...defaultStateVals, hasLoaded: true }); + const { result } = renderHook(() => useIFrameBehavior(props)); + result.current.handleIFrameLoad(); + expect(setShowError).not.toHaveBeenCalled(); expect(logError).not.toHaveBeenCalled(); }); it('does not send track event if loaded', () => { - state.mockVals({ ...defaultStateVals, hasLoaded: true }); - hook = useIFrameBehavior(props); - hook.handleIFrameLoad(); + mockState({ ...defaultStateVals, hasLoaded: true }); + const { result } = renderHook(() => useIFrameBehavior(props)); + result.current.handleIFrameLoad(); expect(sendTrackEvent).not.toHaveBeenCalled(); }); it('registers an event handler to process fetchCourse events.', () => { - hook = useIFrameBehavior(props); - hook.handleIFrameLoad(); + mockState(defaultStateVals); + const { result } = renderHook(() => useIFrameBehavior(props)); + result.current.handleIFrameLoad(); const eventName = 'test-event-name'; const event = { data: { event_name: eventName } }; window.onmessage(event); @@ -380,16 +359,17 @@ describe('useIFrameBehavior hook', () => { }); }); it('forwards handleIframeLoad, showError, and hasLoaded from state fields', () => { - state.mockVals(stateVals); - hook = useIFrameBehavior(props); - expect(hook.iframeHeight).toEqual(stateVals.iframeHeight); - expect(hook.showError).toEqual(stateVals.showError); - expect(hook.hasLoaded).toEqual(stateVals.hasLoaded); + mockState(stateVals); + const { result } = renderHook(() => useIFrameBehavior(props)); + expect(result.current.iframeHeight).toBe(stateVals.iframeHeight); + expect(result.current.showError).toBe(stateVals.showError); + expect(result.current.hasLoaded).toBe(stateVals.hasLoaded); }); }); describe('navigate link for the next unit on auto advance', () => { it('test for link when it is not last unit', () => { - hook = useIFrameBehavior(props); + mockState(defaultStateVals); + renderHook(() => useIFrameBehavior(props)); const { cb } = useEventListener.mock.calls[0][1]; const autoAdvanceMessage = () => ({ data: { type: messageTypes.autoAdvance }, @@ -398,9 +378,10 @@ describe('useIFrameBehavior hook', () => { expect(mockNavigate).toHaveBeenCalledWith('/next-unit-link'); }); it('test for link when it is last unit', () => { + mockState(defaultStateVals); useSequenceNavigationMetadata.mockReset(); useSequenceNavigationMetadata.mockReturnValue({ isLastUnit: true, nextLink: '/next-unit-link' }); - hook = useIFrameBehavior(props); + renderHook(() => useIFrameBehavior(props)); const { cb } = useEventListener.mock.calls[0][1]; const autoAdvanceMessage = () => ({ data: { type: messageTypes.autoAdvance }, diff --git a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.js b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts similarity index 81% rename from src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.js rename to src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts index 3ff83fcc11..9e63015b93 100644 --- a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.js +++ b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts @@ -1,11 +1,10 @@ +import React, { useState } from 'react'; import { getConfig } from '@edx/frontend-platform'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { throttle } from 'lodash'; -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; import { logError } from '@edx/frontend-platform/logging'; import { fetchCourse } from '@src/courseware/data'; @@ -18,13 +17,12 @@ import { messageTypes } from '../constants'; import useLoadBearingHook from './useLoadBearingHook'; -export const stateKeys = StrictDict({ - iframeHeight: 'iframeHeight', - hasLoaded: 'hasLoaded', - showError: 'showError', - windowTopOffset: 'windowTopOffset', - sequences: 'sequences', -}); +export const iframeBehaviorState = { + iframeHeight: (val) => useState(val), // eslint-disable-line + hasLoaded: (val) => useState(val), // eslint-disable-line + showError: (val) => useState(val), // eslint-disable-line + windowTopOffset: (val) => useState(val), // eslint-disable-line +} as const; const useIFrameBehavior = ({ elementId, @@ -38,27 +36,27 @@ const useIFrameBehavior = ({ const dispatch = useDispatch(); const activeSequenceId = useSelector(getSequenceId); const navigate = useNavigate(); - const activeSequence = useModel(stateKeys.sequences, activeSequenceId); + const activeSequence = useModel('sequences', activeSequenceId); const activeUnitId = activeSequence.unitIds.length > 0 ? activeSequence.unitIds[activeSequence.activeUnitIndex] : null; const { isLastUnit, nextLink } = useSequenceNavigationMetadata(activeSequenceId, activeUnitId); - const [iframeHeight, setIframeHeight] = useKeyedState(stateKeys.iframeHeight, 0); - const [hasLoaded, setHasLoaded] = useKeyedState(stateKeys.hasLoaded, false); - const [showError, setShowError] = useKeyedState(stateKeys.showError, false); - const [windowTopOffset, setWindowTopOffset] = useKeyedState(stateKeys.windowTopOffset, null); + const [iframeHeight, setIframeHeight] = iframeBehaviorState.iframeHeight(0); + const [hasLoaded, setHasLoaded] = iframeBehaviorState.hasLoaded(false); + const [showError, setShowError] = iframeBehaviorState.showError(false); + const [windowTopOffset, setWindowTopOffset] = iframeBehaviorState.windowTopOffset(null); React.useEffect(() => { - const frame = document.getElementById(elementId); + const frame = document.getElementById(elementId) as HTMLIFrameElement | null; const { hash } = window.location; if (hash) { // The url hash will be sent to LMS-served iframe in order to find the location of the // hash within the iframe. - frame.contentWindow.postMessage({ hashName: hash }, `${getConfig().LMS_BASE_URL}`); + frame?.contentWindow?.postMessage({ hashName: hash }, `${getConfig().LMS_BASE_URL}`); } }, [id, onLoaded, iframeHeight, hasLoaded]); - const receiveMessage = React.useCallback(({ data }) => { + const receiveMessage = React.useCallback(({ data }: MessageEvent) => { const { type, payload } = data; if (type === messageTypes.resize) { setIframeHeight(payload.height); @@ -82,11 +80,11 @@ const useIFrameBehavior = ({ } else if (data.offset) { // We listen for this message from LMS to know when the page needs to // be scrolled to another location on the page. - window.scrollTo(0, data.offset + document.getElementById('unit-iframe').offsetTop); + window.scrollTo(0, data.offset + document.getElementById('unit-iframe')!.offsetTop); } else if (type === messageTypes.autoAdvance) { // We are listening to autoAdvance message to move to next sequence automatically. // In case it is the last unit we need not do anything. - if (!isLastUnit) { + if (!isLastUnit && nextLink) { navigate(nextLink); } } @@ -109,7 +107,7 @@ const useIFrameBehavior = ({ return undefined; } - const iframeElement = document.getElementById(elementId); + const iframeElement = document.getElementById(elementId) as HTMLIFrameElement | null; if (!iframeElement || !iframeElement.contentWindow) { return undefined; } @@ -123,7 +121,7 @@ const useIFrameBehavior = ({ viewportHeight: window.innerHeight, }, }; - iframeElement.contentWindow.postMessage( + iframeElement?.contentWindow?.postMessage( visibleInfo, `${getConfig().LMS_BASE_URL}`, ); diff --git a/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.js b/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.js index 513b0b7636..424b4ba3be 100644 --- a/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.js +++ b/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.js @@ -1,19 +1,11 @@ import React from 'react'; - -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils/dist'; - import { useEventListener } from '@src/generic/hooks'; -export const stateKeys = StrictDict({ - isOpen: 'isOpen', - options: 'options', -}); - export const DEFAULT_HEIGHT = '100%'; const useModalIFrameData = () => { - const [isOpen, setIsOpen] = useKeyedState(stateKeys.isOpen, false); - const [options, setOptions] = useKeyedState(stateKeys.options, { height: DEFAULT_HEIGHT }); + const [isOpen, setIsOpen] = React.useState(false); + const [options, setOptions] = React.useState({ height: DEFAULT_HEIGHT }); const handleModalClose = () => { const rootFrame = document.querySelector('iframe'); diff --git a/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.test.js b/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.test.js index 99e886f113..29068a1797 100644 --- a/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.test.js +++ b/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.test.js @@ -1,74 +1,85 @@ -import { mockUseKeyedState } from '@edx/react-unit-test-utils'; +import React from 'react'; +import { renderHook } from '@testing-library/react'; import { useEventListener } from '@src/generic/hooks'; import { messageTypes } from '../constants'; -import useModalIFrameData, { stateKeys, DEFAULT_HEIGHT } from './useModalIFrameData'; +import useModalIFrameData, { DEFAULT_HEIGHT } from './useModalIFrameData'; jest.mock('react', () => ({ ...jest.requireActual('react'), useCallback: jest.fn((cb, prereqs) => ({ cb, prereqs })), + useState: jest.fn((initialValue) => [initialValue, jest.fn()]), })); jest.mock('@src/generic/hooks', () => ({ useEventListener: jest.fn(), })); -const state = mockUseKeyedState(stateKeys); +const setIsOpen = jest.fn(); +const setOptions = jest.fn(); + +const defaultState = { + isOpen: false, + options: { height: DEFAULT_HEIGHT }, +}; + +const mockUseStateWithValues = (values) => { + jest.spyOn(React, 'useState') + .mockReturnValueOnce([values.isOpen, setIsOpen]) + .mockReturnValueOnce([values.options, setOptions]); +}; describe('useModalIFrameData', () => { beforeEach(() => { jest.clearAllMocks(); - state.mock(); }); const testHandleModalClose = ({ trigger }) => { const postMessage = jest.fn(); document.querySelector = jest.fn().mockReturnValue({ contentWindow: { postMessage } }); trigger(); - state.expectSetStateCalledWith(stateKeys.isOpen, false); + expect(React.useState).toHaveBeenNthCalledWith(1, false); expect(postMessage).toHaveBeenCalledWith({ type: 'plugin.modal-close' }, '*'); }; describe('behavior', () => { - it('initializes isOpen to false', () => { - useModalIFrameData(); - state.expectInitializedWith(stateKeys.isOpen, false); - }); - it('initializes options with default height', () => { - useModalIFrameData(); - state.expectInitializedWith(stateKeys.options, { height: DEFAULT_HEIGHT }); + it('should initialize with modal closed and default height', () => { + const { result } = renderHook(() => useModalIFrameData()); + + expect(result.current.modalOptions).toEqual({ + isOpen: false, + height: DEFAULT_HEIGHT, + }); }); describe('eventListener', () => { const oldOptions = { some: 'old', options: 'yeah' }; const prepareListener = () => { - useModalIFrameData(); expect(useEventListener).toHaveBeenCalled(); const call = useEventListener.mock.calls[0][1]; expect(call.prereqs).toEqual([]); return call.cb; }; it('consumes modal events and opens sets modal options with open: true', () => { - state.mockVals({ - [stateKeys.isOpen]: false, - [stateKeys.options]: oldOptions, + mockUseStateWithValues({ + isOpen: false, + options: oldOptions, }); + renderHook(() => useModalIFrameData()); const receiveMessage = prepareListener(); const payload = { test: 'values' }; receiveMessage({ data: { type: messageTypes.modal, payload } }); - expect(state.setState.isOpen).toHaveBeenCalledWith(true); - expect(state.setState.options).toHaveBeenCalled(); - const [[setOptionsCb]] = state.setState.options.mock.calls; + expect(setIsOpen).toHaveBeenCalledWith(true); + expect(setOptions).toHaveBeenCalled(); + const [[setOptionsCb]] = setOptions.mock.calls; expect(setOptionsCb(oldOptions)).toEqual({ ...oldOptions, ...payload }); }); it('ignores events with no type', () => { - state.mockVals({ - [stateKeys.isOpen]: false, - [stateKeys.options]: oldOptions, - }); + const { result } = renderHook(() => useModalIFrameData()); + const initialState = result.current.modalOptions; const receiveMessage = prepareListener(); const payload = { test: 'values' }; receiveMessage({ data: { payload } }); - expect(state.setState.isOpen).not.toHaveBeenCalled(); - expect(state.setState.options).not.toHaveBeenCalled(); + expect(result.current.modalOptions).toEqual(initialState); }); it('calls handleModalClose behavior when receiving a "plugin.modal-close" event', () => { + renderHook(() => useModalIFrameData()); const receiveMessage = prepareListener(); testHandleModalClose({ trigger: () => { @@ -80,13 +91,14 @@ describe('useModalIFrameData', () => { }); describe('output', () => { test('returns handleModalClose callback', () => { + mockUseStateWithValues(defaultState); testHandleModalClose({ trigger: useModalIFrameData().handleModalClose }); }); it('forwards modalOptions from state values', () => { const modalOptions = { test: 'options' }; - state.mockVals({ - [stateKeys.options]: modalOptions, - [stateKeys.isOpen]: true, + mockUseStateWithValues({ + isOpen: true, + options: modalOptions, }); expect(useModalIFrameData().modalOptions).toEqual({ ...modalOptions, diff --git a/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.js b/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.js index 960944c60f..a190b62df5 100644 --- a/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.js +++ b/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.js @@ -1,19 +1,13 @@ import React from 'react'; - -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils/dist'; import { useModel } from '@src/generic/model-store'; import { modelKeys } from '../constants'; -export const stateKeys = StrictDict({ - shouldDisplay: 'shouldDisplay', -}); - /** * @return {bool} should the honor code be displayed? */ const useShouldDisplayHonorCode = ({ id, courseId }) => { - const [shouldDisplay, setShouldDisplay] = useKeyedState(stateKeys.shouldDisplay, false); + const [shouldDisplay, setShouldDisplay] = React.useState(false); const { graded } = useModel(modelKeys.units, id); const { userNeedsIntegritySignature } = useModel(modelKeys.coursewareMeta, courseId); diff --git a/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.test.js b/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.test.js index af4aac568d..19ecf5adb1 100644 --- a/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.test.js +++ b/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.test.js @@ -1,22 +1,12 @@ -import React from 'react'; - -import { getEffects, mockUseKeyedState } from '@edx/react-unit-test-utils'; +import { renderHook } from '@testing-library/react'; import { useModel } from '@src/generic/model-store'; - +import useShouldDisplayHonorCode from './useShouldDisplayHonorCode'; import { modelKeys } from '../constants'; -import useShouldDisplayHonorCode, { stateKeys } from './useShouldDisplayHonorCode'; - -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useEffect: jest.fn(), -})); jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn(), })); -const state = mockUseKeyedState(stateKeys); - const props = { id: 'test-id', courseId: 'test-course-id', @@ -28,52 +18,29 @@ const mockModels = (graded, userNeedsIntegritySignature) => { )); }; -describe('useShouldDisplayHonorCode hook', () => { +describe('useShouldDisplayHonorCode', () => { beforeEach(() => { jest.clearAllMocks(); - mockModels(false, false); - state.mock(); }); - describe('behavior', () => { - it('initializes shouldDisplay to false', () => { - useShouldDisplayHonorCode(props); - state.expectInitializedWith(stateKeys.shouldDisplay, false); - }); - describe('effect - on userNeedsIntegritySignature', () => { - describe('graded and needs integrity signature', () => { - it('sets shouldDisplay(true)', () => { - mockModels(true, true); - useShouldDisplayHonorCode(props); - const cb = getEffects([state.setState.shouldDisplay, true], React)[0]; - cb(); - expect(state.setState.shouldDisplay).toHaveBeenCalledWith(true); - }); - }); - describe('not graded', () => { - it('sets should not display', () => { - mockModels(true, false); - useShouldDisplayHonorCode(props); - const cb = getEffects([state.setState.shouldDisplay, false], React)[0]; - cb(); - expect(state.setState.shouldDisplay).toHaveBeenCalledWith(false); - }); - }); - describe('does not need integrity signature', () => { - it('sets should not display', () => { - mockModels(false, true); - useShouldDisplayHonorCode(props); - const cb = getEffects([state.setState.shouldDisplay, true], React)[0]; - cb(); - expect(state.setState.shouldDisplay).toHaveBeenCalledWith(false); - }); - }); - }); + + it('should return false when userNeedsIntegritySignature is false', () => { + mockModels(true, false); + + const { result } = renderHook(() => useShouldDisplayHonorCode(props)); + expect(result.current).toBe(false); + }); + + it('should return false when graded is false', () => { + mockModels(false, true); + + const { result } = renderHook(() => useShouldDisplayHonorCode(props)); + expect(result.current).toBe(false); }); - describe('output', () => { - it('returns shouldDisplay value from state', () => { - const testValue = 'test-value'; - state.mockVal(stateKeys.shouldDisplay, testValue); - expect(useShouldDisplayHonorCode(props)).toEqual(testValue); - }); + + it('should return true when both userNeedsIntegritySignature and graded are true', () => { + mockModels(true, true); + + const { result } = renderHook(() => useShouldDisplayHonorCode(props)); + expect(result.current).toBe(true); }); }); From 3eb9fa3ea8f217bc9c7106e74f3fbd70fc926be4 Mon Sep 17 00:00:00 2001 From: diana-villalvazo-wgu Date: Mon, 7 Jul 2025 09:38:52 -0500 Subject: [PATCH 2/2] chore: lint fixes --- .../Unit/hooks/useIFrameBehavior.test.js | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js index 3d7d226cc1..0af162dfa7 100644 --- a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js +++ b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js @@ -99,11 +99,13 @@ const setShowError = jest.fn(); const setWindowTopOffset = jest.fn(); const mockState = (state) => { - const { iframeHeight, hasLoaded, showError, windowTopOffset } = state; - if ('iframeHeight' in state) jest.spyOn(iframeBehaviorState, 'iframeHeight').mockImplementation(() => [iframeHeight, setIframeHeight]); - if ('hasLoaded' in state) jest.spyOn(iframeBehaviorState, 'hasLoaded').mockImplementation(() => [hasLoaded, setHasLoaded]); - if ('showError' in state) jest.spyOn(iframeBehaviorState, 'showError').mockImplementation(() => [showError, setShowError]); - if ('windowTopOffset' in state) jest.spyOn(iframeBehaviorState, 'windowTopOffset').mockImplementation(() => [windowTopOffset, setWindowTopOffset]); + const { + iframeHeight, hasLoaded, showError, windowTopOffset, + } = state; + if ('iframeHeight' in state) { jest.spyOn(iframeBehaviorState, 'iframeHeight').mockImplementation(() => [iframeHeight, setIframeHeight]); } + if ('hasLoaded' in state) { jest.spyOn(iframeBehaviorState, 'hasLoaded').mockImplementation(() => [hasLoaded, setHasLoaded]); } + if ('showError' in state) { jest.spyOn(iframeBehaviorState, 'showError').mockImplementation(() => [showError, setShowError]); } + if ('windowTopOffset' in state) { jest.spyOn(iframeBehaviorState, 'windowTopOffset').mockImplementation(() => [windowTopOffset, setWindowTopOffset]); } }; describe('useIFrameBehavior hook', () => { @@ -165,7 +167,7 @@ describe('useIFrameBehavior hook', () => { ]); }); describe('resize message', () => { - const height = 23; + const customHeight = 23; const resizeMessage = (height = 23) => ({ data: { type: messageTypes.resize, payload: { height } }, }); @@ -182,9 +184,9 @@ describe('useIFrameBehavior hook', () => { mockState({ ...defaultStateVals, hasLoaded: true }); renderHook(() => useIFrameBehavior(props)); const { cb } = useEventListener.mock.calls[0][1]; - cb(resizeMessage(height)); + cb(resizeMessage(customHeight)); expect(setIframeHeight).toHaveBeenCalledWith(0); - expect(setIframeHeight).toHaveBeenCalledWith(height); + expect(setIframeHeight).toHaveBeenCalledWith(customHeight); }); }); describe('payload height is 0', () => { @@ -194,7 +196,7 @@ describe('useIFrameBehavior hook', () => { const { cb } = useEventListener.mock.calls[0][1]; cb(resizeMessage(0)); expect(setIframeHeight).toHaveBeenCalledWith(0); - expect(setIframeHeight).not.toHaveBeenCalledWith(height); + expect(setIframeHeight).not.toHaveBeenCalledWith(customHeight); }); }); describe('payload is present but uninitialized', () => { @@ -277,7 +279,7 @@ describe('useIFrameBehavior hook', () => { describe('visibility tracking', () => { it('sets up visibility tracking after iframe has loaded', () => { mockState({ ...defaultStateVals, hasLoaded: true }); - + renderHook(() => useIFrameBehavior(props)); expect(global.window.addEventListener).toHaveBeenCalledTimes(2);