feat: upgrade react rotuer to v6 (#793)

* feat: upgrade react rotuer to v6

* fix: test cases

* build: update header and footer

* refactor: update jumpnav test case
This commit is contained in:
Syed Ali Abbas Zaidi
2023-08-18 12:29:07 +05:00
committed by GitHub
parent e7e7f518bf
commit 3d98558bf6
32 changed files with 325 additions and 423 deletions

208
package-lock.json generated
View File

@@ -10,9 +10,9 @@
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-footer": "12.1.2",
"@edx/frontend-component-header": "4.5.0",
"@edx/frontend-platform": "4.6.1",
"@edx/frontend-component-footer": "12.2.0",
"@edx/frontend-component-header": "4.6.0",
"@edx/frontend-platform": "5.0.0",
"@edx/paragon": "20.46.2",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
@@ -48,8 +48,8 @@
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-router": "6.14.2",
"react-router-dom": "6.14.2",
"react-router-hash-link": "2.4.3",
"react-scrollspy": "3.4.3",
"react-transition-group": "4.4.5",
@@ -2167,9 +2167,9 @@
}
},
"node_modules/@edx/frontend-component-footer": {
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.1.2.tgz",
"integrity": "sha512-f1lM1WTpdiD4vjrbE5pMC8J11ObWI9w7upBBk+xqP3Iv27+py9Sr4CJVhQVRwqvIYr6heurvv9UxECbZ0X3alg==",
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.2.0.tgz",
"integrity": "sha512-tAI6TSvb0U15drV/eKtEs8O+oiDtTE/yaN4uDKs++RJxKVpLUlar7sug54b7cVy9Y3FPR/3Cfb/ytsuIqhLmxw==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-brands-svg-icons": "6.4.0",
@@ -2178,7 +2178,7 @@
"@fortawesome/react-fontawesome": "0.2.0"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.0.0",
"@edx/frontend-platform": "^4.0.0 || ^5.0.0",
"prop-types": "^15.5.10",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0"
@@ -2242,33 +2242,32 @@
}
},
"node_modules/@edx/frontend-component-header": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.5.0.tgz",
"integrity": "sha512-bMVZlxBkufn15iEbnzJztzyq/waWqdRpacwQ0DSTnzqwil6ZV+s6oLfBv767J0OrvqwHz2iydamnoAMRWOwI+A==",
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.6.0.tgz",
"integrity": "sha512-zZuMgHQWfFMTquVb4iL/iQMwKRRgts8CFFLyL8R6vQL1WfHd21hndhKii2kp9lBnIJgrilIfF79RsbImb5L0og==",
"dependencies": {
"@edx/paragon": "20.45.5",
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-brands-svg-icons": "6.4.0",
"@fortawesome/free-regular-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@edx/paragon": "20.46.2",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"axios-mock-adapter": "1.21.5",
"babel-polyfill": "6.26.0",
"react-responsive": "8.2.0",
"react-router-dom": "5.3.4",
"react-transition-group": "4.4.5"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.0.0",
"@edx/frontend-platform": "^4.0.0 || ^5.0.0",
"prop-types": "^15.5.10",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@edx/paragon": {
"version": "20.45.5",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.45.5.tgz",
"integrity": "sha512-7GsGPKyxtjFo3Xnj+uQ4vx/Khz7S6srHe8MqcsYCMx2mJ8fulPN2JFm84m+0o1CSwHaL469wBPONI4KCa+vfrA==",
"version": "20.46.2",
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.46.2.tgz",
"integrity": "sha512-px+KS/BV1CbiMKgfVgUofyjJi4CHUCUOLRukJbT66VPPqWP4Xon5Rns6uohoratPXMg2kNN46v2L8wIwqKQ4Lw==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
@@ -2315,57 +2314,57 @@
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
"integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
"integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz",
"integrity": "sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz",
"integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-brands-svg-icons": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.0.tgz",
"integrity": "sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.2.tgz",
"integrity": "sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz",
"integrity": "sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz",
"integrity": "sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz",
"integrity": "sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz",
"integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.4.0"
"@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
@@ -2408,51 +2407,10 @@
"node": ">=10"
}
},
"node_modules/@edx/frontend-component-header/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/@edx/frontend-component-header/node_modules/react-router": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/@edx/frontend-component-header/node_modules/react-router-dom": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz",
"integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.3.4",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/@edx/frontend-platform": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-4.6.1.tgz",
"integrity": "sha512-Fi/k7iZlFYs8qCsAAVz6Dseyzb9bJGh3r6iKUCiAq4emUl9UA/LfFHe4fDZcA5trVIkohhdLqrDu1U3UksY/5w==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.0.0.tgz",
"integrity": "sha512-DD9/B4rnC3BKPiWlbEFF1JIYFbWC6vUBKTyN8sf4khi4DNhhWhsobk+iNeCWNzF9UgCPRbniIqesdV1F9NXNZw==",
"dependencies": {
"@cospired/i18n-iso-languages": "4.1.0",
"@formatjs/intl-pluralrules": "4.3.3",
@@ -2485,7 +2443,7 @@
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0",
"react-redux": "^7.1.1",
"react-router-dom": "^5.0.1",
"react-router-dom": "^6.0.0",
"redux": "^4.0.4"
}
},
@@ -5156,6 +5114,14 @@
"resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz",
"integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA=="
},
"node_modules/@remix-run/router": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz",
"integrity": "sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==",
"engines": {
"node": ">=14"
}
},
"node_modules/@restart/context": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
@@ -16747,20 +16713,6 @@
"node": ">=4"
}
},
"node_modules/mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"dependencies": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
},
"peerDependencies": {
"prop-types": "^15.0.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/mini-css-extract-plugin": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz",
@@ -17700,14 +17652,6 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -19370,40 +19314,33 @@
}
},
"node_modules/react-router": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.2.tgz",
"integrity": "sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.7.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=15"
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.2.tgz",
"integrity": "sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"@remix-run/router": "1.7.2",
"react-router": "6.14.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=15"
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-router-hash-link": {
@@ -19418,11 +19355,6 @@
"react-router-dom": ">=4"
}
},
"node_modules/react-router/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-scrollspy": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/react-scrollspy/-/react-scrollspy-3.4.3.tgz",

View File

@@ -28,9 +28,9 @@
],
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-footer": "12.1.2",
"@edx/frontend-component-header": "4.5.0",
"@edx/frontend-platform": "4.6.1",
"@edx/frontend-component-footer": "12.2.0",
"@edx/frontend-component-header": "4.6.0",
"@edx/frontend-platform": "5.0.0",
"@edx/paragon": "20.46.2",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
@@ -66,8 +66,8 @@
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-router": "6.14.2",
"react-router-dom": "6.14.2",
"react-router-hash-link": "2.4.3",
"react-scrollspy": "3.4.3",
"react-transition-group": "4.4.5",

View File

@@ -2,14 +2,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
fireEvent,
render,
screen,
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
@@ -28,8 +27,6 @@ jest.mock('react-redux', () => ({
jest.mock('@edx/frontend-platform/auth');
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
const history = createMemoryHistory();
const IntlCertificatePreference = injectIntl(CertificatePreference);
const mockStore = configureStore();
@@ -42,7 +39,7 @@ describe('NameChange', () => {
const labelText = 'If checked, this name will appear on your certificates and public-facing records.';
const reduxWrapper = children => (
<Router history={history}>
<Router>
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { connect, useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
@@ -29,7 +29,7 @@ const NameChangeModal = ({
saveState,
}) => {
const dispatch = useDispatch();
const { push } = useHistory();
const navigate = useNavigate();
const { username } = getAuthenticatedUser();
const [verifiedNameInput, setVerifiedNameInput] = useState(formValues.verified_name || '');
const [confirmedWarning, setConfirmedWarning] = useState(false);
@@ -69,9 +69,9 @@ const NameChangeModal = ({
useEffect(() => {
if (saveState === 'complete') {
handleClose();
push(`/id-verification?next=${encodeURIComponent('account/settings')}`);
navigate(`/id-verification?next=${encodeURIComponent('account/settings')}`);
}
}, [handleClose, push, saveState]);
}, [handleClose, navigate, saveState]);
function renderErrors() {
if (Object.keys(errors).length > 0) {

View File

@@ -2,14 +2,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
fireEvent,
render,
screen,
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
@@ -28,8 +27,6 @@ jest.mock('react-redux', () => ({
jest.mock('@edx/frontend-platform/auth');
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ nameChangeSelector: () => ({}) })));
const history = createMemoryHistory();
const IntlNameChange = injectIntl(NameChange);
const mockStore = configureStore();
@@ -39,7 +36,7 @@ describe('NameChange', () => {
let store = {};
const reduxWrapper = children => (
<Router history={history}>
<Router>
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
@@ -168,6 +165,6 @@ describe('NameChange', () => {
props.saveState = 'complete';
render(reduxWrapper(<IntlNameChange {...props} />));
expect(history.location.pathname).toEqual('/id-verification');
expect(window.location.pathname).toEqual('/id-verification');
});
});

View File

@@ -4,7 +4,6 @@ import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp, mergeConfig, setConfig } from '@edx/frontend-platform';
import { BrowserRouter as Router } from 'react-router-dom';
import JumpNav from '../JumpNav';
import configureStore from '../../data/configureStore';
@@ -41,15 +40,11 @@ describe('JumpNav', () => {
it('should not render Optional Information link', () => {
const tree = renderer.create((
// Had to wrap the following in a router or I will receive an error stating:
// "Invariant failed: You should not use <NavLink> outside a <Router>"
<Router>
<IntlProvider locale="en">
<AppProvider store={store}>
<IntlJumpNav {...props} />
</AppProvider>
</IntlProvider>
</Router>
<IntlProvider locale="en">
<AppProvider store={store}>
<IntlJumpNav {...props} />
</AppProvider>
</IntlProvider>
))
.toJSON();
@@ -67,14 +62,11 @@ describe('JumpNav', () => {
};
const tree = renderer.create((
// Same as previous test
<Router>
<IntlProvider locale="en">
<AppProvider store={store}>
<IntlJumpNav {...props} />
</AppProvider>
</IntlProvider>
</Router>
<IntlProvider locale="en">
<AppProvider store={store}>
<IntlJumpNav {...props} />
</AppProvider>
</IntlProvider>
))
.toJSON();

View File

@@ -12,8 +12,10 @@ exports[`JumpNav should not render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#basic-information"
isActive={[Function]}
onClick={[Function]}
>
Account Information
@@ -23,8 +25,10 @@ exports[`JumpNav should not render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#profile-information"
isActive={[Function]}
onClick={[Function]}
>
Profile Information
@@ -34,8 +38,10 @@ exports[`JumpNav should not render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#social-media"
isActive={[Function]}
onClick={[Function]}
>
Social Media Links
@@ -45,8 +51,10 @@ exports[`JumpNav should not render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#site-preferences"
isActive={[Function]}
onClick={[Function]}
>
Site Preferences
@@ -56,8 +64,10 @@ exports[`JumpNav should not render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#linked-accounts"
isActive={[Function]}
onClick={[Function]}
>
Linked Accounts
@@ -67,8 +77,10 @@ exports[`JumpNav should not render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#delete-account"
isActive={[Function]}
onClick={[Function]}
>
Delete My Account
@@ -90,8 +102,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#basic-information"
isActive={[Function]}
onClick={[Function]}
>
Account Information
@@ -101,8 +115,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#profile-information"
isActive={[Function]}
onClick={[Function]}
>
Profile Information
@@ -112,8 +128,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#demographics-information"
isActive={[Function]}
onClick={[Function]}
>
Optional Information
@@ -123,8 +141,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#social-media"
isActive={[Function]}
onClick={[Function]}
>
Social Media Links
@@ -134,8 +154,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#site-preferences"
isActive={[Function]}
onClick={[Function]}
>
Site Preferences
@@ -145,8 +167,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#linked-accounts"
isActive={[Function]}
onClick={[Function]}
>
Linked Accounts
@@ -156,8 +180,10 @@ exports[`JumpNav should render Optional Information link 1`] = `
className=""
>
<a
aria-current={null}
aria-current="page"
className="active"
href="/#delete-account"
isActive={[Function]}
onClick={[Function]}
>
Delete My Account

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import {
Route, Switch, Redirect, useRouteMatch, useLocation,
Route, Routes, useLocation, useNavigate,
} from 'react-router-dom';
import camelCase from 'lodash.camelcase';
import qs from 'qs';
@@ -27,8 +27,8 @@ import messages from './IdVerification.messages';
// eslint-disable-next-line react/prefer-stateless-function
const IdVerificationPage = (props) => {
const { path } = useRouteMatch();
const { search } = useLocation();
const navigate = useNavigate();
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -45,81 +45,82 @@ const IdVerificationPage = (props) => {
}
}, [search]);
return (
<>
{/* If user reloads, redirect to the beginning of the process */}
<Redirect to={`${path}/review-requirements`} />
<div className="page__id-verification container-fluid py-5">
<div className="row">
<div className="col-lg-6 col-md-8">
<VerifiedNameContextProvider>
<IdVerificationContextProvider>
<Switch>
<Route path={`${path}/review-requirements`} component={ReviewRequirementsPanel} />
<Route path={`${path}/request-camera-access`} component={RequestCameraAccessPanel} />
<Route path={`${path}/portrait-photo-context`} component={PortraitPhotoContextPanel} />
<Route path={`${path}/take-portrait-photo`} component={TakePortraitPhotoPanel} />
<Route path={`${path}/id-context`} component={IdContextPanel} />
<Route path={`${path}/get-name-id`} component={GetNameIdPanel} />
<Route path={`${path}/take-id-photo`} component={TakeIdPhotoPanel} />
<Route path={`${path}/summary`} component={SummaryPanel} />
<Route path={`${path}/submitted`} component={SubmittedPanel} />
</Switch>
</IdVerificationContextProvider>
</VerifiedNameContextProvider>
</div>
<div className="col-lg-6 col-md-4 pt-md-0 pt-4 text-right">
<Button variant="link" className="px-0" onClick={() => setIsModalOpen(true)}>
Privacy Information
</Button>
</div>
</div>
<ModalDialog
isOpen={isModalOpen}
title="Id modal"
onClose={() => setIsModalOpen(false)}
size="lg"
hasCloseButton={false}
>
<ModalDialog.Header>
<ModalDialog.Title data-testid="Id-modal">
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<div className="p-3">
<h6>
{props.intl.formatMessage(
messages['id.verification.privacy.need.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
<h6>
{props.intl.formatMessage(
messages['id.verification.privacy.do.with.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{props.intl.formatMessage(
messages['id.verification.privacy.do.with.photo.answer'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
</div>
</ModalDialog.Body>
<ModalDialog.Footer className="p-2">
<ActionRow>
<ModalDialog.CloseButton variant="link">
Close
</ModalDialog.CloseButton>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
useEffect(() => {
navigate('/id-verification/review-requirements');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="page__id-verification container-fluid py-5">
<div className="row">
<div className="col-lg-6 col-md-8">
<VerifiedNameContextProvider>
<IdVerificationContextProvider>
<Routes>
<Route path="/review-requirements" element={<ReviewRequirementsPanel />} />
<Route path="/request-camera-access" element={<RequestCameraAccessPanel />} />
<Route path="/portrait-photo-context" element={<PortraitPhotoContextPanel />} />
<Route path="/take-portrait-photo" element={<TakePortraitPhotoPanel />} />
<Route path="/id-context" element={<IdContextPanel />} />
<Route path="/get-name-id" element={<GetNameIdPanel />} />
<Route path="/take-id-photo" element={<TakeIdPhotoPanel />} />
<Route path="/summary" element={<SummaryPanel />} />
<Route path="/submitted" element={<SubmittedPanel />} />
</Routes>
</IdVerificationContextProvider>
</VerifiedNameContextProvider>
</div>
<div className="col-lg-6 col-md-4 pt-md-0 pt-4 text-right">
<Button variant="link" className="px-0" onClick={() => setIsModalOpen(true)}>
Privacy Information
</Button>
</div>
</div>
</>
<ModalDialog
isOpen={isModalOpen}
title="Id modal"
onClose={() => setIsModalOpen(false)}
size="lg"
hasCloseButton={false}
>
<ModalDialog.Header>
<ModalDialog.Title data-testid="Id-modal">
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<div className="p-3">
<h6>
{props.intl.formatMessage(
messages['id.verification.privacy.need.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
<h6>
{props.intl.formatMessage(
messages['id.verification.privacy.do.with.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{props.intl.formatMessage(
messages['id.verification.privacy.do.with.photo.answer'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
</div>
</ModalDialog.Body>
<ModalDialog.Footer className="p-2">
<ActionRow>
<ModalDialog.CloseButton variant="link">
Close
</ModalDialog.CloseButton>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
</div>
);
};

View File

@@ -1,6 +1,6 @@
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router';
import { Navigate } from 'react-router-dom';
import { useVerificationRedirectSlug } from '../routing-utilities';
const BasePanel = ({
@@ -20,7 +20,7 @@ const BasePanel = ({
const redirectSlug = useVerificationRedirectSlug(name);
if (redirectSlug) {
return <Redirect to={redirectSlug} />;
return <Navigate replace to={`/id-verification/${redirectSlug}`} />;
}
return (

View File

@@ -2,7 +2,7 @@ import React, {
useContext, useEffect, useRef,
} from 'react';
import { Form } from '@edx/paragon';
import { Link, useHistory } from 'react-router-dom';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
@@ -12,7 +12,8 @@ import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
const GetNameIdPanel = (props) => {
const { push, location } = useHistory();
const location = useLocation();
const navigate = useNavigate();
const nameInputRef = useRef();
const panelSlug = 'get-name-id';
const nextPanelSlug = useNextPanelSlug(panelSlug);
@@ -33,7 +34,7 @@ const GetNameIdPanel = (props) => {
const handleSubmit = (e) => {
e.preventDefault();
if (idPhotoName) {
push(nextPanelSlug);
navigate(`/id-verification/${nextPanelSlug}`);
}
};
@@ -79,7 +80,7 @@ const GetNameIdPanel = (props) => {
<div className="action-row">
<Link
to={nextPanelSlug}
to={`/id-verification/${nextPanelSlug}`}
className={`btn btn-primary ${!idPhotoName && 'disabled'}`}
data-testid="next-button"
aria-disabled={!idPhotoName}

View File

@@ -41,7 +41,7 @@ const IdContextPanel = (props) => {
</div>
<CameraHelp isOpen />
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>

View File

@@ -38,7 +38,7 @@ const PortraitPhotoContextPanel = (props) => {
</div>
<CameraHelp isOpen isPortrait />
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>

View File

@@ -85,7 +85,7 @@ const RequestCameraAccessPanel = (props) => {
{props.intl.formatMessage(messages['id.verification.camera.access.success'])}
</p>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>

View File

@@ -118,7 +118,7 @@ const ReviewRequirementsPanel = (props) => {
</p>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>

View File

@@ -1,9 +1,9 @@
import React, { useState, useContext, useEffect } from 'react';
import { getConfig, history } from '@edx/frontend-platform';
import { getConfig } from '@edx/frontend-platform';
import {
Alert, Hyperlink, Form, Button, Spinner,
} from '@edx/paragon';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { submitIdVerification } from '../data/service';
@@ -31,6 +31,7 @@ const SummaryPanel = (props) => {
const nameToBeUsed = idPhotoName || nameOnAccount || '';
const [isSubmitting, setIsSubmitting] = useState(false);
const [submissionError, setSubmissionError] = useState(null);
const navigate = useNavigate();
useEffect(() => setReachedSummary(true), [setReachedSummary]);
@@ -80,7 +81,7 @@ const SummaryPanel = (props) => {
const result = await submitIdVerification(verificationData);
if (result.success) {
stopUserMedia();
history.push(nextPanelSlug);
navigate(`/id-verification/${nextPanelSlug}`);
} else {
stopUserMedia();
setIsSubmitting(false);
@@ -171,10 +172,8 @@ const SummaryPanel = (props) => {
/>
<Link
className="btn btn-outline-primary"
to={{
pathname: 'take-portrait-photo',
state: { fromSummary: true },
}}
to="/id-verification/take-portrait-photo"
state={{ fromSummary: true }}
data-testid="portrait-retake"
>
{props.intl.formatMessage(messages['id.verification.review.portrait.retake'])}
@@ -191,10 +190,8 @@ const SummaryPanel = (props) => {
/>
<Link
className="btn btn-outline-primary"
to={{
pathname: 'take-id-photo',
state: { fromSummary: true },
}}
to="/id-verification/take-id-photo"
state={{ fromSummary: true }}
data-testid="id-retake"
>
{props.intl.formatMessage(messages['id.verification.review.id.retake'])}
@@ -219,10 +216,8 @@ const SummaryPanel = (props) => {
{!profileDataManager && (
<Link
className="btn btn-link ml-3 px-0"
to={{
pathname: 'get-name-id',
state: { fromSummary: true },
}}
to="/id-verification/get-name-id"
state={{ fromSummary: true }}
>
<FormattedMessage
id="id.verification.account.name.edit"

View File

@@ -61,7 +61,7 @@ const TakeIdPhotoPanel = (props) => {
{useCameraForId && <CameraHelp />}
<CollapsibleImageHelp />
<div className="action-row" style={{ visibility: idPhotoFile ? 'unset' : 'hidden' }}>
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>

View File

@@ -35,7 +35,7 @@ const TakePortraitPhotoPanel = (props) => {
</div>
<CameraHelp isPortrait />
<div className="action-row" style={{ visibility: facePhotoFile ? 'unset' : 'hidden' }}>
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, act, screen,
} from '@testing-library/react';
@@ -12,8 +11,6 @@ import AccessBlocked from '../AccessBlocked';
const IntlAccessBlocked = injectIntl(AccessBlocked);
const history = createMemoryHistory();
describe('AccessBlocked', () => {
const defaultProps = {
intl: {},
@@ -28,7 +25,7 @@ describe('AccessBlocked', () => {
defaultProps.error = ERROR_REASONS.EXISTING_REQUEST;
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IntlAccessBlocked {...defaultProps} />
</IntlProvider>
@@ -44,7 +41,7 @@ describe('AccessBlocked', () => {
defaultProps.error = ERROR_REASONS.COURSE_ENROLLMENT;
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IntlAccessBlocked {...defaultProps} />
</IntlProvider>
@@ -60,7 +57,7 @@ describe('AccessBlocked', () => {
defaultProps.error = ERROR_REASONS.CANNOT_VERIFY;
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IntlAccessBlocked {...defaultProps} />
</IntlProvider>

View File

@@ -1,7 +1,6 @@
/* eslint-disable no-import-assign */
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, screen, act, fireEvent,
} from '@testing-library/react';
@@ -22,8 +21,6 @@ window.HTMLMediaElement.prototype.play = () => {};
const IntlCamera = injectIntl(Camera);
const history = createMemoryHistory();
describe('SubmittedPanel', () => {
const defaultProps = {
intl: {},
@@ -45,7 +42,7 @@ describe('SubmittedPanel', () => {
it('takes photo', async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
@@ -61,7 +58,7 @@ describe('SubmittedPanel', () => {
it('shows correct help text for portrait photo capture', async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
@@ -75,7 +72,7 @@ describe('SubmittedPanel', () => {
it('shows correct help text for id photo capture', async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...idProps} />
@@ -90,7 +87,7 @@ describe('SubmittedPanel', () => {
it('shows spinner when loading face detection', async () => {
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
@@ -108,7 +105,7 @@ describe('SubmittedPanel', () => {
it('canvas is visible when detection is enabled', async () => {
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
@@ -128,7 +125,7 @@ describe('SubmittedPanel', () => {
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
@@ -147,7 +144,7 @@ describe('SubmittedPanel', () => {
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
@@ -168,7 +165,7 @@ describe('SubmittedPanel', () => {
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...idProps} />

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, screen, act,
} from '@testing-library/react';
@@ -20,8 +19,6 @@ window.HTMLMediaElement.prototype.play = () => {};
const IntlCollapsible = injectIntl(CollapsibleImageHelp);
const history = createMemoryHistory();
describe('CollapsibleImageHelpPanel', () => {
const defaultProps = { intl: {} };
@@ -36,7 +33,7 @@ describe('CollapsibleImageHelpPanel', () => {
it('shows the correct text if user should switch to upload', async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCollapsible {...defaultProps} />
@@ -56,7 +53,7 @@ describe('CollapsibleImageHelpPanel', () => {
it('shows the correct text if user should switch to camera', async () => {
contextValue.useCameraForId = false;
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCollapsible {...defaultProps} />

View File

@@ -1,8 +1,7 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { MemoryRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
render, act, screen, fireEvent,
@@ -50,7 +49,6 @@ jest.mock('../panels/SubmittedPanel', () => function SubmittedPanelMock() {
const IntlIdVerificationPage = injectIntl(IdVerificationPage);
const mockStore = configureStore();
const history = createMemoryHistory();
describe('IdVerificationPage', () => {
selectors.mockClear();
@@ -60,9 +58,8 @@ describe('IdVerificationPage', () => {
intl: {},
};
it('decodes and stores course_id', async () => {
history.push(`/?course_id=${encodeURIComponent('course-v1:edX+DemoX+Demo_Course')}`);
await act(async () => render((
<Router history={history}>
<Router initialEntries={[`/?course_id=${encodeURIComponent('course-v1:edX+DemoX+Demo_Course')}`]}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
@@ -77,9 +74,8 @@ describe('IdVerificationPage', () => {
});
it('stores `next` value', async () => {
history.push('/?next=dashboard');
await act(async () => render((
<Router history={history}>
<Router initialEntries={['/?next=dashboard']}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
@@ -93,9 +89,8 @@ describe('IdVerificationPage', () => {
);
});
it('shows modal on click of button', async () => {
history.push('/?next=dashboard');
await act(async () => render((
<Router history={history}>
<Router initialEntries={['/?next=dashboard']}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
@@ -108,9 +103,8 @@ describe('IdVerificationPage', () => {
expect(screen.getByTestId('Id-modal')).toBeInTheDocument();
});
it('shows modal on click of button', async () => {
history.push('/?next=dashboard');
await act(async () => render((
<Router history={history}>
<Router initialEntries={['/?next=dashboard']}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, act, screen, fireEvent,
} from '@testing-library/react';
@@ -16,8 +15,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
const IntlGetNameIdPanel = injectIntl(GetNameIdPanel);
const history = createMemoryHistory();
describe('GetNameIdPanel', () => {
const defaultProps = {
intl: {},
@@ -36,7 +33,7 @@ describe('GetNameIdPanel', () => {
const getPanel = async (idVerificationContextValue = IDVerificationContextValue) => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<VerifiedNameContext.Provider value={verifiedNameContextValue}>
<IdVerificationContext.Provider value={idVerificationContextValue}>
@@ -82,6 +79,6 @@ describe('GetNameIdPanel', () => {
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/summary');
expect(window.location.pathname).toEqual('/id-verification/summary');
});
});

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, act, screen, fireEvent,
} from '@testing-library/react';
@@ -15,8 +14,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
const IntlIdContextPanel = injectIntl(IdContextPanel);
const history = createMemoryHistory();
describe('IdContextPanel', () => {
const defaultProps = {
intl: {},
@@ -33,7 +30,7 @@ describe('IdContextPanel', () => {
it('routes to TakeIdPhotoPanel normally', async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlIdContextPanel {...defaultProps} />
@@ -43,13 +40,13 @@ describe('IdContextPanel', () => {
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-id-photo');
expect(window.location.pathname).toEqual('/id-verification/take-id-photo');
});
it('routes to TakeIdPhotoPanel if reachedSummary is true', async () => {
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlIdContextPanel {...defaultProps} />
@@ -59,6 +56,6 @@ describe('IdContextPanel', () => {
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-id-photo');
expect(window.location.pathname).toEqual('/id-verification/take-id-photo');
});
});

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, act, screen, fireEvent,
} from '@testing-library/react';
@@ -15,8 +14,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
const IntlPortraitPhotoContextPanel = injectIntl(PortraitPhotoContextPanel);
const history = createMemoryHistory();
describe('PortraitPhotoContextPanel', () => {
const defaultProps = {
intl: {},
@@ -30,7 +27,7 @@ describe('PortraitPhotoContextPanel', () => {
it('routes to TakePortraitPhotoPanel normally', async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlPortraitPhotoContextPanel {...defaultProps} />
@@ -40,13 +37,13 @@ describe('PortraitPhotoContextPanel', () => {
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-portrait-photo');
expect(window.location.pathname).toEqual('/id-verification/take-portrait-photo');
});
it('routes to TakePortraitPhotoPanel if reachedSummary is true', async () => {
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlPortraitPhotoContextPanel {...defaultProps} />
@@ -56,6 +53,6 @@ describe('PortraitPhotoContextPanel', () => {
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-portrait-photo');
expect(window.location.pathname).toEqual('/id-verification/take-portrait-photo');
});
});

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import Bowser from 'bowser';
import { createMemoryHistory } from 'history';
import {
render, screen, cleanup, act, fireEvent,
} from '@testing-library/react';
@@ -16,8 +15,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
jest.mock('bowser');
const history = createMemoryHistory();
const IntlRequestCameraAccessPanel = injectIntl(RequestCameraAccessPanel);
describe('RequestCameraAccessPanel', () => {
@@ -38,7 +35,7 @@ describe('RequestCameraAccessPanel', () => {
contextValue.mediaAccess = 'pending';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -54,7 +51,7 @@ describe('RequestCameraAccessPanel', () => {
contextValue.mediaAccess = 'granted';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -66,14 +63,14 @@ describe('RequestCameraAccessPanel', () => {
expect(text).toHaveTextContent(/Looks like your camera is working and ready./);
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/portrait-photo-context');
expect(window.location.pathname).toEqual('/id-verification/portrait-photo-context');
});
it('renders correctly with media access denied', async () => {
contextValue.mediaAccess = 'denied';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -89,7 +86,7 @@ describe('RequestCameraAccessPanel', () => {
contextValue.mediaAccess = 'unsupported';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Chrome' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -106,7 +103,7 @@ describe('RequestCameraAccessPanel', () => {
contextValue.mediaAccess = 'unsupported';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -123,7 +120,7 @@ describe('RequestCameraAccessPanel', () => {
contextValue.mediaAccess = 'denied';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Chrome' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -139,7 +136,7 @@ describe('RequestCameraAccessPanel', () => {
contextValue.mediaAccess = 'denied';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Firefox' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -155,7 +152,7 @@ describe('RequestCameraAccessPanel', () => {
contextValue.mediaAccess = 'denied';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Safari' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -171,7 +168,7 @@ describe('RequestCameraAccessPanel', () => {
contextValue.mediaAccess = 'denied';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Internet Explorer' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -188,7 +185,7 @@ describe('RequestCameraAccessPanel', () => {
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -205,7 +202,7 @@ describe('RequestCameraAccessPanel', () => {
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
@@ -215,6 +212,6 @@ describe('RequestCameraAccessPanel', () => {
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/portrait-photo-context');
expect(window.location.pathname).toEqual('/id-verification/portrait-photo-context');
});
});

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, act, screen, fireEvent,
} from '@testing-library/react';
@@ -15,8 +14,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
const IntlReviewRequirementsPanel = injectIntl(ReviewRequirementsPanel);
const history = createMemoryHistory();
describe('ReviewRequirementsPanel', () => {
const defaultProps = {
intl: {},
@@ -26,7 +23,7 @@ describe('ReviewRequirementsPanel', () => {
const getPanel = async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={context}>
<IntlReviewRequirementsPanel {...defaultProps} />
@@ -44,7 +41,7 @@ describe('ReviewRequirementsPanel', () => {
await getPanel();
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/request-camera-access');
expect(window.location.pathname).toEqual('/id-verification/request-camera-access');
});
it('displays an alert if the user\'s account information is managed by a third party', async () => {

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, act, screen,
} from '@testing-library/react';
@@ -15,8 +14,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
const IntlSubmittedPanel = injectIntl(SubmittedPanel);
const history = createMemoryHistory();
describe('SubmittedPanel', () => {
const defaultProps = {
intl: {},
@@ -43,7 +40,7 @@ describe('SubmittedPanel', () => {
it('links to dashboard without courseId or next value', async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlSubmittedPanel {...defaultProps} />
@@ -59,7 +56,7 @@ describe('SubmittedPanel', () => {
it('links to course when courseId is stored', async () => {
sessionStorage.setItem('courseId', 'course-v1:edX+DemoX+Demo_Course');
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlSubmittedPanel {...defaultProps} />
@@ -75,7 +72,7 @@ describe('SubmittedPanel', () => {
it('links to specified page when `next` value is provided', async () => {
sessionStorage.setItem('next', 'some_page');
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlSubmittedPanel {...defaultProps} />

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-import-assign */
import React from 'react';
import { Router } from 'react-router-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import {
render, cleanup, act, screen, fireEvent, waitFor,
@@ -61,16 +61,14 @@ describe('SummaryPanel', () => {
await getPanel();
const button = await screen.findByTestId('portrait-retake');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-portrait-photo');
expect(history.location.state.fromSummary).toEqual(true);
expect(window.location.pathname).toEqual('/id-verification/take-portrait-photo');
});
it('routes back to TakeIdPhotoPanel', async () => {
await getPanel();
const button = await screen.findByTestId('id-retake');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-id-photo');
expect(history.location.state.fromSummary).toEqual(true);
expect(window.location.pathname).toEqual('/id-verification/take-id-photo');
});
it('allows user to upload ID photo', async () => {

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, act, screen, fireEvent,
} from '@testing-library/react';
@@ -14,8 +13,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
jest.mock('../../Camera');
const history = createMemoryHistory();
const IntlTakeIdPhotoPanel = injectIntl(TakeIdPhotoPanel);
describe('TakeIdPhotoPanel', () => {
@@ -37,7 +34,7 @@ describe('TakeIdPhotoPanel', () => {
it('doesn\'t show next button before photo is taken', async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakeIdPhotoPanel {...defaultProps} />
@@ -52,7 +49,7 @@ describe('TakeIdPhotoPanel', () => {
it('shows next button after photo is taken and routes to GetNameIdPanel', async () => {
contextValue.idPhotoFile = 'test.jpg';
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakeIdPhotoPanel {...defaultProps} />
@@ -63,14 +60,14 @@ describe('TakeIdPhotoPanel', () => {
const button = await screen.findByTestId('next-button');
expect(button).toBeVisible();
fireEvent.click(button);
expect(history.location.pathname).toEqual('/get-name-id');
expect(window.location.pathname).toEqual('/id-verification/get-name-id');
});
it('routes back to SummaryPanel if that was the source', async () => {
contextValue.idPhotoFile = 'test.jpg';
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakeIdPhotoPanel {...defaultProps} />
@@ -80,12 +77,12 @@ describe('TakeIdPhotoPanel', () => {
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/summary');
expect(window.location.pathname).toEqual('/id-verification/summary');
});
it('shows correct text if user should use upload', async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakeIdPhotoPanel {...defaultProps} />

View File

@@ -1,7 +1,6 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, act, screen, fireEvent,
} from '@testing-library/react';
@@ -17,8 +16,6 @@ jest.mock('../../Camera', () => function CameraMock() {
return <></>;
});
const history = createMemoryHistory();
const IntlTakePortraitPhotoPanel = injectIntl(TakePortraitPhotoPanel);
describe('TakePortraitPhotoPanel', () => {
@@ -39,7 +36,7 @@ describe('TakePortraitPhotoPanel', () => {
it('doesn\'t show next button before photo is taken', async () => {
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakePortraitPhotoPanel {...defaultProps} />
@@ -54,7 +51,7 @@ describe('TakePortraitPhotoPanel', () => {
it('shows next button after photo is taken and routes to IdContextPanel', async () => {
contextValue.facePhotoFile = 'test.jpg';
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakePortraitPhotoPanel {...defaultProps} />
@@ -65,7 +62,7 @@ describe('TakePortraitPhotoPanel', () => {
const button = await screen.findByTestId('next-button');
expect(button).toBeVisible();
fireEvent.click(button);
expect(history.location.pathname).toEqual('/id-context');
expect(window.location.pathname).toEqual('/id-verification/id-context');
});
it('routes back to SummaryPanel if that was the source', async () => {
@@ -73,7 +70,7 @@ describe('TakePortraitPhotoPanel', () => {
contextValue.idPhotoFile = 'test.jpg';
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakePortraitPhotoPanel {...defaultProps} />
@@ -83,6 +80,6 @@ describe('TakePortraitPhotoPanel', () => {
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/summary');
expect(window.location.pathname).toEqual('/id-verification/summary');
});
});

View File

@@ -8,7 +8,7 @@ import {
} from '@edx/frontend-platform';
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Switch } from 'react-router-dom';
import { Route, Routes, Outlet } from 'react-router-dom';
import Header from '@edx/frontend-component-header';
import Footer from '@edx/frontend-component-footer';
@@ -28,23 +28,26 @@ subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={configureStore()}>
<Head />
<Switch>
<Route path="/coaching_consent" component={CoachingConsent} />
<div className="d-flex flex-column" style={{ minHeight: '100vh' }}>
<Header />
<main className="flex-grow-1">
<Switch>
<Route path="/notifications/:courseId" component={NotificationPreferences} />
<Route path="/notifications" component={NotificationCourses} />
<Route path="/id-verification" component={IdVerificationPage} />
<Route exact path="/" component={AccountSettingsPage} />
<Route path="/notfound" component={NotFoundPage} />
<Route path="*" component={NotFoundPage} />
</Switch>
</main>
<Footer />
</div>
</Switch>
<Routes>
<Route path="/coaching_consent" element={<CoachingConsent />} />
<Route element={(
<div className="d-flex flex-column" style={{ minHeight: '100vh' }}>
<Header />
<main className="flex-grow-1">
<Outlet />
</main>
<Footer />
</div>
)}
>
<Route path="/notifications/:courseId" element={<NotificationPreferences />} />
<Route path="/notifications" element={<NotificationCourses />} />
<Route path="/id-verification/*" element={<IdVerificationPage />} />
<Route path="/" element={<AccountSettingsPage />} />
<Route path="/notfound" element={<NotFoundPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</AppProvider>,
document.getElementById('root'),
);

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router';
import { Link } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Container, Icon, Spinner } from '@edx/paragon';
import { ArrowBack } from '@edx/paragon/icons';