test: deprecate react-test-utils 2/2 (#1751)

This commit is contained in:
Diana Villalvazo
2025-07-28 10:12:32 -06:00
committed by GitHub
parent 06902d8ae8
commit a75c89cd14
6 changed files with 191 additions and 242 deletions

View File

@@ -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,148 +93,147 @@ 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 resizeMessage = (height = 23) => ({
const customHeight = 25;
const defaultHeight = 23;
const resizeMessage = (height = defaultHeight) => ({
data: { type: messageTypes.resize, payload: { height } },
});
const videoFullScreenMessage = (open = false) => ({
data: { type: messageTypes.videoFullScreen, payload: { open } },
});
const testSetIFrameHeight = (height = 23) => {
const testSetIFrameHeight = (height = defaultHeight) => {
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);
it('sets iframe height with payload height', () => {
mockState({ ...defaultStateVals, hasLoaded: true });
renderHook(() => useIFrameBehavior(props));
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage(customHeight));
expect(setIframeHeight).toHaveBeenCalledWith(0);
expect(setIframeHeight).toHaveBeenCalledWith(customHeight);
expect(setIframeHeight).not.toHaveBeenCalledWith(defaultHeight);
});
testOnlySetsHeight();
});
describe('iframeHeight is not 0', () => {
beforeEach(() => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
hook = useIFrameBehavior(props);
});
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(customHeight);
expect(setIframeHeight).not.toHaveBeenCalledWith(defaultHeight);
});
});
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 +247,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 +268,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 +281,9 @@ describe('useIFrameBehavior hook', () => {
});
describe('visibility tracking', () => {
it('sets up visibility tracking after iframe has loaded', () => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
useIFrameBehavior(props);
mockState({ ...defaultStateVals, hasLoaded: true });
const effects = getEffects([true, props.elementId], React);
expect(effects.length).toEqual(2);
effects[0](); // Execute the visibility tracking effect.
renderHook(() => useIFrameBehavior(props));
expect(global.window.addEventListener).toHaveBeenCalledTimes(2);
expect(global.window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
@@ -316,22 +301,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 +323,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 +341,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 +364,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 +383,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 },

View File

@@ -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<number>(val), // eslint-disable-line
hasLoaded: (val) => useState<boolean>(val), // eslint-disable-line
showError: (val) => useState<boolean>(val), // eslint-disable-line
windowTopOffset: (val) => useState<number | null>(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}`,
);

View File

@@ -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');

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);
});
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 false when graded is false', () => {
mockModels(false, true);
const { result } = renderHook(() => useShouldDisplayHonorCode(props));
expect(result.current).toBe(false);
});
it('should return true when both userNeedsIntegritySignature and graded are true', () => {
mockModels(true, true);
const { result } = renderHook(() => useShouldDisplayHonorCode(props));
expect(result.current).toBe(true);
});
});