diff --git a/package-lock.json b/package-lock.json
index d4c9360d37..85a94eacd9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,6 @@
"@edx/frontend-lib-special-exams": "^4.0.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.7.0",
- "@edx/react-unit-test-utils": "^4.0.0",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
@@ -70,6 +69,7 @@
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz",
"integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@ampproject/remapping": {
@@ -2602,30 +2602,6 @@
"atlas": "atlas"
}
},
- "node_modules/@edx/react-unit-test-utils": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@edx/react-unit-test-utils/-/react-unit-test-utils-4.0.0.tgz",
- "integrity": "sha512-QlVYhYD9L2bzx1eAtf8BbCJr00ek9rrHrG+/pW2bVSt+t0uvKHQpX1CNdMrDePv18DsMeC7IOB00t8ZIn4mi7w==",
- "license": "AGPL-3.0",
- "dependencies": {
- "@edx/browserslist-config": "^1.1.1",
- "@reduxjs/toolkit": "^1.5.1",
- "@testing-library/dom": "^10.4.0",
- "@testing-library/jest-dom": "^6.6.3",
- "@testing-library/react": "^16.2.0",
- "classnames": "^2.2.6",
- "core-js": "3.6.5",
- "lodash": "^4.17.21",
- "react-dev-utils": "^12.0.1",
- "react-test-renderer": "^18.3.1"
- },
- "peerDependencies": {
- "@edx/frontend-platform": "^8.3.1",
- "@openedx/frontend-build": "^14.3.0",
- "@openedx/paragon": "^22.0.0 || ^23.0.0",
- "react": "^18.0.0"
- }
- },
"node_modules/@edx/reactifex": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-2.2.0.tgz",
@@ -5552,7 +5528,9 @@
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
+ "dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -5571,6 +5549,7 @@
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
"integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
@@ -5591,6 +5570,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -5604,12 +5584,14 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@testing-library/react": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
"integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
@@ -5705,7 +5687,9 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
- "license": "MIT"
+ "dev": true,
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -9094,18 +9078,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/core-js": {
- "version": "3.6.5",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
- "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==",
- "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
- "hasInstallScript": true,
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
- }
- },
"node_modules/core-js-compat": {
"version": "3.41.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
@@ -9336,6 +9308,7 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
"license": "MIT"
},
"node_modules/cssesc": {
@@ -9973,7 +9946,9 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
- "license": "MIT"
+ "dev": true,
+ "license": "MIT",
+ "peer": true
},
"node_modules/dom-converter": {
"version": "0.2.0",
@@ -13421,6 +13396,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -15999,7 +15975,9 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -16753,6 +16731,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -18988,7 +18967,9 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -19002,7 +18983,9 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -19014,7 +18997,9 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
- "license": "MIT"
+ "dev": true,
+ "license": "MIT",
+ "peer": true
},
"node_modules/process": {
"version": "0.11.10",
@@ -19974,19 +19959,6 @@
"react-dom": ">=16.8"
}
},
- "node_modules/react-shallow-renderer": {
- "version": "16.15.0",
- "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
- "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==",
- "license": "MIT",
- "dependencies": {
- "object-assign": "^4.1.1",
- "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
- },
- "peerDependencies": {
- "react": "^16.0.0 || ^17.0.0 || ^18.0.0"
- }
- },
"node_modules/react-share": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/react-share/-/react-share-4.4.1.tgz",
@@ -20048,26 +20020,6 @@
"react": "^16.8.3 || ^17.0.0-0 || ^18.0.0"
}
},
- "node_modules/react-test-renderer": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz",
- "integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==",
- "license": "MIT",
- "dependencies": {
- "react-is": "^18.3.1",
- "react-shallow-renderer": "^16.15.0",
- "scheduler": "^0.23.2"
- },
- "peerDependencies": {
- "react": "^18.3.1"
- }
- },
- "node_modules/react-test-renderer/node_modules/react-is": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
- "license": "MIT"
- },
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -20220,6 +20172,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"indent-string": "^4.0.0",
@@ -21965,6 +21918,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"min-indent": "^1.0.0"
diff --git a/package.json b/package.json
index 853b42f36d..d3d3791dc0 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,6 @@
"@edx/frontend-lib-special-exams": "^4.0.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.7.0",
- "@edx/react-unit-test-utils": "^4.0.0",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
diff --git a/src/courseware/course/sequence/Unit/ContentIFrame.jsx b/src/courseware/course/sequence/Unit/ContentIFrame.jsx
index 8be355bf14..bc77182506 100644
--- a/src/courseware/course/sequence/Unit/ContentIFrame.jsx
+++ b/src/courseware/course/sequence/Unit/ContentIFrame.jsx
@@ -1,8 +1,6 @@
import PropTypes from 'prop-types';
-import React from 'react';
import { ErrorPage } from '@edx/frontend-platform/react';
-import { StrictDict } from '@edx/react-unit-test-utils';
import { ModalDialog } from '@openedx/paragon';
import { ContentIFrameLoaderSlot } from '../../../../plugin-slots/ContentIFrameLoaderSlot';
@@ -22,10 +20,10 @@ export const IFRAME_FEATURE_POLICY = (
'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *; autoplay *'
);
-export const testIDs = StrictDict({
+export const testIDs = {
contentIFrame: 'content-iframe-test-id',
modalIFrame: 'modal-iframe-test-id',
-});
+};
const ContentIFrame = ({
iframeUrl,
diff --git a/src/courseware/course/sequence/Unit/ContentIFrame.test.jsx b/src/courseware/course/sequence/Unit/ContentIFrame.test.jsx
index 6ef5f08a95..86f72d7df3 100644
--- a/src/courseware/course/sequence/Unit/ContentIFrame.test.jsx
+++ b/src/courseware/course/sequence/Unit/ContentIFrame.test.jsx
@@ -1,25 +1,11 @@
-import React from 'react';
+import { render, screen } from '@testing-library/react';
-import { ErrorPage } from '@edx/frontend-platform/react';
-import { ModalDialog } from '@openedx/paragon';
-import { shallow } from '@edx/react-unit-test-utils';
-
-import PageLoading from '@src/generic/PageLoading';
-
-import { ContentIFrameLoaderSlot } from '@src/plugin-slots/ContentIFrameLoaderSlot';
import * as hooks from './hooks';
-import ContentIFrame, { IFRAME_FEATURE_POLICY, testIDs } from './ContentIFrame';
-
-jest.mock('@edx/frontend-platform/react', () => ({ ErrorPage: 'ErrorPage' }));
+import ContentIFrame, { IFRAME_FEATURE_POLICY } from './ContentIFrame';
-jest.mock('@openedx/paragon', () => jest.requireActual('@edx/react-unit-test-utils')
- .mockComponents({
- ModalDialog: {
- Body: 'ModalDialog.Body',
- },
- }));
+jest.mock('@edx/frontend-platform/react', () => ({ ErrorPage: () =>
ErrorPage
}));
-jest.mock('@src/generic/PageLoading', () => 'PageLoading');
+jest.mock('@src/generic/PageLoading', () => jest.fn(() => PageLoading
));
jest.mock('./hooks', () => ({
useIFrameBehavior: jest.fn(),
@@ -67,14 +53,13 @@ const props = {
title: 'test-title',
};
-let el;
describe('ContentIFrame Component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
beforeEach(() => {
- el = shallow();
+ render();
});
it('initializes iframe behavior hook', () => {
expect(hooks.useIFrameBehavior).toHaveBeenCalledWith({
@@ -89,63 +74,56 @@ describe('ContentIFrame Component', () => {
});
});
describe('output', () => {
- let component;
describe('if shouldShowContent', () => {
describe('if not hasLoaded', () => {
it('displays errorPage if showError', () => {
- hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, showError: true });
- el = shallow();
- expect(el.instance.findByType(ErrorPage).length).toEqual(1);
+ hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, showError: true, shouldShowContent: true });
+ render();
+ const errorPage = screen.getByText('ErrorPage');
+ expect(errorPage).toBeInTheDocument();
});
it('displays PageLoading component if not showError', () => {
- el = shallow();
- [component] = el.instance.findByType(ContentIFrameLoaderSlot);
- expect(component.props.loadingMessage).toEqual(props.loadingMessage);
+ render();
+ const pageLoading = screen.getByText('PageLoading');
+ expect(pageLoading).toBeInTheDocument();
});
});
describe('hasLoaded', () => {
it('does not display PageLoading or ErrorPage', () => {
hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, hasLoaded: true });
- el = shallow();
- expect(el.instance.findByType(PageLoading).length).toEqual(0);
- expect(el.instance.findByType(ErrorPage).length).toEqual(0);
+ render();
+ const pageLoading = screen.queryByText('PageLoading');
+ expect(pageLoading).toBeNull();
+ const errorPage = screen.queryByText('ErrorPage');
+ expect(errorPage).toBeNull();
});
});
it('display iframe with props from hooks', () => {
- el = shallow();
- [component] = el.instance.findByTestId(testIDs.contentIFrame);
- expect(component.props).toEqual({
- allow: IFRAME_FEATURE_POLICY,
- allowFullScreen: true,
- scrolling: 'no',
- referrerPolicy: 'origin',
- title: props.title,
- id: props.elementId,
- src: props.iframeUrl,
- height: iframeBehavior.iframeHeight,
- onLoad: iframeBehavior.handleIFrameLoad,
- 'data-testid': testIDs.contentIFrame,
- });
+ render();
+ const iframe = screen.getByTitle(props.title);
+ expect(iframe).toBeInTheDocument();
+ expect(iframe).toHaveAttribute('id', props.elementId);
+ expect(iframe).toHaveAttribute('src', props.iframeUrl);
+ expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
+ expect(iframe).toHaveAttribute('allowfullscreen', '');
+ expect(iframe).toHaveAttribute('scrolling', 'no');
+ expect(iframe).toHaveAttribute('referrerpolicy', 'origin');
});
});
describe('if not shouldShowContent', () => {
it('does not show PageLoading, ErrorPage, or unit-iframe-wrapper', () => {
- el = shallow();
- expect(el.instance.findByType(PageLoading).length).toEqual(0);
- expect(el.instance.findByType(ErrorPage).length).toEqual(0);
- expect(el.instance.findByTestId(testIDs.contentIFrame).length).toEqual(0);
+ render();
+ expect(screen.queryByText('PageLoading')).toBeNull();
+ expect(screen.queryByText('ErrorPage')).toBeNull();
+ expect(screen.queryByTitle(props.title)).toBeNull();
});
});
it('does not display modal if modalOptions returns isOpen: false', () => {
- el = shallow();
- expect(el.instance.findByType(ModalDialog).length).toEqual(0);
+ render();
+ const modal = screen.queryByRole('dialog');
+ expect(modal).toBeNull();
});
describe('if modalOptions.isOpen', () => {
- const testModalOpenAndHandleClose = () => {
- test('Modal component isOpen, with handleModalClose from hook', () => {
- expect(component.props.onClose).toEqual(modalIFrameData.handleModalClose);
- });
- };
describe('fullscreen modal', () => {
describe('body modal', () => {
beforeEach(() => {
@@ -153,16 +131,14 @@ describe('ContentIFrame Component', () => {
...modalIFrameData,
modalOptions: { ...modalOptions.withBody, isFullscreen: true },
});
- el = shallow();
- [component] = el.instance.findByType(ModalDialog);
+ render();
});
it('displays Modal with div wrapping provided body content if modal.body is provided', () => {
- const content = component.findByType(ModalDialog.Body)[0].children[0];
- expect(content.matches(shallow(
- {modalOptions.withBody.body}
,
- ))).toEqual(true);
+ const dialog = screen.getByRole('dialog');
+ expect(dialog).toBeInTheDocument();
+ const modalBody = screen.getByText(modalOptions.withBody.body);
+ expect(modalBody).toBeInTheDocument();
});
- testModalOpenAndHandleClose();
});
describe('url modal', () => {
beforeEach(() => {
@@ -171,54 +147,38 @@ describe('ContentIFrame Component', () => {
...modalIFrameData,
modalOptions: { ...modalOptions.withUrl, isFullscreen: true },
});
- el = shallow();
- [component] = el.instance.findByType(ModalDialog);
+ render();
});
- testModalOpenAndHandleClose();
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
- const content = component.findByType(ModalDialog.Body)[0].children[0];
- expect(content.matches(shallow(
- ,
- ))).toEqual(true);
+ const iframe = screen.getByTitle(modalOptions.withUrl.title);
+ expect(iframe).toBeInTheDocument();
+ expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
+ expect(iframe).toHaveAttribute('src', modalOptions.withUrl.url);
});
});
});
describe('body modal', () => {
beforeEach(() => {
hooks.useModalIFrameData.mockReturnValueOnce({ ...modalIFrameData, modalOptions: modalOptions.withBody });
- el = shallow();
- [component] = el.instance.findByType(ModalDialog);
+ render();
});
it('displays Modal with div wrapping provided body content if modal.body is provided', () => {
- const content = component.findByType(ModalDialog.Body)[0].children[0];
- expect(content.matches(shallow({modalOptions.withBody.body}
))).toEqual(true);
+ const dialog = screen.getByRole('dialog');
+ expect(dialog).toBeInTheDocument();
+ const modalBody = screen.getByText(modalOptions.withBody.body);
+ expect(modalBody).toBeInTheDocument();
});
- testModalOpenAndHandleClose();
});
describe('url modal', () => {
beforeEach(() => {
hooks.useModalIFrameData.mockReturnValueOnce({ ...modalIFrameData, modalOptions: modalOptions.withUrl });
- el = shallow();
- [component] = el.instance.findByType(ModalDialog);
+ render();
});
- testModalOpenAndHandleClose();
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
- const content = component.findByType(ModalDialog.Body)[0].children[0];
- expect(content.matches(shallow(
- ,
- ))).toEqual(true);
+ const iframe = screen.getByTitle(modalOptions.withUrl.title);
+ expect(iframe).toBeInTheDocument();
+ expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
+ expect(iframe).toHaveAttribute('src', modalOptions.withUrl.url);
});
});
});
diff --git a/src/courseware/course/sequence/Unit/UnitSuspense.test.jsx b/src/courseware/course/sequence/Unit/UnitSuspense.test.jsx
index db829eaa46..4bf4bac146 100644
--- a/src/courseware/course/sequence/Unit/UnitSuspense.test.jsx
+++ b/src/courseware/course/sequence/Unit/UnitSuspense.test.jsx
@@ -1,22 +1,15 @@
-import React from 'react';
-
-import { formatMessage, shallow } from '@edx/react-unit-test-utils';
+import { render, screen } from '@testing-library/react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useModel } from '@src/generic/model-store';
-import PageLoading from '@src/generic/PageLoading';
-
-import { GatedUnitContentMessageSlot } from '@src/plugin-slots/GatedUnitContentMessageSlot';
-import messages from '../messages';
-import HonorCode from '../honor-code';
-import LockPaywall from '../lock-paywall';
import hooks from './hooks';
import { modelKeys } from './constants';
import UnitSuspense from './UnitSuspense';
jest.mock('@edx/frontend-platform/i18n', () => ({
+ ...jest.requireActual('@edx/frontend-platform/i18n'),
defineMessages: m => m,
- useIntl: () => ({ formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage }),
}));
jest.mock('react', () => ({
@@ -24,10 +17,9 @@ jest.mock('react', () => ({
Suspense: 'Suspense',
}));
-jest.mock('../honor-code', () => 'HonorCode');
-jest.mock('../lock-paywall', () => 'LockPaywall');
+jest.mock('../honor-code', () => jest.fn(() => HonorCode
));
+jest.mock('../lock-paywall', () => jest.fn(() => LockPaywall
));
jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn() }));
-jest.mock('@src/generic/PageLoading', () => 'PageLoading');
jest.mock('./hooks', () => ({
useShouldDisplayHonorCode: jest.fn(() => false),
@@ -46,7 +38,6 @@ const props = {
id: 'test-id',
};
-let el;
describe('UnitSuspense component', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -54,7 +45,7 @@ describe('UnitSuspense component', () => {
});
describe('behavior', () => {
it('initializes models', () => {
- el = shallow();
+ render();
const { calls } = useModel.mock;
const [unitCall] = calls.filter(call => call[0] === modelKeys.units);
const [metaCall] = calls.filter(call => call[0] === modelKeys.coursewareMeta);
@@ -66,8 +57,9 @@ describe('UnitSuspense component', () => {
describe('LockPaywall', () => {
const testNoPaywall = () => {
it('does not display LockPaywall', () => {
- el = shallow();
- expect(el.instance.findByType(LockPaywall).length).toEqual(0);
+ render();
+ const lockPaywall = screen.queryByText('LockPaywall');
+ expect(lockPaywall).toBeNull();
});
};
describe('gating not enabled', () => { testNoPaywall(); });
@@ -78,29 +70,29 @@ describe('UnitSuspense component', () => {
describe('gating enabled, gated content included', () => {
beforeEach(() => { mockModels(true, true); });
it('displays LockPaywall in Suspense wrapper with PageLoading fallback', () => {
- el = shallow();
- const [component] = el.instance.findByType(GatedUnitContentMessageSlot);
- expect(component.parent.type).toEqual('Suspense');
- expect(component.parent.props.fallback)
- .toEqual();
- expect(component.props.courseId).toEqual(props.courseId);
+ hooks.useShouldDisplayHonorCode.mockReturnValueOnce(false);
+ render();
+ const lockPaywall = screen.getByText('LockPaywall');
+ expect(lockPaywall).toBeInTheDocument();
+ const suspenseWrapper = lockPaywall.closest('suspense');
+ expect(suspenseWrapper).toBeInTheDocument();
});
});
});
describe('HonorCode', () => {
it('does not display HonorCode if useShouldDisplayHonorCode => false', () => {
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(false);
- el = shallow();
- expect(el.instance.findByType(HonorCode).length).toEqual(0);
+ render();
+ const honorCode = screen.queryByText('HonorCode');
+ expect(honorCode).toBeNull();
});
it('displays HonorCode component in Suspense wrapper with PageLoading fallback if shouldDisplayHonorCode', () => {
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(true);
- el = shallow();
- const [component] = el.instance.findByType(HonorCode);
- expect(component.parent.type).toEqual('Suspense');
- expect(component.parent.props.fallback)
- .toEqual();
- expect(component.props.courseId).toEqual(props.courseId);
+ render();
+ const honorCode = screen.getByText('HonorCode');
+ expect(honorCode).toBeInTheDocument();
+ const suspenseWrapper = honorCode.closest('suspense');
+ expect(suspenseWrapper).toBeInTheDocument();
});
});
});
diff --git a/src/courseware/course/sequence/Unit/constants.js b/src/courseware/course/sequence/Unit/constants.ts
similarity index 60%
rename from src/courseware/course/sequence/Unit/constants.js
rename to src/courseware/course/sequence/Unit/constants.ts
index f8eb23e1c2..af02894af0 100644
--- a/src/courseware/course/sequence/Unit/constants.js
+++ b/src/courseware/course/sequence/Unit/constants.ts
@@ -1,27 +1,25 @@
-import { StrictDict } from '@edx/react-unit-test-utils/dist';
-
-export const modelKeys = StrictDict({
+export const modelKeys = {
units: 'units',
coursewareMeta: 'coursewareMeta',
-});
+} as const;
-export const views = StrictDict({
+export const views = {
student: 'student_view',
public: 'public_view',
-});
+} as const;
export const loadingState = 'loading';
-export const messageTypes = StrictDict({
+export const messageTypes = {
modal: 'plugin.modal',
resize: 'plugin.resize',
videoFullScreen: 'plugin.videoFullScreen',
autoAdvance: 'plugin.autoAdvance',
-});
+} as const;
-export default StrictDict({
+export default {
modelKeys,
views,
loadingState,
messageTypes,
-});
+};
diff --git a/src/courseware/course/sequence/Unit/hooks/useExamAccess.js b/src/courseware/course/sequence/Unit/hooks/useExamAccess.js
index 9a50ea674b..099a55db3e 100644
--- a/src/courseware/course/sequence/Unit/hooks/useExamAccess.js
+++ b/src/courseware/course/sequence/Unit/hooks/useExamAccess.js
@@ -1,19 +1,13 @@
import React from 'react';
import { logError } from '@edx/frontend-platform/logging';
-import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
import { useExamAccessToken, useFetchExamAccessToken, useIsExam } from '@edx/frontend-lib-special-exams';
-export const stateKeys = StrictDict({
- accessToken: 'accessToken',
- blockAccess: 'blockAccess',
-});
-
const useExamAccess = ({
id,
}) => {
const isExam = useIsExam();
- const [blockAccess, setBlockAccess] = useKeyedState(stateKeys.blockAccess, isExam);
+ const [blockAccess, setBlockAccess] = React.useState(isExam);
const fetchExamAccessToken = useFetchExamAccessToken();
diff --git a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js
index 3cafa3ab74..0af162dfa7 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,81 @@ 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 customHeight = 23;
const resizeMessage = (height = 23) => ({
data: { type: messageTypes.resize, payload: { height } },
});
@@ -189,63 +177,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(customHeight));
+ expect(setIframeHeight).toHaveBeenCalledWith(0);
+ expect(setIframeHeight).toHaveBeenCalledWith(customHeight);
});
- 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);
+ });
});
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 +244,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 +265,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 +278,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 +298,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 +320,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 +338,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 +361,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 +380,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);
});
});