React query and react context conversion (#786)
Migrate from Redux to React Query and React Context. This modernizes state management while maintaining all existing functionality. All the redux code and files were removed, including all redux and related packages.
This commit is contained in:
1
.env
1
.env
@@ -1,4 +1,5 @@
|
|||||||
NODE_ENV='production'
|
NODE_ENV='production'
|
||||||
|
APP_ID='learner-dashboard'
|
||||||
NODE_PATH=./src
|
NODE_PATH=./src
|
||||||
BASE_URL=''
|
BASE_URL=''
|
||||||
LMS_BASE_URL=''
|
LMS_BASE_URL=''
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
NODE_ENV='development'
|
NODE_ENV='development'
|
||||||
|
APP_ID='learner-dashboard'
|
||||||
PORT=1996
|
PORT=1996
|
||||||
BASE_URL='localhost:1996'
|
BASE_URL='localhost:1996'
|
||||||
LMS_BASE_URL='http://localhost:18000'
|
LMS_BASE_URL='http://localhost:18000'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
NODE_ENV='test'
|
NODE_ENV='test'
|
||||||
|
APP_ID='learner-dashboard'
|
||||||
PORT=1996
|
PORT=1996
|
||||||
BASE_URL='localhost:1996'
|
BASE_URL='localhost:1996'
|
||||||
LMS_BASE_URL='http://localhost:18000'
|
LMS_BASE_URL='http://localhost:18000'
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ frontend-platform's getConfig loads configuration in the following sequence:
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
NODE_ENV: 'development',
|
NODE_ENV: 'development',
|
||||||
|
APP_ID: 'learner-dashboard',
|
||||||
NODE_PATH: './src',
|
NODE_PATH: './src',
|
||||||
PORT: 1996,
|
PORT: 1996,
|
||||||
BASE_URL: 'localhost:1996',
|
BASE_URL: 'localhost:1996',
|
||||||
|
|||||||
184
package-lock.json
generated
184
package-lock.json
generated
@@ -21,8 +21,7 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||||
"@openedx/paragon": "^23.4.5",
|
"@openedx/paragon": "^23.4.5",
|
||||||
"@redux-devtools/extension": "3.3.0",
|
"@tanstack/react-query": "^5.90.16",
|
||||||
"@reduxjs/toolkit": "^2.0.0",
|
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"core-js": "3.48.0",
|
"core-js": "3.48.0",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
@@ -34,14 +33,9 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-intl": "6.8.9",
|
"react-intl": "6.8.9",
|
||||||
"react-redux": "^7.2.4",
|
|
||||||
"react-router-dom": "6.30.3",
|
"react-router-dom": "6.30.3",
|
||||||
"react-share": "^5.2.2",
|
"react-share": "^5.2.2",
|
||||||
"redux": "4.2.1",
|
|
||||||
"redux-logger": "3.0.6",
|
|
||||||
"redux-thunk": "2.4.2",
|
|
||||||
"regenerator-runtime": "^0.14.0",
|
"regenerator-runtime": "^0.14.0",
|
||||||
"reselect": "^4.0.0",
|
|
||||||
"util": "^0.12.4"
|
"util": "^0.12.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -58,8 +52,7 @@
|
|||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jest-when": "^3.6.0",
|
"jest-when": "^3.6.0",
|
||||||
"react-dev-utils": "^12.0.0",
|
"react-dev-utils": "^12.0.0",
|
||||||
"react-test-renderer": "^18.3.1",
|
"react-test-renderer": "^18.3.1"
|
||||||
"redux-mock-store": "^1.5.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
@@ -2651,9 +2644,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.8.1",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
|
||||||
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5576,66 +5569,6 @@
|
|||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@redux-devtools/extension": {
|
|
||||||
"version": "3.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@redux-devtools/extension/-/extension-3.3.0.tgz",
|
|
||||||
"integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.23.2",
|
|
||||||
"immutable": "^4.3.4"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"redux": "^3.1.0 || ^4.0.0 || ^5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@reduxjs/toolkit": {
|
|
||||||
"version": "2.11.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
|
||||||
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@standard-schema/spec": "^1.0.0",
|
|
||||||
"@standard-schema/utils": "^0.3.0",
|
|
||||||
"immer": "^11.0.0",
|
|
||||||
"redux": "^5.0.1",
|
|
||||||
"redux-thunk": "^3.1.0",
|
|
||||||
"reselect": "^5.1.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
|
||||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-redux": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@reduxjs/toolkit/node_modules/redux": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@reduxjs/toolkit/node_modules/redux-thunk": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"redux": "^5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@reduxjs/toolkit/node_modules/reselect": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
|
||||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.2",
|
"version": "1.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||||
@@ -5700,18 +5633,6 @@
|
|||||||
"@sinonjs/commons": "^3.0.0"
|
"@sinonjs/commons": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@standard-schema/utils": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
|
||||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
||||||
@@ -5983,6 +5904,32 @@
|
|||||||
"url": "https://github.com/sponsors/gregberge"
|
"url": "https://github.com/sponsors/gregberge"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.90.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz",
|
||||||
|
"integrity": "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.90.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz",
|
||||||
|
"integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.90.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@testing-library/dom": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "10.4.1",
|
"version": "10.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||||
@@ -6560,6 +6507,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
|
||||||
"integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==",
|
"integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/hoist-non-react-statics": "^3.3.0",
|
"@types/hoist-non-react-statics": "^3.3.0",
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -9915,13 +9863,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/deep-diff": {
|
|
||||||
"version": "0.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
|
|
||||||
"integrity": "sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug==",
|
|
||||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@@ -13520,22 +13461,6 @@
|
|||||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/immer": {
|
|
||||||
"version": "11.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
|
|
||||||
"integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/immer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/immutable": {
|
|
||||||
"version": "4.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
|
|
||||||
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@@ -16091,13 +16016,6 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.isplainobject": {
|
|
||||||
"version": "4.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
|
||||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.memoize": {
|
"node_modules/lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||||
@@ -19199,6 +19117,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
||||||
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
|
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.15.4",
|
"@babel/runtime": "^7.15.4",
|
||||||
"@types/react-redux": "^7.1.20",
|
"@types/react-redux": "^7.1.20",
|
||||||
@@ -19538,37 +19457,6 @@
|
|||||||
"@babel/runtime": "^7.9.2"
|
"@babel/runtime": "^7.9.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/redux-logger": {
|
|
||||||
"version": "3.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
|
|
||||||
"integrity": "sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"deep-diff": "^0.3.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/redux-mock-store": {
|
|
||||||
"version": "1.5.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.5.tgz",
|
|
||||||
"integrity": "sha512-YxX+ofKUTQkZE4HbhYG4kKGr7oCTJfB0GLy7bSeqx86GLpGirrbUWstMnqXkqHNaQpcnbMGbof2dYs5KsPE6Zg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"lodash.isplainobject": "^4.0.6"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"redux": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/redux-thunk": {
|
|
||||||
"version": "2.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
|
|
||||||
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"redux": "^4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -19735,12 +19623,6 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/reselect": {
|
|
||||||
"version": "4.1.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
|
|
||||||
"integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -41,8 +41,7 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||||
"@openedx/paragon": "^23.4.5",
|
"@openedx/paragon": "^23.4.5",
|
||||||
"@redux-devtools/extension": "3.3.0",
|
"@tanstack/react-query": "^5.90.16",
|
||||||
"@reduxjs/toolkit": "^2.0.0",
|
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"core-js": "3.48.0",
|
"core-js": "3.48.0",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
@@ -54,14 +53,9 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-intl": "6.8.9",
|
"react-intl": "6.8.9",
|
||||||
"react-redux": "^7.2.4",
|
|
||||||
"react-router-dom": "6.30.3",
|
"react-router-dom": "6.30.3",
|
||||||
"react-share": "^5.2.2",
|
"react-share": "^5.2.2",
|
||||||
"redux": "4.2.1",
|
|
||||||
"redux-logger": "3.0.6",
|
|
||||||
"redux-thunk": "2.4.2",
|
|
||||||
"regenerator-runtime": "^0.14.0",
|
"regenerator-runtime": "^0.14.0",
|
||||||
"reselect": "^4.0.0",
|
|
||||||
"util": "^0.12.4"
|
"util": "^0.12.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -78,7 +72,6 @@
|
|||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jest-when": "^3.6.0",
|
"jest-when": "^3.6.0",
|
||||||
"react-dev-utils": "^12.0.0",
|
"react-dev-utils": "^12.0.0",
|
||||||
"react-test-renderer": "^18.3.1",
|
"react-test-renderer": "^18.3.1"
|
||||||
"redux-mock-store": "^1.5.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/App.jsx
48
src/App.jsx
@@ -5,60 +5,30 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
|||||||
import { logError } from '@edx/frontend-platform/logging';
|
import { logError } from '@edx/frontend-platform/logging';
|
||||||
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
||||||
|
|
||||||
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
|
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||||
import { Alert } from '@openedx/paragon';
|
import { Alert } from '@openedx/paragon';
|
||||||
|
|
||||||
import { RequestKeys } from 'data/constants/requests';
|
|
||||||
import store from 'data/store';
|
|
||||||
import {
|
|
||||||
selectors,
|
|
||||||
actions,
|
|
||||||
} from 'data/redux';
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import Dashboard from 'containers/Dashboard';
|
import Dashboard from 'containers/Dashboard';
|
||||||
|
|
||||||
import track from 'tracking';
|
|
||||||
|
|
||||||
import fakeData from 'data/services/lms/fakeData/courses';
|
|
||||||
|
|
||||||
import AppWrapper from 'containers/AppWrapper';
|
import AppWrapper from 'containers/AppWrapper';
|
||||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
import { useMasquerade } from 'data/context';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const { authenticatedUser } = React.useContext(AppContext);
|
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const isFailed = {
|
const { masqueradeUser } = useMasquerade();
|
||||||
initialize: reduxHooks.useRequestIsFailed(RequestKeys.initialize),
|
const { data, isError } = useInitializeLearnerHome();
|
||||||
refreshList: reduxHooks.useRequestIsFailed(RequestKeys.refreshList),
|
const hasNetworkFailure = !masqueradeUser && isError;
|
||||||
};
|
const supportEmail = data?.platformSettings?.supportEmail || undefined;
|
||||||
const hasNetworkFailure = isFailed.initialize || isFailed.refreshList;
|
|
||||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
|
||||||
const loadData = reduxHooks.useLoadData();
|
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
|
|
||||||
window.loadEmptyData = () => {
|
|
||||||
loadData({ ...fakeData.globalData, courses: [] });
|
|
||||||
};
|
|
||||||
window.loadMockData = () => {
|
|
||||||
loadData({
|
|
||||||
...fakeData.globalData,
|
|
||||||
courses: [
|
|
||||||
...fakeData.courseRunData,
|
|
||||||
...fakeData.entitlementData,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
window.store = store;
|
|
||||||
window.selectors = selectors;
|
|
||||||
window.actions = actions;
|
|
||||||
window.track = track;
|
|
||||||
}
|
|
||||||
if (getConfig().HOTJAR_APP_ID) {
|
if (getConfig().HOTJAR_APP_ID) {
|
||||||
try {
|
try {
|
||||||
initializeHotjar({
|
initializeHotjar({
|
||||||
@@ -70,7 +40,7 @@ export const App = () => {
|
|||||||
logError(error);
|
logError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [authenticatedUser, loadData]);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|||||||
@@ -3,30 +3,24 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
import { RequestKeys } from 'data/constants/requests';
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import { App } from './App';
|
import { App } from './App';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
|
jest.mock('data/hooks', () => ({
|
||||||
|
useInitializeLearnerHome: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('data/context', () => ({
|
||||||
|
useMasquerade: jest.fn(() => ({ masqueradeUser: null })),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('@edx/frontend-component-footer', () => ({
|
jest.mock('@edx/frontend-component-footer', () => ({
|
||||||
FooterSlot: jest.fn(() => <div>FooterSlot</div>),
|
FooterSlot: jest.fn(() => <div>FooterSlot</div>),
|
||||||
}));
|
}));
|
||||||
jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
|
jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
|
||||||
jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => <div>LearnerDashboardHeader</div>));
|
jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => <div>LearnerDashboardHeader</div>));
|
||||||
jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
|
jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
|
||||||
jest.mock('data/redux', () => ({
|
|
||||||
selectors: 'redux.selectors',
|
|
||||||
actions: 'redux.actions',
|
|
||||||
thunkActions: 'redux.thunkActions',
|
|
||||||
}));
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useRequestIsFailed: jest.fn(),
|
|
||||||
usePlatformSettingsData: jest.fn(),
|
|
||||||
useLoadData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('data/store', () => 'data/store');
|
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform', () => ({
|
jest.mock('@edx/frontend-platform', () => ({
|
||||||
getConfig: jest.fn(() => ({})),
|
getConfig: jest.fn(() => ({})),
|
||||||
@@ -37,11 +31,15 @@ jest.mock('@edx/frontend-platform/react', () => ({
|
|||||||
ErrorPage: () => 'ErrorPage',
|
ErrorPage: () => 'ErrorPage',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const loadData = jest.fn();
|
|
||||||
reduxHooks.useLoadData.mockReturnValue(loadData);
|
|
||||||
|
|
||||||
const supportEmail = 'test@support.com';
|
const supportEmail = 'test@support.com';
|
||||||
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
useInitializeLearnerHome.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
platformSettings: {
|
||||||
|
supportEmail,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isError: false,
|
||||||
|
});
|
||||||
|
|
||||||
describe('App router component', () => {
|
describe('App router component', () => {
|
||||||
describe('component', () => {
|
describe('component', () => {
|
||||||
@@ -66,7 +64,6 @@ describe('App router component', () => {
|
|||||||
describe('no network failure', () => {
|
describe('no network failure', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
|
||||||
getConfig.mockReturnValue({});
|
getConfig.mockReturnValue({});
|
||||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||||
});
|
});
|
||||||
@@ -79,7 +76,6 @@ describe('App router component', () => {
|
|||||||
describe('no network failure with optimizely url', () => {
|
describe('no network failure with optimizely url', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
|
||||||
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
|
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
|
||||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||||
});
|
});
|
||||||
@@ -92,7 +88,6 @@ describe('App router component', () => {
|
|||||||
describe('no network failure with optimizely project id', () => {
|
describe('no network failure with optimizely project id', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
|
||||||
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
|
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
|
||||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||||
});
|
});
|
||||||
@@ -105,7 +100,10 @@ describe('App router component', () => {
|
|||||||
describe('initialize failure', () => {
|
describe('initialize failure', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
|
useInitializeLearnerHome.mockReturnValue({
|
||||||
|
data: null,
|
||||||
|
isError: true,
|
||||||
|
});
|
||||||
getConfig.mockReturnValue({});
|
getConfig.mockReturnValue({});
|
||||||
render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
|
render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
|
||||||
});
|
});
|
||||||
@@ -119,7 +117,6 @@ describe('App router component', () => {
|
|||||||
});
|
});
|
||||||
describe('refresh failure', () => {
|
describe('refresh failure', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
|
|
||||||
getConfig.mockReturnValue({});
|
getConfig.mockReturnValue({});
|
||||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const configuration = {
|
const configuration = {
|
||||||
// BASE_URL: process.env.BASE_URL,
|
// BASE_URL: process.env.BASE_URL,
|
||||||
|
APP_ID: process.env.APP_ID,
|
||||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||||
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
|
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
|
||||||
CREDIT_PURCHASE_URL: process.env.CREDIT_PURCHASE_URL,
|
CREDIT_PURCHASE_URL: process.env.CREDIT_PURCHASE_URL,
|
||||||
|
|||||||
15
src/containers/AppWrapper/index.test.tsx
Normal file
15
src/containers/AppWrapper/index.test.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import AppWrapper from './index';
|
||||||
|
|
||||||
|
describe('AppWrapper', () => {
|
||||||
|
it('should render children without modification', () => {
|
||||||
|
render(
|
||||||
|
<AppWrapper>
|
||||||
|
<div>Test Child</div>
|
||||||
|
</AppWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Child')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,21 +1,29 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||||
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import useActionDisabledState from '../hooks';
|
import useActionDisabledState from '../hooks';
|
||||||
import ActionButton from './ActionButton';
|
import ActionButton from './ActionButton';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const BeginCourseButton = ({ cardId }) => {
|
export const BeginCourseButton = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
const { data: learnerData } = useInitializeLearnerHome();
|
||||||
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
|
const homeUrl = courseData?.courseRun?.homeUrl;
|
||||||
|
const execEdTrackingParam = useMemo(() => {
|
||||||
|
const isExecEd2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(courseData.enrollment.mode);
|
||||||
|
const { authOrgId } = learnerData.enterpriseDashboard || {};
|
||||||
|
return isExecEd2UCourse ? `?org_id=${authOrgId}` : '';
|
||||||
|
}, [courseData.enrollment.mode, learnerData.enterpriseDashboard]);
|
||||||
const { disableBeginCourse } = useActionDisabledState(cardId);
|
const { disableBeginCourse } = useActionDisabledState(cardId);
|
||||||
|
|
||||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
const handleClick = useCourseTrackingEvent(
|
||||||
track.course.enterCourseClicked,
|
track.course.enterCourseClicked,
|
||||||
cardId,
|
cardId,
|
||||||
homeUrl + execEdTrackingParam,
|
homeUrl + execEdTrackingParam,
|
||||||
|
|||||||
@@ -1,36 +1,42 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
|
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||||
import useActionDisabledState from '../hooks';
|
import useActionDisabledState from '../hooks';
|
||||||
import BeginCourseButton from './BeginCourseButton';
|
import BeginCourseButton from './BeginCourseButton';
|
||||||
|
|
||||||
|
jest.mock('hooks', () => ({
|
||||||
|
useCourseData: jest.fn().mockReturnValue({
|
||||||
|
enrollment: { mode: 'executive-education' },
|
||||||
|
courseRun: { homeUrl: 'home-url' },
|
||||||
|
}),
|
||||||
|
useCourseTrackingEvent: jest.fn().mockReturnValue({
|
||||||
|
trackCourseEvent: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('data/hooks', () => ({
|
||||||
|
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||||
|
data: {
|
||||||
|
enterpriseDashboard: {
|
||||||
|
authOrgId: 'test-org-id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('tracking', () => ({
|
jest.mock('tracking', () => ({
|
||||||
course: {
|
course: {
|
||||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useCardCourseRunData: jest.fn(),
|
|
||||||
useCardExecEdTrackingParam: jest.fn(),
|
|
||||||
useTrackCourseEvent: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
||||||
|
|
||||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||||
|
|
||||||
const homeUrl = 'home-url';
|
const homeUrl = 'home-url';
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
|
||||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
|
||||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
|
||||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
|
||||||
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
cardId: 'cardId',
|
cardId: 'cardId',
|
||||||
@@ -45,11 +51,7 @@ describe('BeginCourseButton', () => {
|
|||||||
describe('initiliaze hooks', () => {
|
describe('initiliaze hooks', () => {
|
||||||
it('initializes course run data with cardId', () => {
|
it('initializes course run data with cardId', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
expect(useCourseData).toHaveBeenCalledWith(props.cardId);
|
||||||
});
|
|
||||||
it('loads exec education path param', () => {
|
|
||||||
renderComponent();
|
|
||||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
|
||||||
});
|
});
|
||||||
it('loads disabled states for begin action from action hooks', () => {
|
it('loads disabled states for begin action from action hooks', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
@@ -73,15 +75,15 @@ describe('BeginCourseButton', () => {
|
|||||||
expect(button).not.toHaveClass('disabled');
|
expect(button).not.toHaveClass('disabled');
|
||||||
expect(button).not.toHaveAttribute('aria-disabled', 'true');
|
expect(button).not.toHaveAttribute('aria-disabled', 'true');
|
||||||
});
|
});
|
||||||
it('should track enter course clicked event on click, with exec ed param', async () => {
|
it('should track enter course clicked event on click, with exec ed param', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const button = screen.getByRole('button', { name: 'Begin Course' });
|
const button = screen.getByRole('button', { name: 'Begin Course' });
|
||||||
user.click(button);
|
user.click(button);
|
||||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||||
track.course.enterCourseClicked,
|
track.course.enterCourseClicked,
|
||||||
props.cardId,
|
props.cardId,
|
||||||
homeUrl + execEdPath(props.cardId),
|
`${homeUrl}?org_id=test-org-id`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseTrackingEvent, useCourseData } from 'hooks';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import useActionDisabledState from '../hooks';
|
import useActionDisabledState from '../hooks';
|
||||||
import ActionButton from './ActionButton';
|
import ActionButton from './ActionButton';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const ResumeButton = ({ cardId }) => {
|
export const ResumeButton = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
const { data: learnerData } = useInitializeLearnerHome();
|
||||||
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
|
const resumeUrl = courseData?.courseRun?.resumeUrl;
|
||||||
|
const execEdTrackingParam = useMemo(() => {
|
||||||
|
const isExecEd2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(courseData.enrollment.mode);
|
||||||
|
const { authOrgId } = learnerData.enterpriseDashboard || {};
|
||||||
|
return isExecEd2UCourse ? `?org_id=${authOrgId}` : '';
|
||||||
|
}, [courseData.enrollment.mode, learnerData.enterpriseDashboard]);
|
||||||
const { disableResumeCourse } = useActionDisabledState(cardId);
|
const { disableResumeCourse } = useActionDisabledState(cardId);
|
||||||
|
|
||||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
const handleClick = useCourseTrackingEvent(
|
||||||
track.course.enterCourseClicked,
|
track.course.enterCourseClicked,
|
||||||
cardId,
|
cardId,
|
||||||
resumeUrl + execEdTrackingParam,
|
resumeUrl + execEdTrackingParam,
|
||||||
|
|||||||
@@ -1,36 +1,47 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useCourseTrackingEvent, useCourseData } from 'hooks';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import useActionDisabledState from '../hooks';
|
import useActionDisabledState from '../hooks';
|
||||||
import ResumeButton from './ResumeButton';
|
import ResumeButton from './ResumeButton';
|
||||||
|
|
||||||
|
const authOrgId = 'auth-org-id';
|
||||||
|
jest.mock('data/hooks', () => ({
|
||||||
|
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||||
|
data: {
|
||||||
|
enterpriseDashboard: {
|
||||||
|
authOrgId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks', () => ({
|
||||||
|
useCourseData: jest.fn().mockReturnValue({
|
||||||
|
enrollment: { mode: 'executive-education' },
|
||||||
|
courseRun: { homeUrl: 'home-url' },
|
||||||
|
}),
|
||||||
|
useCourseTrackingEvent: jest.fn().mockReturnValue({
|
||||||
|
trackCourseEvent: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('tracking', () => ({
|
jest.mock('tracking', () => ({
|
||||||
course: {
|
course: {
|
||||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useCardCourseRunData: jest.fn(),
|
|
||||||
useCardExecEdTrackingParam: jest.fn(),
|
|
||||||
useTrackCourseEvent: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
||||||
|
|
||||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||||
|
|
||||||
const resumeUrl = 'resume-url';
|
useCourseData.mockReturnValue({
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
|
enrollment: { mode: 'executive-education' },
|
||||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
courseRun: { resumeUrl: 'home-url' },
|
||||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
});
|
||||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
|
||||||
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('ResumeButton', () => {
|
describe('ResumeButton', () => {
|
||||||
const props = {
|
const props = {
|
||||||
@@ -39,10 +50,7 @@ describe('ResumeButton', () => {
|
|||||||
describe('initialize hooks', () => {
|
describe('initialize hooks', () => {
|
||||||
beforeEach(() => render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>));
|
beforeEach(() => render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>));
|
||||||
it('initializes course run data with cardId', () => {
|
it('initializes course run data with cardId', () => {
|
||||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
expect(useCourseData).toHaveBeenCalledWith(props.cardId);
|
||||||
});
|
|
||||||
it('loads exec education path param', () => {
|
|
||||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
|
||||||
});
|
});
|
||||||
it('loads disabled states for resume action from action hooks', () => {
|
it('loads disabled states for resume action from action hooks', () => {
|
||||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||||
@@ -73,10 +81,10 @@ describe('ResumeButton', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const button = screen.getByRole('button', { name: 'Resume' });
|
const button = screen.getByRole('button', { name: 'Resume' });
|
||||||
user.click(button);
|
user.click(button);
|
||||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||||
track.course.enterCourseClicked,
|
track.course.enterCourseClicked,
|
||||||
props.cardId,
|
props.cardId,
|
||||||
resumeUrl + execEdPath(props.cardId),
|
`home-url?org_id=${authOrgId}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useSelectSessionModal } from 'data/context';
|
||||||
import useActionDisabledState from '../hooks';
|
import useActionDisabledState from '../hooks';
|
||||||
import ActionButton from './ActionButton';
|
import ActionButton from './ActionButton';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -11,11 +11,11 @@ import messages from './messages';
|
|||||||
export const SelectSessionButton = ({ cardId }) => {
|
export const SelectSessionButton = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { disableSelectSession } = useActionDisabledState(cardId);
|
const { disableSelectSession } = useActionDisabledState(cardId);
|
||||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
const { updateSelectSessionModal } = useSelectSessionModal();
|
||||||
return (
|
return (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
disabled={disableSelectSession}
|
disabled={disableSelectSession}
|
||||||
onClick={openSessionModal}
|
onClick={() => updateSelectSessionModal(cardId)}
|
||||||
>
|
>
|
||||||
{formatMessage(messages.selectSession)}
|
{formatMessage(messages.selectSession)}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useSelectSessionModal } from 'data/context';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import useActionDisabledState from '../hooks';
|
import useActionDisabledState from '../hooks';
|
||||||
|
|
||||||
import SelectSessionButton from './SelectSessionButton';
|
import SelectSessionButton from './SelectSessionButton';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('data/context', () => ({
|
||||||
reduxHooks: {
|
useSelectSessionModal: jest.fn().mockReturnValue({
|
||||||
useUpdateSelectSessionModalCallback: jest.fn(),
|
updateSelectSessionModal: jest.fn(),
|
||||||
},
|
}),
|
||||||
}));
|
}));
|
||||||
jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false })));
|
jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false })));
|
||||||
|
|
||||||
@@ -33,11 +33,15 @@ describe('SelectSessionButton', () => {
|
|||||||
});
|
});
|
||||||
describe('on click', () => {
|
describe('on click', () => {
|
||||||
it('should call openSessionModal', async () => {
|
it('should call openSessionModal', async () => {
|
||||||
|
const mockedUpdateSelectSessionModal = jest.fn();
|
||||||
|
useSelectSessionModal.mockReturnValue({
|
||||||
|
updateSelectSessionModal: mockedUpdateSelectSessionModal,
|
||||||
|
});
|
||||||
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
|
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const button = screen.getByRole('button', { name: 'Select Session' });
|
const button = screen.getByRole('button', { name: 'Select Session' });
|
||||||
await user.click(button);
|
await user.click(button);
|
||||||
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(props.cardId);
|
expect(mockedUpdateSelectSessionModal).toHaveBeenCalledWith(props.cardId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import PropTypes from 'prop-types';
|
|||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseTrackingEvent, useCourseData } from 'hooks';
|
||||||
import useActionDisabledState from '../hooks';
|
import useActionDisabledState from '../hooks';
|
||||||
import ActionButton from './ActionButton';
|
import ActionButton from './ActionButton';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const ViewCourseButton = ({ cardId }) => {
|
export const ViewCourseButton = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
|
const homeUrl = courseData?.courseRun?.homeUrl;
|
||||||
const { disableViewCourse } = useActionDisabledState(cardId);
|
const { disableViewCourse } = useActionDisabledState(cardId);
|
||||||
|
|
||||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
const handleClick = useCourseTrackingEvent(
|
||||||
track.course.enterCourseClicked,
|
track.course.enterCourseClicked,
|
||||||
cardId,
|
cardId,
|
||||||
homeUrl,
|
homeUrl,
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useCourseTrackingEvent } from 'hooks';
|
||||||
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import useActionDisabledState from '../hooks';
|
import useActionDisabledState from '../hooks';
|
||||||
import ViewCourseButton from './ViewCourseButton';
|
import ViewCourseButton from './ViewCourseButton';
|
||||||
|
|
||||||
|
jest.mock('hooks', () => ({
|
||||||
|
useCourseData: jest.fn().mockReturnValue({
|
||||||
|
courseRun: { homeUrl: 'homeUrl' },
|
||||||
|
}),
|
||||||
|
useCourseTrackingEvent: jest.fn().mockReturnValue({
|
||||||
|
trackCourseEvent: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('tracking', () => ({
|
jest.mock('tracking', () => ({
|
||||||
course: {
|
course: {
|
||||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
|
|
||||||
useTrackCourseEvent: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
|
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
|
||||||
|
|
||||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||||
@@ -35,15 +38,18 @@ describe('ViewCourseButton', () => {
|
|||||||
expect(button).not.toHaveAttribute('aria-disabled', 'true');
|
expect(button).not.toHaveAttribute('aria-disabled', 'true');
|
||||||
});
|
});
|
||||||
it('calls trackCourseEvent on click', async () => {
|
it('calls trackCourseEvent on click', async () => {
|
||||||
|
const mockedTrackCourseEvent = jest.fn();
|
||||||
|
useCourseTrackingEvent.mockReturnValue(mockedTrackCourseEvent);
|
||||||
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
|
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const button = screen.getByRole('button', { name: 'View Course' });
|
const button = screen.getByRole('button', { name: 'View Course' });
|
||||||
await user.click(button);
|
await user.click(button);
|
||||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||||
track.course.enterCourseClicked,
|
track.course.enterCourseClicked,
|
||||||
defaultProps.cardId,
|
defaultProps.cardId,
|
||||||
homeUrl,
|
homeUrl,
|
||||||
);
|
);
|
||||||
|
expect(mockedTrackCourseEvent).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
it('learner cannot view course', () => {
|
it('learner cannot view course', () => {
|
||||||
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
|
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { ActionRow } from '@openedx/paragon';
|
import { ActionRow } from '@openedx/paragon';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useEntitlementInfo } from 'hooks';
|
||||||
|
|
||||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||||
import SelectSessionButton from './SelectSessionButton';
|
import SelectSessionButton from './SelectSessionButton';
|
||||||
@@ -12,11 +12,10 @@ import ResumeButton from './ResumeButton';
|
|||||||
import ViewCourseButton from './ViewCourseButton';
|
import ViewCourseButton from './ViewCourseButton';
|
||||||
|
|
||||||
export const CourseCardActions = ({ cardId }) => {
|
export const CourseCardActions = ({ cardId }) => {
|
||||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
const cardData = useCourseData(cardId);
|
||||||
const {
|
const hasStarted = cardData.enrollment.hasStarted || false;
|
||||||
hasStarted,
|
const { isEntitlement, isFulfilled } = useEntitlementInfo(cardData);
|
||||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
const isArchived = cardData.courseRun.isArchived || false;
|
||||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionRow data-test-id="CourseCardActions">
|
<ActionRow data-test-id="CourseCardActions">
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
|
|
||||||
import CourseCardActions from '.';
|
import CourseCardActions from '.';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
...jest.requireActual('hooks'),
|
||||||
useCardCourseRunData: jest.fn(),
|
useCourseData: jest.fn(),
|
||||||
useCardEnrollmentData: jest.fn(),
|
|
||||||
useCardEntitlementData: jest.fn(),
|
|
||||||
useMasqueradeData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('plugin-slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
|
jest.mock('plugin-slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
|
||||||
@@ -24,26 +19,22 @@ const props = { cardId };
|
|||||||
describe('CourseCardActions', () => {
|
describe('CourseCardActions', () => {
|
||||||
const mockHooks = ({
|
const mockHooks = ({
|
||||||
isEntitlement = false,
|
isEntitlement = false,
|
||||||
isExecEd2UCourse = false,
|
|
||||||
isFulfilled = false,
|
isFulfilled = false,
|
||||||
isArchived = false,
|
isArchived = false,
|
||||||
isVerified = false,
|
|
||||||
hasStarted = false,
|
hasStarted = false,
|
||||||
isMasquerading = false,
|
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
|
useCourseData.mockReturnValueOnce({
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
|
enrollment: { hasStarted },
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted });
|
courseRun: { isArchived },
|
||||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
entitlement: isEntitlement !== null ? { isEntitlement, isFulfilled } : null,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const renderComponent = () => render(<CourseCardActions {...props} />);
|
const renderComponent = () => render(<CourseCardActions {...props} />);
|
||||||
describe('hooks', () => {
|
describe('hooks', () => {
|
||||||
it('initializes redux hooks', () => {
|
it('initializes hooks', () => {
|
||||||
mockHooks();
|
mockHooks();
|
||||||
renderComponent();
|
renderComponent();
|
||||||
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
|
||||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('output', () => {
|
describe('output', () => {
|
||||||
@@ -63,7 +54,7 @@ describe('CourseCardActions', () => {
|
|||||||
});
|
});
|
||||||
describe('not entitlement, verified, or exec ed', () => {
|
describe('not entitlement, verified, or exec ed', () => {
|
||||||
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
|
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
|
||||||
mockHooks({ isArchived: true });
|
mockHooks({ isArchived: true, isEntitlement: null });
|
||||||
renderComponent();
|
renderComponent();
|
||||||
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
||||||
expect(CourseCardActionSlot).toBeInTheDocument();
|
expect(CourseCardActionSlot).toBeInTheDocument();
|
||||||
@@ -72,7 +63,7 @@ describe('CourseCardActions', () => {
|
|||||||
});
|
});
|
||||||
describe('unstarted courses', () => {
|
describe('unstarted courses', () => {
|
||||||
it('renders CourseCardActionSlot and BeginCourseButton', () => {
|
it('renders CourseCardActionSlot and BeginCourseButton', () => {
|
||||||
mockHooks();
|
mockHooks({ isEntitlement: null });
|
||||||
renderComponent();
|
renderComponent();
|
||||||
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
||||||
expect(CourseCardActionSlot).toBeInTheDocument();
|
expect(CourseCardActionSlot).toBeInTheDocument();
|
||||||
@@ -82,7 +73,7 @@ describe('CourseCardActions', () => {
|
|||||||
});
|
});
|
||||||
describe('active courses (started, and not archived)', () => {
|
describe('active courses (started, and not archived)', () => {
|
||||||
it('renders CourseCardActionSlot and ResumeButton', () => {
|
it('renders CourseCardActionSlot and ResumeButton', () => {
|
||||||
mockHooks({ hasStarted: true });
|
mockHooks({ hasStarted: true, isEntitlement: null });
|
||||||
renderComponent();
|
renderComponent();
|
||||||
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
||||||
expect(CourseCardActionSlot).toBeInTheDocument();
|
expect(CourseCardActionSlot).toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
/* eslint-disable max-len */
|
/* eslint-disable max-len */
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { MailtoLink, Hyperlink } from '@openedx/paragon';
|
import { MailtoLink, Hyperlink } from '@openedx/paragon';
|
||||||
import { CheckCircle } from '@openedx/paragon/icons';
|
import { CheckCircle } from '@openedx/paragon/icons';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { baseAppUrl } from 'data/services/lms/urls';
|
||||||
|
|
||||||
import { utilHooks, reduxHooks } from 'hooks';
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
import { utilHooks, useCourseData } from 'hooks';
|
||||||
import Banner from 'components/Banner';
|
import Banner from 'components/Banner';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -14,15 +16,32 @@ import messages from './messages';
|
|||||||
const { useFormatDate } = utilHooks;
|
const { useFormatDate } = utilHooks;
|
||||||
|
|
||||||
export const CertificateBanner = ({ cardId }) => {
|
export const CertificateBanner = ({ cardId }) => {
|
||||||
const certificate = reduxHooks.useCardCertificateData(cardId);
|
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||||
|
const courseData = useCourseData(cardId);
|
||||||
const {
|
const {
|
||||||
isAudit,
|
certificate = {},
|
||||||
isVerified,
|
isVerified = false,
|
||||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
isAudit = false,
|
||||||
const { isPassing } = reduxHooks.useCardGradeData(cardId);
|
isPassing = false,
|
||||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
isArchived = false,
|
||||||
const { minPassingGrade, progressUrl } = reduxHooks.useCardCourseRunData(cardId);
|
minPassingGrade = 0,
|
||||||
const { supportEmail, billingEmail } = reduxHooks.usePlatformSettingsData();
|
progressUrl = '',
|
||||||
|
} = useMemo(() => ({
|
||||||
|
isVerified: courseData?.enrollment?.isVerified,
|
||||||
|
isAudit: courseData?.enrollment?.isAudit,
|
||||||
|
certificate: courseData?.certificate || {},
|
||||||
|
isPassing: courseData?.gradeData?.isPassing,
|
||||||
|
isArchived: courseData?.courseRun?.isArchived,
|
||||||
|
minPassingGrade: Math.floor((courseData?.courseRun?.minPassingGrade ?? 0) * 100),
|
||||||
|
progressUrl: baseAppUrl(courseData?.courseRun?.progressUrl || ''),
|
||||||
|
}), [courseData]);
|
||||||
|
const { supportEmail, billingEmail } = useMemo(
|
||||||
|
() => ({
|
||||||
|
supportEmail: learnerHomeData?.platformSettings?.supportEmail,
|
||||||
|
billingEmail: learnerHomeData?.platformSettings?.billingEmail,
|
||||||
|
}),
|
||||||
|
[learnerHomeData],
|
||||||
|
);
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const formatDate = useFormatDate();
|
const formatDate = useFormatDate();
|
||||||
|
|
||||||
@@ -75,7 +94,7 @@ export const CertificateBanner = ({ cardId }) => {
|
|||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (certificate.isEarnedButUnavailable) {
|
if (certificate.isEarned && new Date(certificate.availableDate) > new Date()) {
|
||||||
return (
|
return (
|
||||||
<Banner>
|
<Banner>
|
||||||
{formatMessage(
|
{formatMessage(
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import CertificateBanner from './CertificateBanner';
|
import CertificateBanner from './CertificateBanner';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
utilHooks: {
|
utilHooks: {
|
||||||
useFormatDate: jest.fn(() => date => date),
|
useFormatDate: jest.fn(() => date => date),
|
||||||
},
|
},
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useCardCertificateData: jest.fn(),
|
}));
|
||||||
useCardCourseRunData: jest.fn(),
|
|
||||||
useCardEnrollmentData: jest.fn(),
|
jest.mock('data/hooks', () => ({
|
||||||
useCardGradeData: jest.fn(),
|
useInitializeLearnerHome: jest.fn(),
|
||||||
usePlatformSettingsData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const defaultCertificate = {
|
const defaultCertificate = {
|
||||||
@@ -35,9 +35,14 @@ const supportEmail = 'suport@email.com';
|
|||||||
const billingEmail = 'billing@email.com';
|
const billingEmail = 'billing@email.com';
|
||||||
|
|
||||||
describe('CertificateBanner', () => {
|
describe('CertificateBanner', () => {
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValue({
|
useCourseData.mockReturnValue({
|
||||||
minPassingGrade: 0.8,
|
enrollment: {},
|
||||||
progressUrl: 'progressUrl',
|
certificate: {},
|
||||||
|
gradeData: {},
|
||||||
|
courseRun: {
|
||||||
|
minPassingGrade: 0.8,
|
||||||
|
progressUrl: 'progressUrl',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const createWrapper = ({
|
const createWrapper = ({
|
||||||
certificate = {},
|
certificate = {},
|
||||||
@@ -46,11 +51,17 @@ describe('CertificateBanner', () => {
|
|||||||
courseRun = {},
|
courseRun = {},
|
||||||
platformSettings = {},
|
platformSettings = {},
|
||||||
}) => {
|
}) => {
|
||||||
reduxHooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
|
useCourseData.mockReturnValue({
|
||||||
reduxHooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
|
enrollment: { ...defaultEnrollment, ...enrollment },
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
|
certificate: { ...defaultCertificate, ...certificate },
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
|
gradeData: { ...defaultGrade, ...grade },
|
||||||
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings });
|
courseRun: {
|
||||||
|
...defaultCourseRun,
|
||||||
|
...courseRun,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const lernearData = { data: { platformSettings: { ...defaultPlatformSettings, ...platformSettings } } };
|
||||||
|
useInitializeLearnerHome.mockReturnValue(lernearData);
|
||||||
return render(<IntlProvider locale="en"><CertificateBanner {...props} /></IntlProvider>);
|
return render(<IntlProvider locale="en"><CertificateBanner {...props} /></IntlProvider>);
|
||||||
};
|
};
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -222,7 +233,8 @@ describe('CertificateBanner', () => {
|
|||||||
isPassing: true,
|
isPassing: true,
|
||||||
},
|
},
|
||||||
certificate: {
|
certificate: {
|
||||||
isEarnedButUnavailable: true,
|
isEarned: true,
|
||||||
|
availableDate: '10/20/3030',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const banner = screen.getByRole('alert');
|
const banner = screen.getByRole('alert');
|
||||||
@@ -239,4 +251,27 @@ describe('CertificateBanner', () => {
|
|||||||
const banner = screen.queryByRole('alert');
|
const banner = screen.queryByRole('alert');
|
||||||
expect(banner).toBeNull();
|
expect(banner).toBeNull();
|
||||||
});
|
});
|
||||||
|
it('should use default values when courseData is empty or undefined', () => {
|
||||||
|
useCourseData.mockReturnValue({});
|
||||||
|
const lernearData = { data: { platformSettings: { supportEmail } } };
|
||||||
|
useInitializeLearnerHome.mockReturnValue(lernearData);
|
||||||
|
render(<IntlProvider locale="en"><CertificateBanner cardId="test-card" /></IntlProvider>);
|
||||||
|
|
||||||
|
const mockedUseMemo = jest.spyOn(React, 'useMemo');
|
||||||
|
const useMemoCall = mockedUseMemo.mock.calls.find(call => call[1].some(dep => dep === undefined || dep === null));
|
||||||
|
|
||||||
|
if (useMemoCall) {
|
||||||
|
const result = useMemoCall[0]();
|
||||||
|
|
||||||
|
expect(result.certificate).toEqual({});
|
||||||
|
expect(result.isVerified).toBe(false);
|
||||||
|
expect(result.isAudit).toBe(false);
|
||||||
|
expect(result.isPassing).toBe(false);
|
||||||
|
expect(result.isArchived).toBe(false);
|
||||||
|
expect(result.minPassingGrade).toBe(0);
|
||||||
|
expect(result.progressUrl).toBeDefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
mockedUseMemo.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
/* eslint-disable max-len */
|
/* eslint-disable max-len */
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Hyperlink } from '@openedx/paragon';
|
import { Hyperlink } from '@openedx/paragon';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { utilHooks, reduxHooks } from 'hooks';
|
import { utilHooks, useCourseData } from 'hooks';
|
||||||
import Banner from 'components/Banner';
|
import Banner from 'components/Banner';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const CourseBanner = ({ cardId }) => {
|
export const CourseBanner = ({ cardId }) => {
|
||||||
const {
|
|
||||||
isVerified,
|
|
||||||
isAuditAccessExpired,
|
|
||||||
coursewareAccess = {},
|
|
||||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
|
||||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const courseData = useCourseData(cardId);
|
||||||
|
const {
|
||||||
|
isVerified = false,
|
||||||
|
isAuditAccessExpired = false,
|
||||||
|
coursewareAccess = {},
|
||||||
|
} = useMemo(() => ({
|
||||||
|
isVerified: courseData.enrollment?.isVerified,
|
||||||
|
isAuditAccessExpired: courseData.enrollment?.isAuditAccessExpired,
|
||||||
|
coursewareAccess: courseData.enrollment?.coursewareAccess || {},
|
||||||
|
}), [courseData]);
|
||||||
|
const courseRun = courseData?.courseRun || {};
|
||||||
const formatDate = utilHooks.useFormatDate();
|
const formatDate = utilHooks.useFormatDate();
|
||||||
|
|
||||||
const { hasUnmetPrerequisites, isStaff, isTooEarly } = coursewareAccess;
|
const { hasUnmetPrerequisites, isStaff, isTooEarly } = coursewareAccess;
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import { formatMessage } from 'testUtils';
|
import { formatMessage } from 'testUtils';
|
||||||
import { CourseBanner } from './CourseBanner';
|
import { CourseBanner } from './CourseBanner';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
|
useCourseData: jest.fn(),
|
||||||
utilHooks: {
|
utilHooks: {
|
||||||
useFormatDate: () => date => date,
|
useFormatDate: () => date => date,
|
||||||
},
|
},
|
||||||
reduxHooks: {
|
|
||||||
useCardCourseRunData: jest.fn(),
|
|
||||||
useCardEnrollmentData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
@@ -39,13 +36,15 @@ const renderCourseBanner = (overrides = {}) => {
|
|||||||
courseRun = {},
|
courseRun = {},
|
||||||
enrollment = {},
|
enrollment = {},
|
||||||
} = overrides;
|
} = overrides;
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
useCourseData.mockReturnValue({
|
||||||
...courseRunData,
|
courseRun: {
|
||||||
...courseRun,
|
...courseRunData,
|
||||||
});
|
...courseRun,
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
},
|
||||||
...enrollmentData,
|
enrollment: {
|
||||||
...enrollment,
|
...enrollmentData,
|
||||||
|
...enrollment,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return render(<IntlProvider locale="en"><CourseBanner cardId={cardId} /></IntlProvider>);
|
return render(<IntlProvider locale="en"><CourseBanner cardId={cardId} /></IntlProvider>);
|
||||||
};
|
};
|
||||||
@@ -53,13 +52,20 @@ const renderCourseBanner = (overrides = {}) => {
|
|||||||
describe('CourseBanner', () => {
|
describe('CourseBanner', () => {
|
||||||
it('initializes data with course number from enrollment, course and course run data', () => {
|
it('initializes data with course number from enrollment, course and course run data', () => {
|
||||||
renderCourseBanner();
|
renderCourseBanner();
|
||||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
|
||||||
});
|
});
|
||||||
it('no display if learner is verified', () => {
|
it('no display if learner is verified', () => {
|
||||||
renderCourseBanner({ enrollment: { isVerified: true } });
|
renderCourseBanner({ enrollment: { isVerified: true } });
|
||||||
expect(screen.queryByRole('alert')).toBeNull();
|
expect(screen.queryByRole('alert')).toBeNull();
|
||||||
});
|
});
|
||||||
|
it('should use default values when enrollment data is undefined', () => {
|
||||||
|
renderCourseBanner({
|
||||||
|
enrollment: undefined,
|
||||||
|
courseRun: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(useCourseData).toHaveBeenCalledWith('test-card-id');
|
||||||
|
});
|
||||||
describe('audit access expired', () => {
|
describe('audit access expired', () => {
|
||||||
it('should display correct message and link', () => {
|
it('should display correct message and link', () => {
|
||||||
renderCourseBanner({ enrollment: { isAuditAccessExpired: true } });
|
renderCourseBanner({ enrollment: { isAuditAccessExpired: true } });
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
|
|
||||||
import ApprovedContent from './views/ApprovedContent';
|
import ApprovedContent from './views/ApprovedContent';
|
||||||
import EligibleContent from './views/EligibleContent';
|
import EligibleContent from './views/EligibleContent';
|
||||||
@@ -15,9 +17,29 @@ export const statusComponents = StrictDict({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const useCreditBannerData = (cardId) => {
|
export const useCreditBannerData = (cardId) => {
|
||||||
const credit = reduxHooks.useCardCreditData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||||
if (!credit.isEligible) { return null; }
|
const supportEmail = useMemo(
|
||||||
|
() => (learnerHomeData?.platformSettings?.supportEmail),
|
||||||
|
[learnerHomeData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const credit = useMemo(() => {
|
||||||
|
const creditData = courseData?.credit;
|
||||||
|
if (!creditData || Object.keys(creditData).length === 0) {
|
||||||
|
return { isEligible: false };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isEligible: true,
|
||||||
|
providerStatusUrl: creditData.providerStatusUrl,
|
||||||
|
providerName: creditData.providerName,
|
||||||
|
providerId: creditData.providerId,
|
||||||
|
error: creditData.error,
|
||||||
|
purchased: creditData.purchased,
|
||||||
|
requestStatus: creditData.requestStatus,
|
||||||
|
};
|
||||||
|
}, [courseData]);
|
||||||
|
if (!credit.isEligible || !courseData?.credit?.isEligible) { return null; }
|
||||||
|
|
||||||
const { error, purchased, requestStatus } = credit;
|
const { error, purchased, requestStatus } = credit;
|
||||||
let ContentComponent = EligibleContent;
|
let ContentComponent = EligibleContent;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { keyStore } from 'utils';
|
import { keyStore } from 'utils';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
|
||||||
import ApprovedContent from './views/ApprovedContent';
|
import ApprovedContent from './views/ApprovedContent';
|
||||||
import EligibleContent from './views/EligibleContent';
|
import EligibleContent from './views/EligibleContent';
|
||||||
@@ -9,12 +10,19 @@ import RejectedContent from './views/RejectedContent';
|
|||||||
|
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('react', () => ({
|
||||||
reduxHooks: {
|
...jest.requireActual('react'),
|
||||||
useCardCreditData: jest.fn(),
|
useMemo: (fn) => fn(),
|
||||||
usePlatformSettingsData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks', () => ({
|
||||||
|
useCourseData: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('data/hooks', () => ({
|
||||||
|
useInitializeLearnerHome: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('./views/ApprovedContent', () => 'ApprovedContent');
|
jest.mock('./views/ApprovedContent', () => 'ApprovedContent');
|
||||||
jest.mock('./views/EligibleContent', () => 'EligibleContent');
|
jest.mock('./views/EligibleContent', () => 'EligibleContent');
|
||||||
jest.mock('./views/MustRequestContent', () => 'MustRequestContent');
|
jest.mock('./views/MustRequestContent', () => 'MustRequestContent');
|
||||||
@@ -34,18 +42,18 @@ const defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadHook = (creditData = {}) => {
|
const loadHook = (creditData = {}) => {
|
||||||
reduxHooks.useCardCreditData.mockReturnValue({ ...defaultProps, ...creditData });
|
useCourseData.mockReturnValue({ credit: { ...defaultProps, ...creditData } });
|
||||||
out = hooks.useCreditBannerData(cardId);
|
out = hooks.useCreditBannerData(cardId);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('useCreditBannerData hook', () => {
|
describe('useCreditBannerData hook', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
useInitializeLearnerHome.mockReturnValue({ data: { platformSettings: { supportEmail } } });
|
||||||
});
|
});
|
||||||
it('loads card credit data with cardID and loads platform settings data', () => {
|
it('loads card credit data with cardID and loads platform settings data', () => {
|
||||||
loadHook({ isEligible: false });
|
loadHook({ isEligible: false });
|
||||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
expect(reduxHooks.usePlatformSettingsData).toHaveBeenCalledWith();
|
expect(useInitializeLearnerHome).toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
describe('non-credit-eligible learner', () => {
|
describe('non-credit-eligible learner', () => {
|
||||||
it('returns null if the learner is not credit eligible', () => {
|
it('returns null if the learner is not credit eligible', () => {
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||||
import CreditContent from './components/CreditContent';
|
import CreditContent from './components/CreditContent';
|
||||||
import ProviderLink from './components/ProviderLink';
|
import ProviderLink from './components/ProviderLink';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const ApprovedContent = ({ cardId }) => {
|
export const ApprovedContent = ({ cardId }) => {
|
||||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
const { providerStatusUrl: href, providerName } = useMemo(() => {
|
||||||
|
const creditData = courseData?.credit;
|
||||||
|
return {
|
||||||
|
providerStatusUrl: creditData.providerStatusUrl,
|
||||||
|
providerName: creditData.providerName,
|
||||||
|
};
|
||||||
|
}, [courseData]);
|
||||||
|
const isMasquerading = useIsMasquerading();
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
return (
|
return (
|
||||||
<CreditContent
|
<CreditContent
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { formatMessage } from 'testUtils';
|
import { formatMessage } from 'testUtils';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import ApprovedContent from './ApprovedContent';
|
import ApprovedContent from './ApprovedContent';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useCardCreditData: jest.fn(),
|
useIsMasquerading: jest.fn(),
|
||||||
useMasqueradeData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
@@ -17,14 +15,14 @@ const credit = {
|
|||||||
providerStatusUrl: 'test-credit-provider-status-url',
|
providerStatusUrl: 'test-credit-provider-status-url',
|
||||||
providerName: 'test-credit-provider-name',
|
providerName: 'test-credit-provider-name',
|
||||||
};
|
};
|
||||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
useCourseData.mockReturnValue({ credit });
|
||||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
useIsMasquerading.mockReturnValue(false);
|
||||||
|
|
||||||
describe('ApprovedContent component', () => {
|
describe('ApprovedContent component', () => {
|
||||||
describe('hooks', () => {
|
describe('hooks', () => {
|
||||||
it('initializes credit data with cardId', () => {
|
it('initializes credit data with cardId', () => {
|
||||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
@@ -56,7 +54,7 @@ describe('ApprovedContent component', () => {
|
|||||||
});
|
});
|
||||||
describe('when masquerading', () => {
|
describe('when masquerading', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
useIsMasquerading.mockReturnValue(true);
|
||||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
|
|
||||||
import CreditContent from './components/CreditContent';
|
import CreditContent from './components/CreditContent';
|
||||||
@@ -11,8 +11,9 @@ import messages from './messages';
|
|||||||
|
|
||||||
export const EligibleContent = ({ cardId }) => {
|
export const EligibleContent = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { providerName } = reduxHooks.useCardCreditData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
const { courseId } = reduxHooks.useCardCourseRunData(cardId);
|
const providerName = courseData?.credit?.providerName;
|
||||||
|
const courseId = courseData?.courseRun?.courseId;
|
||||||
|
|
||||||
const onClick = track.credit.purchase(courseId);
|
const onClick = track.credit.purchase(courseId);
|
||||||
const getCredit = formatMessage(messages.getCredit);
|
const getCredit = formatMessage(messages.getCredit);
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import EligibleContent from './EligibleContent';
|
import EligibleContent from './EligibleContent';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useCardCreditData: jest.fn(),
|
|
||||||
useCardCourseRunData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('tracking', () => ({
|
jest.mock('tracking', () => ({
|
||||||
@@ -26,8 +23,7 @@ const courseId = 'test-course-id';
|
|||||||
const credit = {
|
const credit = {
|
||||||
providerName: 'test-credit-provider-name',
|
providerName: 'test-credit-provider-name',
|
||||||
};
|
};
|
||||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
useCourseData.mockReturnValue({ credit, courseRun: { courseId } });
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
|
|
||||||
|
|
||||||
const renderEligibleContent = () => render(<IntlProvider locale="en" messages={{}}><EligibleContent cardId={cardId} /></IntlProvider>);
|
const renderEligibleContent = () => render(<IntlProvider locale="en" messages={{}}><EligibleContent cardId={cardId} /></IntlProvider>);
|
||||||
|
|
||||||
@@ -35,11 +31,7 @@ describe('EligibleContent component', () => {
|
|||||||
describe('hooks', () => {
|
describe('hooks', () => {
|
||||||
it('initializes credit data with cardId', () => {
|
it('initializes credit data with cardId', () => {
|
||||||
renderEligibleContent();
|
renderEligibleContent();
|
||||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
});
|
|
||||||
it('initializes course run data with cardId', () => {
|
|
||||||
renderEligibleContent();
|
|
||||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('behavior', () => {
|
describe('behavior', () => {
|
||||||
@@ -63,7 +55,7 @@ describe('EligibleContent component', () => {
|
|||||||
expect(eligibleMessage).toHaveTextContent(credit.providerName);
|
expect(eligibleMessage).toHaveTextContent(credit.providerName);
|
||||||
});
|
});
|
||||||
it('message is formatted eligible message if no provider', () => {
|
it('message is formatted eligible message if no provider', () => {
|
||||||
reduxHooks.useCardCreditData.mockReturnValue({});
|
useCourseData.mockReturnValue({ credit: {}, courseRun: { courseId } });
|
||||||
renderEligibleContent();
|
renderEligibleContent();
|
||||||
const eligibleMessage = screen.getByTestId('credit-msg');
|
const eligibleMessage = screen.getByTestId('credit-msg');
|
||||||
expect(eligibleMessage).toBeInTheDocument();
|
expect(eligibleMessage).toBeInTheDocument();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useIsMasquerading } from 'hooks';
|
||||||
import CreditContent from './components/CreditContent';
|
import CreditContent from './components/CreditContent';
|
||||||
import ProviderLink from './components/ProviderLink';
|
import ProviderLink from './components/ProviderLink';
|
||||||
import hooks from './hooks';
|
import hooks from './hooks';
|
||||||
@@ -13,7 +13,7 @@ import messages from './messages';
|
|||||||
export const MustRequestContent = ({ cardId }) => {
|
export const MustRequestContent = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
|
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
|
||||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
const isMasquerading = useIsMasquerading();
|
||||||
return (
|
return (
|
||||||
<CreditContent
|
<CreditContent
|
||||||
action={{
|
action={{
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import hooks from './hooks';
|
import hooks from './hooks';
|
||||||
import MustRequestContent from './MustRequestContent';
|
import MustRequestContent from './MustRequestContent';
|
||||||
@@ -12,10 +11,8 @@ jest.mock('./hooks', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useMasqueradeData: jest.fn(),
|
useIsMasquerading: jest.fn(),
|
||||||
useCardCreditData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
@@ -44,10 +41,12 @@ describe('MustRequestContent component', () => {
|
|||||||
requestData,
|
requestData,
|
||||||
createCreditRequest,
|
createCreditRequest,
|
||||||
});
|
});
|
||||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
useIsMasquerading.mockReturnValue(false);
|
||||||
reduxHooks.useCardCreditData.mockReturnValue({
|
useCourseData.mockReturnValue({
|
||||||
providerName,
|
credit: {
|
||||||
providerStatusUrl,
|
providerName,
|
||||||
|
providerStatusUrl,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,7 +89,7 @@ describe('MustRequestContent component', () => {
|
|||||||
|
|
||||||
describe('when masquerading', () => {
|
describe('when masquerading', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
useIsMasquerading.mockReturnValue(true);
|
||||||
renderMustRequestContent();
|
renderMustRequestContent();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||||
import CreditContent from './components/CreditContent';
|
import CreditContent from './components/CreditContent';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const PendingContent = ({ cardId }) => {
|
export const PendingContent = ({ cardId }) => {
|
||||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
const { providerStatusUrl: href, providerName } = courseData?.credit || {};
|
||||||
|
const isMasquerading = useIsMasquerading();
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
return (
|
return (
|
||||||
<CreditContent
|
<CreditContent
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import PendingContent from './PendingContent';
|
import PendingContent from './PendingContent';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
|
useCourseData: jest.fn(),
|
||||||
|
useIsMasquerading: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
const providerName = 'test-credit-provider-name';
|
const providerName = 'test-credit-provider-name';
|
||||||
const providerStatusUrl = 'test-credit-provider-status-url';
|
const providerStatusUrl = 'test-credit-provider-status-url';
|
||||||
reduxHooks.useCardCreditData.mockReturnValue({
|
useIsMasquerading.mockReturnValue(false);
|
||||||
providerName,
|
useCourseData.mockReturnValue({
|
||||||
providerStatusUrl,
|
credit: {
|
||||||
|
providerName,
|
||||||
|
providerStatusUrl,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
|
||||||
|
|
||||||
const renderPendingContent = () => render(
|
const renderPendingContent = () => render(
|
||||||
<IntlProvider messages={{}} locale="en">
|
<IntlProvider messages={{}} locale="en">
|
||||||
@@ -28,7 +30,7 @@ describe('PendingContent component', () => {
|
|||||||
describe('hooks', () => {
|
describe('hooks', () => {
|
||||||
it('initializes card credit data with cardId', () => {
|
it('initializes card credit data with cardId', () => {
|
||||||
renderPendingContent();
|
renderPendingContent();
|
||||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('behavior', () => {
|
describe('behavior', () => {
|
||||||
@@ -56,7 +58,7 @@ describe('PendingContent component', () => {
|
|||||||
});
|
});
|
||||||
describe('when masqueradeData is true', () => {
|
describe('when masqueradeData is true', () => {
|
||||||
it('disables the view details button', () => {
|
it('disables the view details button', () => {
|
||||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
useIsMasquerading.mockReturnValue(true);
|
||||||
renderPendingContent();
|
renderPendingContent();
|
||||||
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
|
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
|
||||||
expect(button).toHaveClass('disabled');
|
expect(button).toHaveClass('disabled');
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import CreditContent from './components/CreditContent';
|
import CreditContent from './components/CreditContent';
|
||||||
import ProviderLink from './components/ProviderLink';
|
import ProviderLink from './components/ProviderLink';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const RejectedContent = ({ cardId }) => {
|
export const RejectedContent = ({ cardId }) => {
|
||||||
const credit = reduxHooks.useCardCreditData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
|
const credit = courseData?.credit;
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
return (
|
return (
|
||||||
<CreditContent
|
<CreditContent
|
||||||
message={formatMessage(messages.rejected, {
|
message={formatMessage(messages.rejected, {
|
||||||
providerName: credit.providerName,
|
providerName: credit?.providerName,
|
||||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import RejectedContent from './RejectedContent';
|
import RejectedContent from './RejectedContent';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useCardCreditData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
@@ -15,7 +13,9 @@ const credit = {
|
|||||||
providerStatusUrl: 'test-credit-provider-status-url',
|
providerStatusUrl: 'test-credit-provider-status-url',
|
||||||
providerName: 'test-credit-provider-name',
|
providerName: 'test-credit-provider-name',
|
||||||
};
|
};
|
||||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
useCourseData.mockReturnValue({
|
||||||
|
credit,
|
||||||
|
});
|
||||||
|
|
||||||
const renderRejectedContent = () => render(<IntlProvider><RejectedContent cardId={cardId} /></IntlProvider>);
|
const renderRejectedContent = () => render(<IntlProvider><RejectedContent cardId={cardId} /></IntlProvider>);
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ describe('RejectedContent component', () => {
|
|||||||
describe('hooks', () => {
|
describe('hooks', () => {
|
||||||
it('initializes credit data with cardId', () => {
|
it('initializes credit data with cardId', () => {
|
||||||
renderRejectedContent();
|
renderRejectedContent();
|
||||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import { Hyperlink } from '@openedx/paragon';
|
import { Hyperlink } from '@openedx/paragon';
|
||||||
|
|
||||||
export const ProviderLink = ({ cardId }) => {
|
export const ProviderLink = ({ cardId }) => {
|
||||||
const credit = reduxHooks.useCardCreditData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
|
const credit = courseData?.credit || {};
|
||||||
return (
|
return (
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
href={credit.providerStatusUrl}
|
href={credit.providerStatusUrl}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useCourseData } from 'hooks';
|
||||||
|
|
||||||
import ProviderLink from './ProviderLink';
|
import ProviderLink from './ProviderLink';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useCardCreditData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
@@ -23,12 +21,12 @@ const renderProviderLink = () => render(
|
|||||||
describe('ProviderLink component', () => {
|
describe('ProviderLink component', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
useCourseData.mockReturnValue({ credit });
|
||||||
renderProviderLink();
|
renderProviderLink();
|
||||||
});
|
});
|
||||||
describe('hooks', () => {
|
describe('hooks', () => {
|
||||||
it('initializes credit hook with cardId', () => {
|
it('initializes credit hook with cardId', () => {
|
||||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
import { apiHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
|
import { useCreateCreditRequest } from 'data/hooks';
|
||||||
|
|
||||||
import * as module from './hooks';
|
import * as module from './hooks';
|
||||||
|
|
||||||
@@ -11,13 +12,19 @@ export const state = StrictDict({
|
|||||||
|
|
||||||
export const useCreditRequestData = (cardId) => {
|
export const useCreditRequestData = (cardId) => {
|
||||||
const [requestData, setRequestData] = module.state.creditRequestData(null);
|
const [requestData, setRequestData] = module.state.creditRequestData(null);
|
||||||
const createCreditApiRequest = apiHooks.useCreateCreditRequest(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
|
const providerId = courseData?.credit?.providerId;
|
||||||
|
const { authenticatedUser: { username } } = React.useContext(AppContext);
|
||||||
|
const courseId = courseData?.courseRun?.courseId;
|
||||||
|
const { mutate: createCreditMutation } = useCreateCreditRequest();
|
||||||
|
|
||||||
const createCreditRequest = (e) => {
|
const createCreditRequest = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
createCreditApiRequest()
|
createCreditMutation({ providerId, courseId, username }, {
|
||||||
.then((request) => {
|
onSuccess: (response) => {
|
||||||
setRequestData(request.data);
|
setRequestData(response.data);
|
||||||
});
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
return { requestData, createCreditRequest };
|
return { requestData, createCreditRequest };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { MockUseState } from 'testUtils';
|
|
||||||
import { apiHooks } from 'hooks';
|
|
||||||
import * as hooks from './hooks';
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
apiHooks: {
|
|
||||||
useCreateCreditRequest: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const state = new MockUseState(hooks);
|
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
|
||||||
const requestData = { data: 'request data' };
|
|
||||||
const creditRequest = jest.fn().mockReturnValue(Promise.resolve(requestData));
|
|
||||||
apiHooks.useCreateCreditRequest.mockReturnValue(creditRequest);
|
|
||||||
const event = { preventDefault: jest.fn() };
|
|
||||||
|
|
||||||
let out;
|
|
||||||
describe('Credit Banner view hooks', () => {
|
|
||||||
describe('state', () => {
|
|
||||||
state.testGetter(state.keys.creditRequestData);
|
|
||||||
});
|
|
||||||
describe('useCreditRequestData', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
state.mock();
|
|
||||||
out = hooks.useCreditRequestData(cardId);
|
|
||||||
});
|
|
||||||
describe('behavior', () => {
|
|
||||||
it('initializes creditRequestData state field with null value', () => {
|
|
||||||
state.expectInitializedWith(state.keys.creditRequestData, null);
|
|
||||||
});
|
|
||||||
it('calls useCreateCreditRequest with passed cardID', () => {
|
|
||||||
expect(apiHooks.useCreateCreditRequest).toHaveBeenCalledWith(cardId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('output', () => {
|
|
||||||
it('returns requestData state value', () => {
|
|
||||||
state.mockVal(state.keys.creditRequestData, requestData);
|
|
||||||
out = hooks.useCreditRequestData(cardId);
|
|
||||||
expect(out.requestData).toEqual(requestData);
|
|
||||||
});
|
|
||||||
describe('createCreditRequest', () => {
|
|
||||||
it('returns an event handler that prevents default click behavior', () => {
|
|
||||||
out.createCreditRequest(event);
|
|
||||||
expect(event.preventDefault).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
it('calls api.createCreditRequest and sets requestData with the response', async () => {
|
|
||||||
await out.createCreditRequest(event);
|
|
||||||
expect(creditRequest).toHaveBeenCalledWith();
|
|
||||||
expect(state.setState.creditRequestData).toHaveBeenCalledWith(requestData.data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import React from 'react';
|
||||||
|
import * as api from 'data/services/lms/api';
|
||||||
|
import { useCourseData } from 'hooks';
|
||||||
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
|
import * as hooks from './hooks';
|
||||||
|
|
||||||
|
jest.mock('data/services/lms/api', () => ({
|
||||||
|
createCreditRequest: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks', () => ({
|
||||||
|
useCourseData: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||||
|
...jest.requireActual('@edx/frontend-platform/logging'),
|
||||||
|
logError: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const createWrapper = () => {
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: { retry: false },
|
||||||
|
mutations: { retry: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<AppContext.Provider value={{
|
||||||
|
authenticatedUser: { username: 'test-user' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AppContext.Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useCreditRequestData', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = createWrapper();
|
||||||
|
(useCourseData as jest.Mock).mockReturnValue({
|
||||||
|
credit: { providerId: 'provider-123' },
|
||||||
|
courseRun: { courseId: 'course-456' },
|
||||||
|
});
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes requestData as null', () => {
|
||||||
|
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current.requestData).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns createCreditRequest function', () => {
|
||||||
|
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||||
|
|
||||||
|
expect(typeof result.current.createCreditRequest).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents default event behavior', async () => {
|
||||||
|
const event = { preventDefault: jest.fn() };
|
||||||
|
(api.createCreditRequest as jest.Mock).mockResolvedValue({ data: 'success' });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.createCreditRequest(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls API with correct parameters', async () => {
|
||||||
|
const event = { preventDefault: jest.fn() };
|
||||||
|
(api.createCreditRequest as jest.Mock).mockResolvedValue({ data: 'success' });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.createCreditRequest(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||||
|
providerId: 'provider-123',
|
||||||
|
courseId: 'course-456',
|
||||||
|
username: 'test-user',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets requestData with response data on success', async () => {
|
||||||
|
const event = { preventDefault: jest.fn() };
|
||||||
|
const responseData = { data: { id: 'credit-123', status: 'pending' } };
|
||||||
|
(api.createCreditRequest as jest.Mock).mockResolvedValue(responseData);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.createCreditRequest(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||||
|
providerId: 'provider-123',
|
||||||
|
courseId: 'course-456',
|
||||||
|
username: 'test-user',
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.requestData).toEqual(responseData.data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles missing providerId gracefully', async () => {
|
||||||
|
const event = { preventDefault: jest.fn() };
|
||||||
|
(useCourseData as jest.Mock).mockReturnValue({
|
||||||
|
credit: null,
|
||||||
|
courseRun: { courseId: 'course-456' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.createCreditRequest(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||||
|
providerId: undefined,
|
||||||
|
courseId: 'course-456',
|
||||||
|
username: 'test-user',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles missing courseId gracefully', async () => {
|
||||||
|
const event = { preventDefault: jest.fn() };
|
||||||
|
(useCourseData as jest.Mock).mockReturnValue({
|
||||||
|
credit: { providerId: 'provider-123' },
|
||||||
|
courseRun: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.createCreditRequest(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||||
|
providerId: 'provider-123',
|
||||||
|
courseId: undefined,
|
||||||
|
username: 'test-user',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles API errors without crashing', async () => {
|
||||||
|
const event = { preventDefault: jest.fn() };
|
||||||
|
(api.createCreditRequest as jest.Mock).mockRejectedValue(new Error('API Error'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.createCreditRequest(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.requestData).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses cardId to fetch course data', () => {
|
||||||
|
renderHook(() => hooks.useCreditRequestData('different-card'), { wrapper });
|
||||||
|
|
||||||
|
expect(useCourseData).toHaveBeenCalledWith('different-card');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles undefined response data', async () => {
|
||||||
|
const event = { preventDefault: jest.fn() };
|
||||||
|
(api.createCreditRequest as jest.Mock).mockResolvedValue({ status: 200 });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.createCreditRequest(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.requestData).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Button, MailtoLink } from '@openedx/paragon';
|
import { Button, MailtoLink } from '@openedx/paragon';
|
||||||
|
|
||||||
import { utilHooks, reduxHooks } from 'hooks';
|
import { utilHooks, useCourseData, useEntitlementInfo } from 'hooks';
|
||||||
|
import { useSelectSessionModal } from 'data/context';
|
||||||
import Banner from 'components/Banner';
|
import Banner from 'components/Banner';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const EntitlementBanner = ({ cardId }) => {
|
export const EntitlementBanner = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||||
|
const courseData = useCourseData(cardId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEntitlement,
|
isEntitlement,
|
||||||
hasSessions,
|
hasSessions,
|
||||||
@@ -18,9 +23,12 @@ export const EntitlementBanner = ({ cardId }) => {
|
|||||||
changeDeadline,
|
changeDeadline,
|
||||||
showExpirationWarning,
|
showExpirationWarning,
|
||||||
isExpired,
|
isExpired,
|
||||||
} = reduxHooks.useCardEntitlementData(cardId);
|
} = useEntitlementInfo(courseData);
|
||||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
const supportEmail = useMemo(
|
||||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
() => learnerHomeData?.platformSettings?.supportEmail,
|
||||||
|
[learnerHomeData],
|
||||||
|
);
|
||||||
|
const { updateSelectSessionModal } = useSelectSessionModal();
|
||||||
const formatDate = utilHooks.useFormatDate();
|
const formatDate = utilHooks.useFormatDate();
|
||||||
|
|
||||||
if (!isEntitlement) {
|
if (!isEntitlement) {
|
||||||
@@ -42,7 +50,7 @@ export const EntitlementBanner = ({ cardId }) => {
|
|||||||
{formatMessage(messages.entitlementExpiringSoon, {
|
{formatMessage(messages.entitlementExpiringSoon, {
|
||||||
changeDeadline: formatDate(changeDeadline),
|
changeDeadline: formatDate(changeDeadline),
|
||||||
selectSessionButton: (
|
selectSessionButton: (
|
||||||
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
|
<Button variant="link" size="inline" className="m-0 p-0" onClick={() => updateSelectSessionModal(cardId)}>
|
||||||
{formatMessage(messages.selectSession)}
|
{formatMessage(messages.selectSession)}
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,22 +1,40 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { formatMessage } from 'testUtils';
|
import { formatMessage } from 'testUtils';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import EntitlementBanner from './EntitlementBanner';
|
import EntitlementBanner from './EntitlementBanner';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
|
jest.mock('react', () => ({
|
||||||
|
...jest.requireActual('react'),
|
||||||
|
useMemo: (fn) => fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('data/hooks', () => ({
|
||||||
|
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||||
|
data: {
|
||||||
|
platformSettings: {
|
||||||
|
supportEmail: 'test-support-email',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
const mockUpdateSelectSessionModal = jest.fn().mockName('updateSelectSessionModal');
|
||||||
|
jest.mock('data/context/SelectSessionProvider', () => ({
|
||||||
|
useSelectSessionModal: () => ({
|
||||||
|
updateSelectSessionModal: mockUpdateSelectSessionModal,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
|
...jest.requireActual('hooks'),
|
||||||
|
useCourseData: jest.fn(),
|
||||||
utilHooks: {
|
utilHooks: {
|
||||||
useFormatDate: () => date => date,
|
useFormatDate: () => date => date?.toDateString(),
|
||||||
},
|
|
||||||
reduxHooks: {
|
|
||||||
usePlatformSettingsData: jest.fn(),
|
|
||||||
useCardEntitlementData: jest.fn(),
|
|
||||||
useUpdateSelectSessionModalCallback: jest.fn(
|
|
||||||
(cardId) => jest.fn().mockName(`updateSelectSessionModalCallback(${cardId})`),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
@@ -32,16 +50,20 @@ const platformData = { supportEmail: 'test-support-email' };
|
|||||||
|
|
||||||
const renderComponent = (overrides = {}) => {
|
const renderComponent = (overrides = {}) => {
|
||||||
const { entitlement = {} } = overrides;
|
const { entitlement = {} } = overrides;
|
||||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
|
useCourseData.mockReturnValue({
|
||||||
reduxHooks.usePlatformSettingsData.mockReturnValueOnce(platformData);
|
entitlement: { ...entitlementData, ...entitlement },
|
||||||
|
platformSettings: platformData,
|
||||||
|
});
|
||||||
return render(<IntlProvider locale="en"><EntitlementBanner cardId={cardId} /></IntlProvider>);
|
return render(<IntlProvider locale="en"><EntitlementBanner cardId={cardId} /></IntlProvider>);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('EntitlementBanner', () => {
|
describe('EntitlementBanner', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
it('initializes data with course number from entitlement', () => {
|
it('initializes data with course number from entitlement', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(cardId);
|
|
||||||
});
|
});
|
||||||
it('no display if not an entitlement', () => {
|
it('no display if not an entitlement', () => {
|
||||||
renderComponent({ entitlement: { isEntitlement: false } });
|
renderComponent({ entitlement: { isEntitlement: false } });
|
||||||
@@ -56,7 +78,10 @@ describe('EntitlementBanner', () => {
|
|||||||
expect(banner.innerHTML).toContain(platformData.supportEmail);
|
expect(banner.innerHTML).toContain(platformData.supportEmail);
|
||||||
});
|
});
|
||||||
it('renders when expiration warning', () => {
|
it('renders when expiration warning', () => {
|
||||||
renderComponent({ entitlement: { showExpirationWarning: true } });
|
const deadline = new Date();
|
||||||
|
deadline.setDate(deadline.getDate() + 4);
|
||||||
|
const deadlineStr = `${deadline.getMonth() + 1}/${deadline.getDate()}/${deadline.getFullYear()}`;
|
||||||
|
renderComponent({ entitlement: { changeDeadline: deadlineStr, isFulfilled: false, availableSessions: [1, 2, 3] } });
|
||||||
const banner = screen.getByRole('alert');
|
const banner = screen.getByRole('alert');
|
||||||
expect(banner).toBeInTheDocument();
|
expect(banner).toBeInTheDocument();
|
||||||
expect(banner).toHaveClass('alert-info');
|
expect(banner).toHaveClass('alert-info');
|
||||||
@@ -64,9 +89,37 @@ describe('EntitlementBanner', () => {
|
|||||||
expect(button).toBeInTheDocument();
|
expect(button).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('renders expired banner', () => {
|
it('renders expired banner', () => {
|
||||||
renderComponent({ entitlement: { isExpired: true } });
|
renderComponent({ entitlement: { isExpired: true, availableSessions: [1, 2, 3] } });
|
||||||
const banner = screen.getByRole('alert');
|
const banner = screen.getByRole('alert');
|
||||||
expect(banner).toBeInTheDocument();
|
expect(banner).toBeInTheDocument();
|
||||||
expect(banner.innerHTML).toContain(formatMessage(messages.entitlementExpired));
|
expect(banner.innerHTML).toContain(formatMessage(messages.entitlementExpired));
|
||||||
});
|
});
|
||||||
|
it('should call updateSelectSessionModal with cardId when select session button is clicked', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const deadline = new Date();
|
||||||
|
deadline.setDate(deadline.getDate() + 4);
|
||||||
|
const deadlineStr = `${deadline.getMonth() + 1}/${deadline.getDate()}/${deadline.getFullYear()}`;
|
||||||
|
renderComponent({ entitlement: { changeDeadline: deadlineStr, isFulfilled: false, availableSessions: [1, 2, 3] } });
|
||||||
|
const banner = screen.getByRole('alert');
|
||||||
|
expect(banner).toBeInTheDocument();
|
||||||
|
expect(banner).toHaveClass('alert-info');
|
||||||
|
const button = screen.getByRole('button', { name: formatMessage(messages.selectSession) });
|
||||||
|
expect(button).toBeInTheDocument();
|
||||||
|
await user.click(button);
|
||||||
|
|
||||||
|
expect(mockUpdateSelectSessionModal).toHaveBeenCalledWith(cardId);
|
||||||
|
});
|
||||||
|
it('should return null when isExpired is false and showExpirationWarning is false', () => {
|
||||||
|
renderComponent({
|
||||||
|
entitlement: {
|
||||||
|
isEntitlement: true,
|
||||||
|
hasSessions: true,
|
||||||
|
isFulfilled: true,
|
||||||
|
showExpirationWarning: false,
|
||||||
|
isExpired: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const banner = screen.queryByRole('alert');
|
||||||
|
expect(banner).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { Program } from '@openedx/paragon/icons';
|
import { Program } from '@openedx/paragon/icons';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import Banner from 'components/Banner';
|
import Banner from 'components/Banner';
|
||||||
|
|
||||||
import ProgramList from './ProgramsList';
|
import ProgramList from './ProgramsList';
|
||||||
@@ -12,10 +12,10 @@ import messages from './messages';
|
|||||||
|
|
||||||
export const RelatedProgramsBanner = ({ cardId }) => {
|
export const RelatedProgramsBanner = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const courseData = useCourseData(cardId);
|
||||||
|
const programData = courseData?.programs;
|
||||||
|
|
||||||
const programData = reduxHooks.useCardRelatedProgramsData(cardId);
|
if (!courseData || !programData?.relatedPrograms.length) {
|
||||||
|
|
||||||
if (!programData?.length) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export const RelatedProgramsBanner = ({ cardId }) => {
|
|||||||
<span className="font-weight-bolder">
|
<span className="font-weight-bolder">
|
||||||
{formatMessage(messages.relatedPrograms)}
|
{formatMessage(messages.relatedPrograms)}
|
||||||
</span>
|
</span>
|
||||||
<ProgramList programs={programData.list} />
|
<ProgramList programs={programData.relatedPrograms} />
|
||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import RelatedProgramsBanner from '.';
|
import RelatedProgramsBanner from '.';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useCardRelatedProgramsData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
@@ -27,21 +25,21 @@ const programData = {
|
|||||||
|
|
||||||
describe('RelatedProgramsBanner', () => {
|
describe('RelatedProgramsBanner', () => {
|
||||||
it('render empty', () => {
|
it('render empty', () => {
|
||||||
reduxHooks.useCardRelatedProgramsData.mockReturnValue({});
|
useCourseData.mockReturnValue(null);
|
||||||
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
|
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
|
||||||
const banner = screen.queryByRole('alert');
|
const banner = screen.queryByRole('alert');
|
||||||
expect(banner).toBeNull();
|
expect(banner).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render with programs', () => {
|
it('render with programs', () => {
|
||||||
reduxHooks.useCardRelatedProgramsData.mockReturnValue(programData);
|
useCourseData.mockReturnValue({ programs: { relatedPrograms: programData.list } });
|
||||||
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
|
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
|
||||||
const list = screen.getByRole('list');
|
const list = screen.getByRole('list');
|
||||||
expect(list.childElementCount).toBe(programData.list.length);
|
expect(list.childElementCount).toBe(programData.list.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render related programs title', () => {
|
it('render related programs title', () => {
|
||||||
reduxHooks.useCardRelatedProgramsData.mockReturnValue(programData);
|
useCourseData.mockReturnValue({ programs: { relatedPrograms: programData.list } });
|
||||||
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
|
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
|
||||||
const title = screen.getByText('Related Programs:');
|
const title = screen.getByText('Related Programs:');
|
||||||
expect(title).toBeInTheDocument();
|
expect(title).toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
|
|
||||||
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
|
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
|
||||||
import CertificateBanner from './CertificateBanner';
|
import CertificateBanner from './CertificateBanner';
|
||||||
@@ -10,7 +10,11 @@ import EntitlementBanner from './EntitlementBanner';
|
|||||||
import RelatedProgramsBanner from './RelatedProgramsBanner';
|
import RelatedProgramsBanner from './RelatedProgramsBanner';
|
||||||
|
|
||||||
export const CourseCardBanners = ({ cardId }) => {
|
export const CourseCardBanners = ({ cardId }) => {
|
||||||
const { isEnrolled } = reduxHooks.useCardEnrollmentData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
|
if (!courseData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { isEnrolled = false } = courseData.enrollment;
|
||||||
return (
|
return (
|
||||||
<div className="course-card-banners" data-testid="CourseCardBanners">
|
<div className="course-card-banners" data-testid="CourseCardBanners">
|
||||||
<RelatedProgramsBanner cardId={cardId} />
|
<RelatedProgramsBanner cardId={cardId} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
|
|
||||||
import CourseCardBanners from '.';
|
import CourseCardBanners from '.';
|
||||||
|
|
||||||
@@ -20,9 +20,11 @@ const mockedComponents = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(() => ({
|
||||||
useCardEnrollmentData: jest.fn(() => ({ isEnrolled: true })),
|
enrollment: {
|
||||||
},
|
isEnrolled: true,
|
||||||
|
},
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('CourseCardBanners', () => {
|
describe('CourseCardBanners', () => {
|
||||||
@@ -36,8 +38,13 @@ describe('CourseCardBanners', () => {
|
|||||||
return expect(mockedComponent).toBeInTheDocument();
|
return expect(mockedComponent).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('render null with no courseData', () => {
|
||||||
|
useCourseData.mockReturnValue(null);
|
||||||
|
const { container } = render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
it('render with isEnrolled false', () => {
|
it('render with isEnrolled false', () => {
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
|
useCourseData.mockReturnValue({ enrollment: { isEnrolled: false } });
|
||||||
render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
|
render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
|
||||||
const mockedComponentsIfNotEnrolled = mockedComponents.slice(-2);
|
const mockedComponentsIfNotEnrolled = mockedComponents.slice(-2);
|
||||||
mockedComponentsIfNotEnrolled.map((componentName) => {
|
mockedComponentsIfNotEnrolled.map((componentName) => {
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { utilHooks, reduxHooks } from 'hooks';
|
import { utilHooks, useCourseData, useEntitlementInfo } from 'hooks';
|
||||||
|
import { useSelectSessionModal } from 'data/context';
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const useAccessMessage = ({ cardId }) => {
|
export const useAccessMessage = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const enrollment = reduxHooks.useCardEnrollmentData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
const { courseRun, enrollment } = courseData || {};
|
||||||
const formatDate = utilHooks.useFormatDate();
|
const formatDate = utilHooks.useFormatDate();
|
||||||
if (!courseRun.isStarted) {
|
if (!courseRun.isStarted) {
|
||||||
if (!courseRun.startDate && !courseRun.advertisedStart) { return null; }
|
if (!courseRun.startDate && !courseRun.advertisedStart) { return null; }
|
||||||
const startDate = courseRun.advertisedStart ? courseRun.advertisedStart : formatDate(courseRun.startDate);
|
const startDate = courseRun.advertisedStart ? courseRun.advertisedStart : formatDate(courseRun.startDate);
|
||||||
return formatMessage(messages.courseStarts, { startDate });
|
return formatMessage(messages.courseStarts, { startDate });
|
||||||
}
|
}
|
||||||
if (enrollment.isEnrolled) {
|
if (enrollment?.isEnrolled) {
|
||||||
const { isArchived, endDate } = courseRun;
|
const { isArchived, endDate } = courseRun;
|
||||||
const {
|
const {
|
||||||
accessExpirationDate,
|
accessExpirationDate,
|
||||||
@@ -38,15 +39,15 @@ export const useAccessMessage = ({ cardId }) => {
|
|||||||
|
|
||||||
export const useCardDetailsData = ({ cardId }) => {
|
export const useCardDetailsData = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const providerName = reduxHooks.useCardProviderData(cardId).name;
|
const courseData = useCourseData(cardId);
|
||||||
const { courseNumber } = reduxHooks.useCardCourseData(cardId);
|
const providerName = courseData?.courseProvider?.name;
|
||||||
|
const courseNumber = courseData?.course?.courseNumber;
|
||||||
const {
|
const {
|
||||||
isEntitlement,
|
isEntitlement,
|
||||||
isFulfilled,
|
isFulfilled,
|
||||||
canChange,
|
canChange,
|
||||||
} = reduxHooks.useCardEntitlementData(cardId);
|
} = useEntitlementInfo(courseData);
|
||||||
|
const { updateSelectSessionModal } = useSelectSessionModal();
|
||||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
providerName: providerName || formatMessage(messages.unknownProviderName),
|
providerName: providerName || formatMessage(messages.unknownProviderName),
|
||||||
@@ -54,7 +55,7 @@ export const useCardDetailsData = ({ cardId }) => {
|
|||||||
isEntitlement,
|
isEntitlement,
|
||||||
isFulfilled,
|
isFulfilled,
|
||||||
canChange,
|
canChange,
|
||||||
openSessionModal,
|
openSessionModal: () => updateSelectSessionModal(cardId),
|
||||||
courseNumber,
|
courseNumber,
|
||||||
changeOrLeaveSessionMessage: formatMessage(messages.changeOrLeaveSessionButton),
|
changeOrLeaveSessionMessage: formatMessage(messages.changeOrLeaveSessionButton),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { keyStore } from 'utils';
|
import { keyStore } from 'utils';
|
||||||
import { utilHooks, reduxHooks } from 'hooks';
|
import { utilHooks, useCourseData } from 'hooks';
|
||||||
|
import { useSelectSessionModal } from 'data/context';
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
|
jest.mock('react', () => ({
|
||||||
|
...jest.requireActual('react'),
|
||||||
|
useMemo: (fn) => fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateSelectSessionModalMock = jest.fn().mockName('updateSelectSessionModal');
|
||||||
|
jest.mock('data/context', () => ({
|
||||||
|
useSelectSessionModal: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
|
...jest.requireActual('hooks'),
|
||||||
|
useCourseData: jest.fn(),
|
||||||
utilHooks: {
|
utilHooks: {
|
||||||
useFormatDate: jest.fn(),
|
useFormatDate: jest.fn(),
|
||||||
},
|
},
|
||||||
reduxHooks: {
|
|
||||||
useCardCourseData: jest.fn(),
|
|
||||||
useCardCourseRunData: jest.fn(),
|
|
||||||
useCardEnrollmentData: jest.fn(),
|
|
||||||
useCardEntitlementData: jest.fn(),
|
|
||||||
useCardProviderData: jest.fn(),
|
|
||||||
useUpdateSelectSessionModalCallback: (...args) => ({ updateSelectSessionModalCallback: args }),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||||
@@ -60,15 +63,13 @@ describe('CourseCardDetails hooks', () => {
|
|||||||
const runHook = ({ provider = {}, entitlement = {} }) => {
|
const runHook = ({ provider = {}, entitlement = {} }) => {
|
||||||
jest.spyOn(hooks, hookKeys.useAccessMessage)
|
jest.spyOn(hooks, hookKeys.useAccessMessage)
|
||||||
.mockImplementationOnce(mockAccessMessage);
|
.mockImplementationOnce(mockAccessMessage);
|
||||||
reduxHooks.useCardProviderData.mockReturnValueOnce({
|
useCourseData.mockReturnValue({
|
||||||
...providerData,
|
courseProvider: { ...providerData, ...provider },
|
||||||
...provider,
|
course: { courseNumber },
|
||||||
|
courseRun: {},
|
||||||
|
entitlement: { ...entitlementData, ...entitlement },
|
||||||
});
|
});
|
||||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({
|
useSelectSessionModal.mockReturnValue({ updateSelectSessionModal: updateSelectSessionModalMock });
|
||||||
...entitlementData,
|
|
||||||
...entitlement,
|
|
||||||
});
|
|
||||||
reduxHooks.useCardCourseData.mockReturnValueOnce({ courseNumber });
|
|
||||||
out = hooks.useCardDetailsData({ cardId });
|
out = hooks.useCardDetailsData({ cardId });
|
||||||
};
|
};
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -85,6 +86,10 @@ describe('CourseCardDetails hooks', () => {
|
|||||||
it('forward changeOrLeaveSessionMessage', () => {
|
it('forward changeOrLeaveSessionMessage', () => {
|
||||||
expect(out.changeOrLeaveSessionMessage).toEqual(formatMessage(messages.changeOrLeaveSessionButton));
|
expect(out.changeOrLeaveSessionMessage).toEqual(formatMessage(messages.changeOrLeaveSessionButton));
|
||||||
});
|
});
|
||||||
|
it('calls updateSelectSessionModal when openSessionModal is called', () => {
|
||||||
|
out.openSessionModal();
|
||||||
|
expect(updateSelectSessionModalMock).toHaveBeenCalledWith(cardId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useAccessMessage', () => {
|
describe('useAccessMessage', () => {
|
||||||
@@ -101,21 +106,16 @@ describe('CourseCardDetails hooks', () => {
|
|||||||
endDate: '10/20/2000',
|
endDate: '10/20/2000',
|
||||||
};
|
};
|
||||||
const runHook = ({ enrollment = {}, courseRun = {} }) => {
|
const runHook = ({ enrollment = {}, courseRun = {} }) => {
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
useCourseData.mockReturnValue({
|
||||||
...courseRunData,
|
courseRun: { ...courseRunData, ...courseRun },
|
||||||
...courseRun,
|
enrollment: { ...enrollmentData, ...enrollment },
|
||||||
});
|
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
|
||||||
...enrollmentData,
|
|
||||||
...enrollment,
|
|
||||||
});
|
});
|
||||||
out = hooks.useAccessMessage({ cardId });
|
out = hooks.useAccessMessage({ cardId });
|
||||||
};
|
};
|
||||||
|
|
||||||
it('loads data from enrollment and course run data based on course number', () => {
|
it('loads data from enrollment and course run data based on course number', () => {
|
||||||
runHook({});
|
runHook({});
|
||||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if not started yet', () => {
|
describe('if not started yet', () => {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { baseAppUrl } from 'data/services/lms/urls';
|
||||||
|
|
||||||
import { Badge } from '@openedx/paragon';
|
import { Badge } from '@openedx/paragon';
|
||||||
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||||
import verifiedRibbon from 'assets/verified-ribbon.png';
|
import verifiedRibbon from 'assets/verified-ribbon.png';
|
||||||
import useActionDisabledState from './hooks';
|
import useActionDisabledState from './hooks';
|
||||||
|
|
||||||
@@ -15,11 +16,10 @@ const { courseImageClicked } = track.course;
|
|||||||
|
|
||||||
export const CourseCardImage = ({ cardId, orientation }) => {
|
export const CourseCardImage = ({ cardId, orientation }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { bannerImgSrc } = reduxHooks.useCardCourseData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
const { homeUrl } = courseData?.courseRun || {};
|
||||||
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
|
|
||||||
const { disableCourseTitle } = useActionDisabledState(cardId);
|
const { disableCourseTitle } = useActionDisabledState(cardId);
|
||||||
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
|
const handleImageClicked = useCourseTrackingEvent(courseImageClicked, cardId, homeUrl);
|
||||||
const wrapperClassName = `pgn__card-wrapper-image-cap d-inline-block overflow-visible ${orientation}`;
|
const wrapperClassName = `pgn__card-wrapper-image-cap d-inline-block overflow-visible ${orientation}`;
|
||||||
const image = (
|
const image = (
|
||||||
<>
|
<>
|
||||||
@@ -27,11 +27,11 @@ export const CourseCardImage = ({ cardId, orientation }) => {
|
|||||||
// w-100 is necessary for images on Safari, otherwise stretches full height of the image
|
// w-100 is necessary for images on Safari, otherwise stretches full height of the image
|
||||||
// https://stackoverflow.com/a/44250830
|
// https://stackoverflow.com/a/44250830
|
||||||
className="pgn__card-image-cap w-100 show"
|
className="pgn__card-image-cap w-100 show"
|
||||||
src={bannerImgSrc}
|
src={courseData?.course?.bannerImgSrc && baseAppUrl(courseData.course.bannerImgSrc)}
|
||||||
alt={formatMessage(messages.bannerAlt)}
|
alt={formatMessage(messages.bannerAlt)}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
isVerified && (
|
courseData?.enrollment?.isVerified && (
|
||||||
<span
|
<span
|
||||||
className="course-card-verify-ribbon-container"
|
className="course-card-verify-ribbon-container"
|
||||||
title={formatMessage(messages.verifiedHoverDescription)}
|
title={formatMessage(messages.verifiedHoverDescription)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { formatMessage } from 'testUtils';
|
import { formatMessage } from 'testUtils';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import useActionDisabledState from './hooks';
|
import useActionDisabledState from './hooks';
|
||||||
import { CourseCardImage } from './CourseCardImage';
|
import { CourseCardImage } from './CourseCardImage';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
@@ -10,14 +10,14 @@ const homeUrl = 'https://example.com';
|
|||||||
const bannerImgSrc = 'banner-img-src.jpg';
|
const bannerImgSrc = 'banner-img-src.jpg';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(() => ({
|
||||||
useCardCourseData: jest.fn(() => ({ bannerImgSrc })),
|
course: { bannerImgSrc },
|
||||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
courseRun: { homeUrl },
|
||||||
useCardEnrollmentData: jest.fn(),
|
enrollment: {},
|
||||||
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
|
})),
|
||||||
trackCourseEvent: { eventName, cardId, url },
|
useCourseTrackingEvent: jest.fn((eventName, cardId, url) => ({
|
||||||
})),
|
trackCourseEvent: { eventName, cardId, url },
|
||||||
},
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./hooks', () => jest.fn());
|
jest.mock('./hooks', () => jest.fn());
|
||||||
@@ -30,7 +30,13 @@ describe('CourseCardImage', () => {
|
|||||||
|
|
||||||
it('renders course image with correct attributes', () => {
|
it('renders course image with correct attributes', () => {
|
||||||
useActionDisabledState.mockReturnValue({ disableCourseTitle: true });
|
useActionDisabledState.mockReturnValue({ disableCourseTitle: true });
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
|
useCourseData.mockReturnValue(
|
||||||
|
{
|
||||||
|
course: { bannerImgSrc },
|
||||||
|
courseRun: { homeUrl },
|
||||||
|
enrollment: { isVerified: true },
|
||||||
|
},
|
||||||
|
);
|
||||||
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
||||||
|
|
||||||
const image = screen.getByRole('img', { name: formatMessage(messages.bannerAlt) });
|
const image = screen.getByRole('img', { name: formatMessage(messages.bannerAlt) });
|
||||||
@@ -41,7 +47,13 @@ describe('CourseCardImage', () => {
|
|||||||
|
|
||||||
it('isVerified, should render badge', () => {
|
it('isVerified, should render badge', () => {
|
||||||
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
|
useCourseData.mockReturnValue(
|
||||||
|
{
|
||||||
|
course: { bannerImgSrc },
|
||||||
|
courseRun: { homeUrl },
|
||||||
|
enrollment: { isVerified: true },
|
||||||
|
},
|
||||||
|
);
|
||||||
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
||||||
|
|
||||||
const badge = screen.getByText(formatMessage(messages.verifiedBanner));
|
const badge = screen.getByText(formatMessage(messages.verifiedBanner));
|
||||||
@@ -52,7 +64,13 @@ describe('CourseCardImage', () => {
|
|||||||
|
|
||||||
it('renders link with correct href if disableCourseTitle is false', () => {
|
it('renders link with correct href if disableCourseTitle is false', () => {
|
||||||
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: false });
|
useCourseData.mockReturnValue(
|
||||||
|
{
|
||||||
|
course: { bannerImgSrc },
|
||||||
|
courseRun: { homeUrl },
|
||||||
|
enrollment: { isVerified: false },
|
||||||
|
},
|
||||||
|
);
|
||||||
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
||||||
|
|
||||||
const link = screen.getByRole('link');
|
const link = screen.getByRole('link');
|
||||||
@@ -61,12 +79,15 @@ describe('CourseCardImage', () => {
|
|||||||
describe('hooks', () => {
|
describe('hooks', () => {
|
||||||
it('initializes', () => {
|
it('initializes', () => {
|
||||||
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
|
useCourseData.mockReturnValue(
|
||||||
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
{
|
||||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
|
course: { bannerImgSrc },
|
||||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
|
courseRun: { homeUrl },
|
||||||
props.cardId,
|
enrollment: { isVerified: true },
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
||||||
|
expect(useCourseData).toHaveBeenCalledWith(props.cardId);
|
||||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import * as ReactShare from 'react-share';
|
import * as ReactShare from 'react-share';
|
||||||
|
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Dropdown } from '@openedx/paragon';
|
import { Dropdown } from '@openedx/paragon';
|
||||||
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseTrackingEvent, useCourseData, useIsMasquerading } from 'hooks';
|
||||||
|
import { useCardSocialSettingsData } from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const testIds = {
|
export const testIds = {
|
||||||
@@ -16,14 +16,15 @@ export const testIds = {
|
|||||||
|
|
||||||
export const SocialShareMenu = ({ cardId, emailSettings }) => {
|
export const SocialShareMenu = ({ cardId, emailSettings }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const courseData = useCourseData(cardId);
|
||||||
|
const courseName = courseData?.course?.courseName;
|
||||||
|
const isExecEd2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(courseData.enrollment.mode);
|
||||||
|
const isEmailEnabled = courseData?.enrollment?.isEmailEnabled ?? false;
|
||||||
|
const { twitter, facebook } = useCardSocialSettingsData(cardId);
|
||||||
|
const isMasquerading = useIsMasquerading();
|
||||||
|
|
||||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
const handleTwitterShare = useCourseTrackingEvent(track.socialShare, cardId, 'twitter');
|
||||||
const { isEmailEnabled, isExecEd2UCourse } = reduxHooks.useCardEnrollmentData(cardId);
|
const handleFacebookShare = useCourseTrackingEvent(track.socialShare, cardId, 'facebook');
|
||||||
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
|
|
||||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
|
||||||
|
|
||||||
const handleTwitterShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'twitter');
|
|
||||||
const handleFacebookShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'facebook');
|
|
||||||
|
|
||||||
if (isExecEd2UCourse) {
|
if (isExecEd2UCourse) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseTrackingEvent, useCourseData, useIsMasquerading } from 'hooks';
|
||||||
|
|
||||||
import { useEmailSettings } from './hooks';
|
import { useEmailSettings, useCardSocialSettingsData } from './hooks';
|
||||||
import SocialShareMenu from './SocialShareMenu';
|
import SocialShareMenu from './SocialShareMenu';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
@@ -15,16 +15,13 @@ jest.mock('tracking', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useMasqueradeData: jest.fn(),
|
useCourseTrackingEvent: jest.fn((...args) => ({ trackCourseEvent: args })),
|
||||||
useCardCourseData: jest.fn(),
|
useIsMasquerading: jest.fn(),
|
||||||
useCardEnrollmentData: jest.fn(),
|
|
||||||
useCardSocialSettingsData: jest.fn(),
|
|
||||||
useTrackCourseEvent: jest.fn((...args) => ({ trackCourseEvent: args })),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('./hooks', () => ({
|
||||||
useEmailSettings: jest.fn(),
|
useEmailSettings: jest.fn(),
|
||||||
|
useCardSocialSettingsData: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
@@ -57,23 +54,25 @@ const socialShare = {
|
|||||||
|
|
||||||
const mockHooks = (returnVals = {}) => {
|
const mockHooks = (returnVals = {}) => {
|
||||||
mockHook(
|
mockHook(
|
||||||
reduxHooks.useCardEnrollmentData,
|
useCourseData,
|
||||||
{
|
{
|
||||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
enrollment: {
|
||||||
isExecEd2UCourse: !!returnVals.isExecEd2UCourse,
|
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||||
|
mode: returnVals.isExecEd2UCourse ? 'exec-ed-2u' : 'standard',
|
||||||
|
},
|
||||||
|
course: { courseName },
|
||||||
},
|
},
|
||||||
{ isCardHook: true },
|
{ isCardHook: true },
|
||||||
);
|
);
|
||||||
mockHook(reduxHooks.useCardCourseData, { courseName }, { isCardHook: true });
|
|
||||||
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
|
|
||||||
mockHook(
|
mockHook(
|
||||||
reduxHooks.useCardSocialSettingsData,
|
useCardSocialSettingsData,
|
||||||
{
|
{
|
||||||
facebook: { ...socialShare.facebook, isEnabled: !!returnVals.facebook?.isEnabled },
|
facebook: { ...socialShare.facebook, isEnabled: !!returnVals.facebook?.isEnabled },
|
||||||
twitter: { ...socialShare.twitter, isEnabled: !!returnVals.twitter?.isEnabled },
|
twitter: { ...socialShare.twitter, isEnabled: !!returnVals.twitter?.isEnabled },
|
||||||
},
|
},
|
||||||
{ isCardHook: true },
|
{ isCardHook: true },
|
||||||
);
|
);
|
||||||
|
mockHook(useIsMasquerading, !!returnVals.isMasquerading);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderComponent = () => render(<IntlProvider locale="en"><SocialShareMenu {...props} /></IntlProvider>);
|
const renderComponent = () => render(<IntlProvider locale="en"><SocialShareMenu {...props} /></IntlProvider>);
|
||||||
@@ -87,13 +86,12 @@ describe('SocialShareMenu', () => {
|
|||||||
it('initializes local hooks', () => {
|
it('initializes local hooks', () => {
|
||||||
when(useEmailSettings).expectCalledWith();
|
when(useEmailSettings).expectCalledWith();
|
||||||
});
|
});
|
||||||
it('initializes redux hook data ', () => {
|
it('initializes hook data ', () => {
|
||||||
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
when(useCourseData).expectCalledWith(props.cardId);
|
||||||
when(reduxHooks.useCardCourseData).expectCalledWith(props.cardId);
|
when(useCardSocialSettingsData).expectCalledWith(props.cardId);
|
||||||
when(reduxHooks.useCardSocialSettingsData).expectCalledWith(props.cardId);
|
when(useIsMasquerading).expectCalledWith();
|
||||||
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
when(useCourseTrackingEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter');
|
||||||
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter');
|
when(useCourseTrackingEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook');
|
||||||
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
|
||||||
export const state = StrictDict({
|
export const state = StrictDict({
|
||||||
isUnenrollConfirmVisible: (val) => useState(val), // eslint-disable-line
|
isUnenrollConfirmVisible: (val) => useState(val), // eslint-disable-line
|
||||||
@@ -27,7 +28,7 @@ export const useEmailSettings = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useHandleToggleDropdown = (cardId) => {
|
export const useHandleToggleDropdown = (cardId) => {
|
||||||
const trackCourseEvent = reduxHooks.useTrackCourseEvent(
|
const trackCourseEvent = useCourseTrackingEvent(
|
||||||
track.course.courseOptionsDropdownClicked,
|
track.course.courseOptionsDropdownClicked,
|
||||||
cardId,
|
cardId,
|
||||||
);
|
);
|
||||||
@@ -36,10 +37,30 @@ export const useHandleToggleDropdown = (cardId) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCardSocialSettingsData = (cardId) => {
|
||||||
|
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||||
|
const courseData = useCourseData(cardId);
|
||||||
|
const socialShareSettings = learnerHomeData?.socialShareSettings;
|
||||||
|
const { socialShareUrl } = courseData?.course || {};
|
||||||
|
const defaultSettings = { isEnabled: false, shareUrl: '' };
|
||||||
|
|
||||||
|
if (!socialShareSettings) {
|
||||||
|
return { facebook: defaultSettings, twitter: defaultSettings };
|
||||||
|
}
|
||||||
|
const { facebook, twitter } = socialShareSettings;
|
||||||
|
const loadSettings = (target) => ({
|
||||||
|
isEnabled: target.isEnabled,
|
||||||
|
shareUrl: `${socialShareUrl}?${target.utmParams}`,
|
||||||
|
});
|
||||||
|
return { facebook: loadSettings(facebook), twitter: loadSettings(twitter) };
|
||||||
|
};
|
||||||
|
|
||||||
export const useOptionVisibility = (cardId) => {
|
export const useOptionVisibility = (cardId) => {
|
||||||
const { isEnrolled, isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
|
const isEmailEnabled = courseData?.enrollment?.isEmailEnabled ?? false;
|
||||||
const { isEarned } = reduxHooks.useCardCertificateData(cardId);
|
const isEnrolled = courseData?.enrollment?.isEnrolled ?? false;
|
||||||
|
const { twitter, facebook } = useCardSocialSettingsData(cardId);
|
||||||
|
const isEarned = courseData?.certificate?.isEarned ?? false;
|
||||||
|
|
||||||
const shouldShowUnenrollItem = isEnrolled && !isEarned;
|
const shouldShowUnenrollItem = isEnrolled && !isEarned;
|
||||||
const shouldShowDropdown = (
|
const shouldShowDropdown = (
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { MockUseState } from 'testUtils';
|
import { MockUseState } from 'testUtils';
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
|
|
||||||
|
jest.mock('data/hooks', () => ({
|
||||||
|
useInitializeLearnerHome: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useCardCertificateData: jest.fn(),
|
useCourseTrackingEvent: jest.fn(),
|
||||||
useCardEnrollmentData: jest.fn(),
|
|
||||||
useCardSocialSettingsData: jest.fn(),
|
|
||||||
useTrackCourseEvent: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const trackCourseEvent = jest.fn();
|
const trackCourseEvent = jest.fn();
|
||||||
reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
|
useCourseTrackingEvent.mockReturnValue(trackCourseEvent);
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
let out;
|
let out;
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ describe('CourseCardMenu hooks', () => {
|
|||||||
beforeEach(() => { out = hooks.useHandleToggleDropdown(cardId); });
|
beforeEach(() => { out = hooks.useHandleToggleDropdown(cardId); });
|
||||||
describe('behavior', () => {
|
describe('behavior', () => {
|
||||||
it('initializes course event tracker with event name and card ID', () => {
|
it('initializes course event tracker with event name and card ID', () => {
|
||||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||||
track.course.courseOptionsDropdownClicked,
|
track.course.courseOptionsDropdownClicked,
|
||||||
cardId,
|
cardId,
|
||||||
);
|
);
|
||||||
@@ -88,55 +89,61 @@ describe('CourseCardMenu hooks', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('useOptionVisibility', () => {
|
describe('useOptionVisibility', () => {
|
||||||
const mockReduxHooks = (returnVals = {}) => {
|
const mockHooks = (returnVals = {}) => {
|
||||||
reduxHooks.useCardSocialSettingsData.mockReturnValueOnce({
|
useInitializeLearnerHome.mockReturnValue({
|
||||||
facebook: { isEnabled: !!returnVals.facebook?.isEnabled },
|
data: {
|
||||||
twitter: { isEnabled: !!returnVals.twitter?.isEnabled },
|
socialShareSettings: {
|
||||||
|
facebook: { isEnabled: !!returnVals.facebook?.isEnabled },
|
||||||
|
twitter: { isEnabled: !!returnVals.twitter?.isEnabled },
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
useCourseData.mockReturnValue({
|
||||||
isEnrolled: !!returnVals.isEnrolled,
|
enrollment: {
|
||||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
isEnrolled: !!returnVals.isEnrolled,
|
||||||
});
|
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||||
reduxHooks.useCardCertificateData.mockReturnValueOnce({
|
},
|
||||||
isEarned: !!returnVals.isEarned,
|
certificate: {
|
||||||
|
isEarned: !!returnVals.isEarned,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
describe('shouldShowUnenrollItem', () => {
|
describe('shouldShowUnenrollItem', () => {
|
||||||
it('returns true if enrolled and not earned', () => {
|
it('returns true if enrolled and not earned', () => {
|
||||||
mockReduxHooks({ isEnrolled: true });
|
mockHooks({ isEnrolled: true });
|
||||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(true);
|
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(true);
|
||||||
});
|
});
|
||||||
it('returns false if not enrolled', () => {
|
it('returns false if not enrolled', () => {
|
||||||
mockReduxHooks();
|
mockHooks();
|
||||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(false);
|
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(false);
|
||||||
});
|
});
|
||||||
it('returns false if enrolled but also earned', () => {
|
it('returns false if enrolled but also earned', () => {
|
||||||
mockReduxHooks({ isEarned: true });
|
mockHooks({ isEarned: true });
|
||||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(false);
|
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('shouldShowDropdown', () => {
|
describe('shouldShowDropdown', () => {
|
||||||
it('returns false if not enrolled and both email and socials are disabled', () => {
|
it('returns false if not enrolled and both email and socials are disabled', () => {
|
||||||
mockReduxHooks();
|
mockHooks();
|
||||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(false);
|
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(false);
|
||||||
});
|
});
|
||||||
it('returns false if enrolled but already earned, and both email and socials are disabled', () => {
|
it('returns false if enrolled but already earned, and both email and socials are disabled', () => {
|
||||||
mockReduxHooks({ isEnrolled: true, isEarned: true });
|
mockHooks({ isEnrolled: true, isEarned: true });
|
||||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(false);
|
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(false);
|
||||||
});
|
});
|
||||||
it('returns true if either social is enabled', () => {
|
it('returns true if either social is enabled', () => {
|
||||||
mockReduxHooks({ facebook: { isEnabled: true } });
|
mockHooks({ facebook: { isEnabled: true } });
|
||||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||||
mockReduxHooks({ twitter: { isEnabled: true } });
|
mockHooks({ twitter: { isEnabled: true } });
|
||||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||||
});
|
});
|
||||||
it('returns true if email is enabled', () => {
|
it('returns true if email is enabled', () => {
|
||||||
mockReduxHooks({ isEmailEnabled: true });
|
mockHooks({ isEmailEnabled: true });
|
||||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||||
});
|
});
|
||||||
it('returns true if enrolled and not earned', () => {
|
it('returns true if enrolled and not earned', () => {
|
||||||
mockReduxHooks({ isEnrolled: true });
|
mockHooks({ isEnrolled: true });
|
||||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { MoreVert } from '@openedx/paragon/icons';
|
|||||||
|
|
||||||
import EmailSettingsModal from 'containers/EmailSettingsModal';
|
import EmailSettingsModal from 'containers/EmailSettingsModal';
|
||||||
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||||
import SocialShareMenu from './SocialShareMenu';
|
import SocialShareMenu from './SocialShareMenu';
|
||||||
import {
|
import {
|
||||||
useEmailSettings,
|
useEmailSettings,
|
||||||
@@ -23,13 +23,15 @@ export const testIds = {
|
|||||||
|
|
||||||
export const CourseCardMenu = ({ cardId }) => {
|
export const CourseCardMenu = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const courseData = useCourseData(cardId);
|
||||||
|
|
||||||
|
const isEmailEnabled = courseData?.enrollment?.isEmailEnabled ?? false;
|
||||||
|
|
||||||
const emailSettings = useEmailSettings();
|
const emailSettings = useEmailSettings();
|
||||||
const unenrollModal = useUnenrollData();
|
const unenrollModal = useUnenrollData();
|
||||||
const handleToggleDropdown = useHandleToggleDropdown(cardId);
|
const handleToggleDropdown = useHandleToggleDropdown(cardId);
|
||||||
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
|
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
|
||||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
const isMasquerading = useIsMasquerading();
|
||||||
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
|
||||||
|
|
||||||
if (!shouldShowDropdown) {
|
if (!shouldShowDropdown) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -4,16 +4,14 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
import CourseCardMenu from '.';
|
import CourseCardMenu from '.';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useMasqueradeData: jest.fn(),
|
useIsMasquerading: jest.fn(),
|
||||||
useCardEnrollmentData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
jest.mock('./SocialShareMenu', () => jest.fn(() => <div>SocialShareMenu</div>));
|
jest.mock('./SocialShareMenu', () => jest.fn(() => <div>SocialShareMenu</div>));
|
||||||
jest.mock('containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
|
jest.mock('containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
|
||||||
@@ -69,10 +67,14 @@ const mockHooks = (returnVals = {}) => {
|
|||||||
},
|
},
|
||||||
{ isCardHook: true },
|
{ isCardHook: true },
|
||||||
);
|
);
|
||||||
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
|
mockHook(useIsMasquerading, !!returnVals.isMasquerading);
|
||||||
mockHook(
|
mockHook(
|
||||||
reduxHooks.useCardEnrollmentData,
|
useCourseData,
|
||||||
{ isEmailEnabled: !!returnVals.isEmailEnabled },
|
{
|
||||||
|
enrollment: {
|
||||||
|
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
{ isCardHook: true },
|
{ isCardHook: true },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -87,13 +89,10 @@ describe('CourseCardMenu', () => {
|
|||||||
});
|
});
|
||||||
it('initializes local hooks', () => {
|
it('initializes local hooks', () => {
|
||||||
when(hooks.useEmailSettings).expectCalledWith();
|
when(hooks.useEmailSettings).expectCalledWith();
|
||||||
when(hooks.useUnenrollData).expectCalledWith();
|
|
||||||
when(hooks.useHandleToggleDropdown).expectCalledWith(props.cardId);
|
|
||||||
when(hooks.useOptionVisibility).expectCalledWith(props.cardId);
|
|
||||||
});
|
});
|
||||||
it('initializes redux hook data ', () => {
|
it('initializes hook data ', () => {
|
||||||
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
when(useIsMasquerading).expectCalledWith();
|
||||||
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
when(useCourseData).expectCalledWith(props.cardId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||||
import useActionDisabledState from './hooks';
|
import useActionDisabledState from './hooks';
|
||||||
|
|
||||||
const { courseTitleClicked } = track.course;
|
const { courseTitleClicked } = track.course;
|
||||||
|
|
||||||
export const CourseCardTitle = ({ cardId }) => {
|
export const CourseCardTitle = ({ cardId }) => {
|
||||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
const courseName = courseData?.course?.courseName;
|
||||||
const handleTitleClicked = reduxHooks.useTrackCourseEvent(
|
const homeUrl = courseData?.courseRun?.homeUrl;
|
||||||
|
const handleTitleClicked = useCourseTrackingEvent(
|
||||||
courseTitleClicked,
|
courseTitleClicked,
|
||||||
cardId,
|
cardId,
|
||||||
homeUrl,
|
homeUrl,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import useActionDisabledState from './hooks';
|
import useActionDisabledState from './hooks';
|
||||||
import CourseCardTitle from './CourseCardTitle';
|
import CourseCardTitle from './CourseCardTitle';
|
||||||
@@ -12,11 +12,8 @@ jest.mock('tracking', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useCardCourseData: jest.fn(),
|
useCourseTrackingEvent: jest.fn(),
|
||||||
useCardCourseRunData: jest.fn(),
|
|
||||||
useTrackCourseEvent: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
|
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
|
||||||
@@ -32,9 +29,11 @@ describe('CourseCardTitle', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
reduxHooks.useCardCourseData.mockReturnValue({ courseName });
|
useCourseData.mockReturnValue({
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
course: { courseName },
|
||||||
reduxHooks.useTrackCourseEvent.mockReturnValue(handleTitleClick);
|
courseRun: { homeUrl },
|
||||||
|
});
|
||||||
|
useCourseTrackingEvent.mockReturnValue(handleTitleClick);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders course name as link when not disabled', async () => {
|
it('renders course name as link when not disabled', async () => {
|
||||||
@@ -62,9 +61,8 @@ describe('CourseCardTitle', () => {
|
|||||||
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
||||||
render(<CourseCardTitle {...props} />);
|
render(<CourseCardTitle {...props} />);
|
||||||
|
|
||||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
|
expect(useCourseData).toHaveBeenCalledWith(props.cardId);
|
||||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
|
||||||
track.course.courseTitleClicked,
|
track.course.courseTitleClicked,
|
||||||
props.cardId,
|
props.cardId,
|
||||||
homeUrl,
|
homeUrl,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import * as module from './hooks';
|
import * as module from './hooks';
|
||||||
@@ -14,7 +14,8 @@ export const state = StrictDict({
|
|||||||
export const useRelatedProgramsBadgeData = ({ cardId }) => {
|
export const useRelatedProgramsBadgeData = ({ cardId }) => {
|
||||||
const [isOpen, setIsOpen] = module.state.isOpen(false);
|
const [isOpen, setIsOpen] = module.state.isOpen(false);
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const numPrograms = reduxHooks.useCardRelatedProgramsData(cardId).length;
|
const courseData = useCourseData(cardId);
|
||||||
|
const numPrograms = courseData?.programs?.relatedPrograms?.length || 0;
|
||||||
let programsMessage = '';
|
let programsMessage = '';
|
||||||
if (numPrograms) {
|
if (numPrograms) {
|
||||||
programsMessage = formatMessage(
|
programsMessage = formatMessage(
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { MockUseState } from 'testUtils';
|
import { MockUseState } from 'testUtils';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useCardRelatedProgramsData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||||
@@ -39,8 +37,10 @@ describe('RelatedProgramsBadge hooks', () => {
|
|||||||
describe('useRelatedProgramsBadgeData', () => {
|
describe('useRelatedProgramsBadgeData', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
state.mock();
|
state.mock();
|
||||||
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({
|
useCourseData.mockReturnValue({
|
||||||
length: numPrograms,
|
programs: {
|
||||||
|
relatedPrograms: new Array(numPrograms).fill({}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
out = hooks.useRelatedProgramsBadgeData({ cardId });
|
out = hooks.useRelatedProgramsBadgeData({ cardId });
|
||||||
});
|
});
|
||||||
@@ -64,12 +64,12 @@ describe('RelatedProgramsBadge hooks', () => {
|
|||||||
expect(out.numPrograms).toEqual(numPrograms);
|
expect(out.numPrograms).toEqual(numPrograms);
|
||||||
});
|
});
|
||||||
test('returns empty programsMessage if no programs', () => {
|
test('returns empty programsMessage if no programs', () => {
|
||||||
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 0 });
|
useCourseData.mockReturnValueOnce({ programs: { relatedPrograms: [] } });
|
||||||
out = hooks.useRelatedProgramsBadgeData({ cardId });
|
out = hooks.useRelatedProgramsBadgeData({ cardId });
|
||||||
expect(out.programsMessage).toEqual('');
|
expect(out.programsMessage).toEqual('');
|
||||||
});
|
});
|
||||||
test('returns badgeLabelSingular programsMessage if 1 programs', () => {
|
test('returns badgeLabelSingular programsMessage if 1 programs', () => {
|
||||||
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 1 });
|
useCourseData.mockReturnValueOnce({ programs: { relatedPrograms: [{}] } });
|
||||||
out = hooks.useRelatedProgramsBadgeData({ cardId });
|
out = hooks.useRelatedProgramsBadgeData({ cardId });
|
||||||
expect(out.programsMessage).toEqual(formatMessage(
|
expect(out.programsMessage).toEqual(formatMessage(
|
||||||
messages.badgeLabelSingular,
|
messages.badgeLabelSingular,
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useEntitlementInfo, useIsMasquerading } from 'hooks';
|
||||||
|
|
||||||
export const useActionDisabledState = (cardId) => {
|
export const useActionDisabledState = (cardId) => {
|
||||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
const courseData = useCourseData(cardId);
|
||||||
|
const isMasquerading = useIsMasquerading();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hasAccess, isAudit, isAuditAccessExpired,
|
isAudit, isAuditAccessExpired,
|
||||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
} = courseData.enrollment || {};
|
||||||
|
const { isStaff, hasUnmetPrereqs, isTooEarly } = courseData.enrollment?.coursewareAccess || {};
|
||||||
|
const hasAccess = isStaff || !(hasUnmetPrereqs || isTooEarly);
|
||||||
const {
|
const {
|
||||||
isEntitlement, isFulfilled, canChange, hasSessions,
|
isEntitlement, isFulfilled, canChange, hasSessions,
|
||||||
} = reduxHooks.useCardEntitlementData(cardId);
|
} = useEntitlementInfo(courseData);
|
||||||
|
|
||||||
const { resumeUrl, homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
|
||||||
|
|
||||||
|
const { resumeUrl, homeUrl } = courseData.courseRun || {};
|
||||||
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||||
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||||
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
|
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
|
|
||||||
|
jest.mock('react', () => ({
|
||||||
|
...jest.requireActual('react'),
|
||||||
|
useMemo: jest.fn((fn) => fn()),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
...jest.requireActual('hooks'),
|
||||||
useMasqueradeData: jest.fn(),
|
useCourseData: jest.fn(),
|
||||||
useCardEnrollmentData: jest.fn(),
|
useIsMasquerading: jest.fn(),
|
||||||
useCardEntitlementData: jest.fn(),
|
|
||||||
useCardCourseRunData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cardId = 'my-test-course-number';
|
const cardId = 'my-test-course-number';
|
||||||
@@ -38,25 +39,38 @@ describe('useActionDisabledState', () => {
|
|||||||
isAuditAccessExpired,
|
isAuditAccessExpired,
|
||||||
resumeUrl,
|
resumeUrl,
|
||||||
homeUrl,
|
homeUrl,
|
||||||
|
availableSessions,
|
||||||
} = { ...defaultData, ...args };
|
} = { ...defaultData, ...args };
|
||||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
useIsMasquerading.mockReturnValue(isMasquerading);
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
useCourseData.mockReturnValue({
|
||||||
hasAccess,
|
enrollment: {
|
||||||
isAudit,
|
hasAccess,
|
||||||
isAuditAccessExpired,
|
isAudit,
|
||||||
});
|
isAuditAccessExpired,
|
||||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({
|
coursewareAccess: {
|
||||||
isEntitlement,
|
isStaff: false,
|
||||||
isFulfilled,
|
hasUnmetPrereqs: !hasAccess,
|
||||||
canChange,
|
isTooEarly: !hasAccess,
|
||||||
hasSessions,
|
},
|
||||||
});
|
},
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
entitlement: isEntitlement ? {
|
||||||
resumeUrl,
|
isEntitlement: true,
|
||||||
homeUrl,
|
isFulfilled,
|
||||||
|
canChange,
|
||||||
|
hasSessions,
|
||||||
|
availableSessions,
|
||||||
|
} : {},
|
||||||
|
courseRun: {
|
||||||
|
resumeUrl,
|
||||||
|
homeUrl,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
const runHook = () => hooks.useActionDisabledState(cardId);
|
const runHook = () => hooks.useActionDisabledState(cardId);
|
||||||
describe('disableBeginCourse', () => {
|
describe('disableBeginCourse', () => {
|
||||||
const testDisabled = (data, expected) => {
|
const testDisabled = (data, expected) => {
|
||||||
@@ -142,6 +156,7 @@ describe('useActionDisabledState', () => {
|
|||||||
hasAccess: true,
|
hasAccess: true,
|
||||||
canChange: true,
|
canChange: true,
|
||||||
hasSessions: true,
|
hasSessions: true,
|
||||||
|
availableSessions: ['session1'],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +1,6 @@
|
|||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
|
|
||||||
export const useIsCollapsed = () => {
|
export const useIsCollapsed = () => {
|
||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
return width < breakpoints.small.maxWidth;
|
return width < breakpoints.small.maxWidth;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCardData = ({ cardId }) => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const { title, bannerImgSrc } = reduxHooks.useCardCourseData(cardId);
|
|
||||||
const { isEnrolled } = reduxHooks.useCardEnrollmentData(cardId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isEnrolled,
|
|
||||||
title,
|
|
||||||
bannerImgSrc,
|
|
||||||
formatMessage,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useCardData;
|
|
||||||
|
|||||||
@@ -1,58 +1,32 @@
|
|||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useWindowSize } from '@openedx/paragon';
|
||||||
|
import { useIsCollapsed } from './hooks';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
jest.mock('@openedx/paragon', () => ({
|
||||||
|
useWindowSize: jest.fn(),
|
||||||
import * as hooks from './hooks';
|
breakpoints: {
|
||||||
|
small: {
|
||||||
jest.mock('hooks', () => ({
|
maxWidth: 576,
|
||||||
reduxHooks: {
|
},
|
||||||
useCardCourseData: jest.fn(),
|
|
||||||
useCardEnrollmentData: jest.fn(),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
describe('useIsCollapsed', () => {
|
||||||
const { formatMessage } = jest.requireActual('testUtils');
|
afterEach(() => {
|
||||||
return {
|
|
||||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
|
||||||
useIntl: () => ({
|
|
||||||
formatMessage,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const cardId = 'my-test-course-number';
|
|
||||||
|
|
||||||
describe('CourseCard hooks', () => {
|
|
||||||
let out;
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useCardData', () => {
|
it('should return true when window width is smaller than small breakpoint', () => {
|
||||||
const courseData = {
|
useWindowSize.mockReturnValue({ width: 500 });
|
||||||
title: 'fake-title',
|
const { result } = renderHook(() => useIsCollapsed());
|
||||||
bannerImgSrc: 'my-banner-url',
|
expect(result.current).toBe(true);
|
||||||
};
|
expect(useWindowSize).toHaveBeenCalled();
|
||||||
const runHook = ({ course = {} }) => {
|
});
|
||||||
reduxHooks.useCardCourseData.mockReturnValueOnce({
|
|
||||||
...courseData,
|
it('should return false when window width is larger than small breakpoint', () => {
|
||||||
...course,
|
useWindowSize.mockReturnValue({ width: 800 });
|
||||||
});
|
const { result } = renderHook(() => useIsCollapsed());
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: 'test-is-enrolled' });
|
expect(result.current).toBe(false);
|
||||||
out = hooks.useCardData({ cardId });
|
expect(useWindowSize).toHaveBeenCalled();
|
||||||
};
|
|
||||||
beforeEach(() => {
|
|
||||||
runHook({});
|
|
||||||
});
|
|
||||||
it('forwards formatMessage from useIntl', () => {
|
|
||||||
expect(out.formatMessage).toEqual(formatMessage);
|
|
||||||
});
|
|
||||||
it('passes course title and banner URL form course data', () => {
|
|
||||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
|
|
||||||
expect(out.title).toEqual(courseData.title);
|
|
||||||
expect(out.bannerImgSrc).toEqual(courseData.bannerImgSrc);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { Button, Chip } from '@openedx/paragon';
|
import { Button, Chip } from '@openedx/paragon';
|
||||||
import { CloseSmall } from '@openedx/paragon/icons';
|
import { CloseSmall } from '@openedx/paragon/icons';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useFilters } from 'data/context';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
export const ActiveCourseFilters = ({
|
export const ActiveCourseFilters = () => {
|
||||||
filters,
|
|
||||||
handleRemoveFilter,
|
|
||||||
}) => {
|
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const clearFilters = reduxHooks.useClearFilters();
|
const { filters, clearFilters, removeFilter } = useFilters();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="course-list-active-filters">
|
<div id="course-list-active-filters">
|
||||||
{filters.map(filter => (
|
{filters.map(filter => (
|
||||||
<Chip
|
<Chip
|
||||||
key={filter}
|
key={filter}
|
||||||
iconAfter={CloseSmall}
|
iconAfter={CloseSmall}
|
||||||
onClick={handleRemoveFilter(filter)}
|
onClick={() => removeFilter(filter)}
|
||||||
>
|
>
|
||||||
{formatMessage(messages[filter])}
|
{formatMessage(messages[filter])}
|
||||||
</Chip>
|
</Chip>
|
||||||
@@ -32,9 +29,5 @@ export const ActiveCourseFilters = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
ActiveCourseFilters.propTypes = {
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
handleRemoveFilter: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ActiveCourseFilters;
|
export default ActiveCourseFilters;
|
||||||
|
|||||||
@@ -1,28 +1,54 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { formatMessage } from 'testUtils';
|
import { formatMessage } from 'testUtils';
|
||||||
|
import { useFilters } from 'data/context';
|
||||||
|
|
||||||
import { FilterKeys } from 'data/constants/app';
|
import { FilterKeys } from 'data/constants/app';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import ActiveCourseFilters from './ActiveCourseFilters';
|
import ActiveCourseFilters from './ActiveCourseFilters';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const filters = Object.values(FilterKeys);
|
const filters = Object.values(FilterKeys);
|
||||||
|
|
||||||
|
jest.mock('data/context', () => ({
|
||||||
|
useFilters: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const removeFiltersMock = jest.fn().mockName('removeFilter');
|
||||||
|
const clearFiltersMock = jest.fn().mockName('clearFilters');
|
||||||
|
useFilters.mockReturnValue({
|
||||||
|
filters,
|
||||||
|
removeFilter: removeFiltersMock,
|
||||||
|
clearFilters: clearFiltersMock,
|
||||||
|
});
|
||||||
|
|
||||||
describe('ActiveCourseFilters', () => {
|
describe('ActiveCourseFilters', () => {
|
||||||
const props = {
|
|
||||||
filters,
|
|
||||||
handleRemoveFilter: jest.fn().mockName('handleRemoveFilter'),
|
|
||||||
};
|
|
||||||
it('renders chips correctly', () => {
|
it('renders chips correctly', () => {
|
||||||
render(<IntlProvider locale="en"><ActiveCourseFilters {...props} /></IntlProvider>);
|
render(<IntlProvider locale="en"><ActiveCourseFilters /></IntlProvider>);
|
||||||
filters.map((key) => {
|
filters.map((key) => {
|
||||||
const chip = screen.getByText(formatMessage(messages[key]));
|
const chip = screen.getByText(formatMessage(messages[key]));
|
||||||
return expect(chip).toBeInTheDocument();
|
return expect(chip).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('renders button correctly', () => {
|
it('renders button correctly', () => {
|
||||||
render(<IntlProvider locale="en"><ActiveCourseFilters {...props} /></IntlProvider>);
|
render(<IntlProvider locale="en"><ActiveCourseFilters /></IntlProvider>);
|
||||||
const button = screen.getByRole('button', { name: formatMessage(messages.clearAll) });
|
const button = screen.getByRole('button', { name: formatMessage(messages.clearAll) });
|
||||||
expect(button).toBeInTheDocument();
|
expect(button).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
it('should call onClick when button is clicked remove filter', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<IntlProvider locale="en"><ActiveCourseFilters /></IntlProvider>);
|
||||||
|
const removeButton = screen.getByRole('button', { name: formatMessage(messages[filters[0]]) });
|
||||||
|
await user.click(removeButton);
|
||||||
|
expect(removeFiltersMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(removeFiltersMock).toHaveBeenCalledWith(filters[0]);
|
||||||
|
});
|
||||||
|
it('should call onClick when button is clicked clear all filters', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<IntlProvider locale="en"><ActiveCourseFilters /></IntlProvider>);
|
||||||
|
screen.debug();
|
||||||
|
const clearAllButton = screen.getByRole('button', { name: formatMessage(messages.clearAll) });
|
||||||
|
await user.click(clearAllButton);
|
||||||
|
expect(clearFiltersMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import track from 'tracking';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
@@ -14,44 +13,51 @@ import {
|
|||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
import { Close, Tune } from '@openedx/paragon/icons';
|
import { Close, Tune } from '@openedx/paragon/icons';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
import { useFilters } from 'data/context';
|
||||||
import FilterForm from './components/FilterForm';
|
import FilterForm from './components/FilterForm';
|
||||||
import SortForm from './components/SortForm';
|
import SortForm from './components/SortForm';
|
||||||
import useCourseFilterControlsData from './hooks';
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
export const CourseFilterControls = ({
|
export const CourseFilterControls = () => {
|
||||||
sortBy,
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
setSortBy,
|
const [targetRef, setTargetRef] = React.useState(null);
|
||||||
filters,
|
|
||||||
}) => {
|
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const hasCourses = reduxHooks.useHasCourses();
|
const { data } = useInitializeLearnerHome();
|
||||||
|
const hasCourses = React.useMemo(() => data?.courses?.length > 0, [data]);
|
||||||
const {
|
const {
|
||||||
isOpen,
|
filters, sortBy, setSortBy, addFilter, removeFilter,
|
||||||
open,
|
} = useFilters();
|
||||||
close,
|
|
||||||
target,
|
const openFiltersOptions = () => {
|
||||||
setTarget,
|
track.filter.filterClicked();
|
||||||
handleFilterChange,
|
setIsOpen(true);
|
||||||
handleSortChange,
|
};
|
||||||
} = useCourseFilterControlsData({
|
const closeFiltersOptions = () => {
|
||||||
filters,
|
track.filter.filterOptionSelected(filters);
|
||||||
setSortBy,
|
setIsOpen(false);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const handleSortChange = (event) => {
|
||||||
|
setSortBy(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterChange = ({ target: { checked, value } }) => {
|
||||||
|
const update = checked ? addFilter : removeFilter;
|
||||||
|
update(value);
|
||||||
|
};
|
||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
const isMobile = width < breakpoints.small.minWidth;
|
const isMobile = width < breakpoints.small.minWidth;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="course-filter-controls">
|
<div id="course-filter-controls">
|
||||||
<Button
|
<Button
|
||||||
ref={setTarget}
|
ref={setTargetRef}
|
||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
iconBefore={Tune}
|
iconBefore={Tune}
|
||||||
onClick={open}
|
onClick={openFiltersOptions}
|
||||||
disabled={!hasCourses}
|
disabled={!hasCourses}
|
||||||
>
|
>
|
||||||
{formatMessage(messages.refine)}
|
{formatMessage(messages.refine)}
|
||||||
@@ -63,7 +69,7 @@ export const CourseFilterControls = ({
|
|||||||
className="w-75"
|
className="w-75"
|
||||||
position="left"
|
position="left"
|
||||||
show={isOpen}
|
show={isOpen}
|
||||||
onClose={close}
|
onClose={closeFiltersOptions}
|
||||||
>
|
>
|
||||||
<div className="p-1 mr-3">
|
<div className="p-1 mr-3">
|
||||||
<b>{formatMessage(messages.refine)}</b>
|
<b>{formatMessage(messages.refine)}</b>
|
||||||
@@ -76,16 +82,16 @@ export const CourseFilterControls = ({
|
|||||||
<SortForm {...{ sortBy, handleSortChange }} />
|
<SortForm {...{ sortBy, handleSortChange }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="pgn__modal-close-container">
|
<div className="pgn__modal-close-container">
|
||||||
<ModalCloseButton variant="tertiary" onClick={close}>
|
<ModalCloseButton variant="tertiary" onClick={closeFiltersOptions}>
|
||||||
<Icon src={Close} />
|
<Icon src={Close} />
|
||||||
</ModalCloseButton>
|
</ModalCloseButton>
|
||||||
</div>
|
</div>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
) : (
|
) : (
|
||||||
<ModalPopup
|
<ModalPopup
|
||||||
positionRef={target}
|
positionRef={targetRef}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={close}
|
onClose={closeFiltersOptions}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -106,10 +112,5 @@ export const CourseFilterControls = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
CourseFilterControls.propTypes = {
|
|
||||||
sortBy: PropTypes.string.isRequired,
|
|
||||||
setSortBy: PropTypes.func.isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CourseFilterControls;
|
export default CourseFilterControls;
|
||||||
|
|||||||
@@ -1,75 +1,150 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import { formatMessage } from 'testUtils';
|
import { formatMessage } from 'testUtils';
|
||||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { FilterKeys, SortKeys } from 'data/constants/app';
|
import { FilterKeys, SortKeys } from 'data/constants/app';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
import { useFilters } from 'data/context';
|
||||||
|
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import CourseFilterControls from './CourseFilterControls';
|
import CourseFilterControls from './CourseFilterControls';
|
||||||
import useCourseFilterControlsData from './hooks';
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('data/hooks', () => ({
|
||||||
reduxHooks: { useHasCourses: jest.fn() },
|
useInitializeLearnerHome: jest.fn().mockReturnValue({ data: { courses: [1, 2, 3] } }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./hooks', () => jest.fn());
|
|
||||||
|
|
||||||
jest.mock('@openedx/paragon', () => ({
|
jest.mock('@openedx/paragon', () => ({
|
||||||
...jest.requireActual('@openedx/paragon'),
|
...jest.requireActual('@openedx/paragon'),
|
||||||
useWindowSize: jest.fn(),
|
useWindowSize: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('tracking', () => ({
|
||||||
|
filter: {
|
||||||
|
filterClicked: jest.fn().mockName('segment.filterClicked'),
|
||||||
|
filterOptionSelected: jest.fn().mockName('segment.filterOptionSelected'),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const filters = Object.values(FilterKeys);
|
const filters = Object.values(FilterKeys);
|
||||||
|
|
||||||
const mockControlsData = {
|
jest.mock('data/context', () => ({
|
||||||
isOpen: false,
|
useFilters: jest.fn(),
|
||||||
open: jest.fn().mockName('open'),
|
}));
|
||||||
close: jest.fn().mockName('close'),
|
|
||||||
target: 'target-test',
|
const setSortByMock = jest.fn().mockName('setSortBy');
|
||||||
setTarget: jest.fn(),
|
useFilters.mockReturnValue({
|
||||||
handleFilterChange: jest.fn().mockName('handleFilterChange'),
|
filters,
|
||||||
handleSortChange: jest.fn().mockName('handleSortChange'),
|
removeFilter: jest.fn().mockName('removeFilter'),
|
||||||
};
|
clearFilters: jest.fn().mockName('clearFilters'),
|
||||||
|
setSortBy: setSortByMock,
|
||||||
|
addFilter: jest.fn().mockName('addFilter'),
|
||||||
|
});
|
||||||
|
|
||||||
describe('CourseFilterControls', () => {
|
describe('CourseFilterControls', () => {
|
||||||
const props = {
|
|
||||||
sortBy: SortKeys.enrolled,
|
|
||||||
setSortBy: jest.fn().mockName('setSortBy'),
|
|
||||||
filters,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('mobile and open', () => {
|
describe('mobile and open', () => {
|
||||||
it('should render sheet', () => {
|
it('should render sheet', async () => {
|
||||||
reduxHooks.useHasCourses.mockReturnValue(true);
|
const user = userEvent.setup();
|
||||||
useCourseFilterControlsData.mockReturnValue({ ...mockControlsData, isOpen: true });
|
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth - 1 });
|
||||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth - 1 });
|
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||||
render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
|
const filtersButton = screen.getByRole('button', { name: 'Refine' });
|
||||||
const sheet = screen.getByRole('presentation', { hidden: true });
|
await user.click(filtersButton);
|
||||||
expect(sheet).toBeInTheDocument();
|
await waitFor(() => {
|
||||||
expect(sheet.parentElement).toHaveClass('sheet-container');
|
const sheet = screen.getByRole('presentation', { hidden: true });
|
||||||
|
expect(sheet).toBeInTheDocument();
|
||||||
|
expect(sheet.parentElement).toHaveClass('sheet-container');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('is not mobile', () => {
|
describe('is not mobile', () => {
|
||||||
it('should have button disabled', () => {
|
it('should have button disabled', async () => {
|
||||||
reduxHooks.useHasCourses.mockReturnValue(true);
|
const user = userEvent.setup();
|
||||||
useCourseFilterControlsData.mockReturnValue({ ...mockControlsData, isOpen: true });
|
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
|
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||||
render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
|
const filtersButton = screen.getByRole('button', { name: 'Refine' });
|
||||||
const filterForm = screen.getByText(messages.courseStatus.defaultMessage);
|
await user.click(filtersButton);
|
||||||
const modal = filterForm.closest('div.pgn__modal-popup__tooltip');
|
await waitFor(() => {
|
||||||
expect(modal).toBeInTheDocument();
|
const filterForm = screen.getByText(messages.courseStatus.defaultMessage);
|
||||||
|
const modal = filterForm.closest('div.pgn__modal-popup__tooltip');
|
||||||
|
expect(modal).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('no courses', () => {
|
describe('no courses', () => {
|
||||||
it('should have button disabled', () => {
|
it('should have button disabled', () => {
|
||||||
reduxHooks.useHasCourses.mockReturnValue(false);
|
useInitializeLearnerHome.mockReturnValue({ data: { courses: [] } });
|
||||||
useCourseFilterControlsData.mockReturnValue(mockControlsData);
|
|
||||||
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||||
render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
|
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||||
const button = screen.getByRole('button', { name: formatMessage(messages.refine) });
|
const button = screen.getByRole('button', { name: formatMessage(messages.refine) });
|
||||||
expect(button).toBeInTheDocument();
|
expect(button).toBeInTheDocument();
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('with courses', () => {
|
||||||
|
it('should have button enabled', () => {
|
||||||
|
useInitializeLearnerHome.mockReturnValue({ data: { courses: [1, 2, 3] } });
|
||||||
|
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||||
|
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||||
|
const button = screen.getByRole('button', { name: formatMessage(messages.refine) });
|
||||||
|
expect(button).toBeInTheDocument();
|
||||||
|
expect(button).toBeEnabled();
|
||||||
|
});
|
||||||
|
it('should call setSortBy on sort change', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
useInitializeLearnerHome.mockReturnValue({ data: { courses: [1, 2, 3] } });
|
||||||
|
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||||
|
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||||
|
const filtersButton = screen.getByRole('button', { name: 'Refine' });
|
||||||
|
await user.click(filtersButton);
|
||||||
|
await waitFor(async () => {
|
||||||
|
const sortRadio = screen.getByRole('radio', { name: formatMessage(messages.sortTitle) });
|
||||||
|
await user.click(sortRadio);
|
||||||
|
expect(setSortByMock).toHaveBeenCalledWith(SortKeys.title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should call addFilter on filter check', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const addFilterMock = jest.fn().mockName('addFilter');
|
||||||
|
const removeFilterMock = jest.fn().mockName('removeFilter');
|
||||||
|
useFilters.mockReturnValue({
|
||||||
|
filters: [],
|
||||||
|
removeFilter: removeFilterMock,
|
||||||
|
clearFilters: jest.fn().mockName('clearFilters'),
|
||||||
|
setSortBy: jest.fn().mockName('setSortBy'),
|
||||||
|
addFilter: addFilterMock,
|
||||||
|
});
|
||||||
|
useInitializeLearnerHome.mockReturnValue({ data: { courses: [1, 2, 3] } });
|
||||||
|
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||||
|
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||||
|
const filtersButton = screen.getByRole('button', { name: 'Refine' });
|
||||||
|
await user.click(filtersButton);
|
||||||
|
await waitFor(async () => {
|
||||||
|
const filterCheckbox = screen.getByText('In-Progress');
|
||||||
|
await user.click(filterCheckbox);
|
||||||
|
expect(addFilterMock).toHaveBeenCalledWith(FilterKeys.inProgress);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should call removeFilter on filter uncheck', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const addFilterMock = jest.fn().mockName('addFilter');
|
||||||
|
const removeFilterMock = jest.fn().mockName('removeFilter');
|
||||||
|
useFilters.mockReturnValue({
|
||||||
|
filters: [FilterKeys.inProgress],
|
||||||
|
removeFilter: removeFilterMock,
|
||||||
|
clearFilters: jest.fn().mockName('clearFilters'),
|
||||||
|
setSortBy: jest.fn().mockName('setSortBy'),
|
||||||
|
addFilter: addFilterMock,
|
||||||
|
});
|
||||||
|
useInitializeLearnerHome.mockReturnValue({ data: { courses: [1, 2, 3] } });
|
||||||
|
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||||
|
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||||
|
const filtersButton = screen.getByRole('button', { name: 'Refine' });
|
||||||
|
await user.click(filtersButton);
|
||||||
|
await waitFor(async () => {
|
||||||
|
const filterCheckbox = screen.getByText('In-Progress');
|
||||||
|
await user.click(filterCheckbox);
|
||||||
|
expect(removeFilterMock).toHaveBeenCalledWith(FilterKeys.inProgress);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useToggle } from '@openedx/paragon';
|
|
||||||
|
|
||||||
import { StrictDict } from 'utils';
|
|
||||||
import track from 'tracking';
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
|
|
||||||
import * as module from './hooks';
|
|
||||||
|
|
||||||
export const state = StrictDict({
|
|
||||||
target: (val) => React.useState(val), // eslint-disable-line
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up a toggle for the modal as well as helper functions for handling changes to the form controls.
|
|
||||||
*
|
|
||||||
* @param {array} filters Currently active course filters
|
|
||||||
* @param {function} setSortBy Set function for sorting the course list
|
|
||||||
* @returns {object} data and functions for managing the CourseFilterControls component
|
|
||||||
*/
|
|
||||||
export const useCourseFilterControlsData = ({
|
|
||||||
filters,
|
|
||||||
setSortBy,
|
|
||||||
}) => {
|
|
||||||
const [isOpen, toggleOpen, toggleClose] = useToggle(false);
|
|
||||||
const [target, setTarget] = module.state.target(null);
|
|
||||||
|
|
||||||
const addFilter = reduxHooks.useAddFilter();
|
|
||||||
const removeFilter = reduxHooks.useRemoveFilter();
|
|
||||||
|
|
||||||
const handleFilterChange = ({ target: { checked, value } }) => {
|
|
||||||
const update = checked ? addFilter : removeFilter;
|
|
||||||
update(value);
|
|
||||||
};
|
|
||||||
const handleSortChange = ({ target: { value } }) => {
|
|
||||||
setSortBy(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
track.filter.filterClicked();
|
|
||||||
toggleOpen();
|
|
||||||
};
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
track.filter.filterOptionSelected(filters);
|
|
||||||
toggleClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
isOpen,
|
|
||||||
open,
|
|
||||||
close,
|
|
||||||
target,
|
|
||||||
setTarget,
|
|
||||||
handleFilterChange,
|
|
||||||
handleSortChange,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useCourseFilterControlsData;
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import { useToggle } from '@openedx/paragon';
|
|
||||||
|
|
||||||
import { MockUseState } from 'testUtils';
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
|
|
||||||
import track from 'tracking';
|
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
|
||||||
|
|
||||||
jest.mock('@openedx/paragon', () => ({
|
|
||||||
...jest.requireActual('@openedx/paragon'),
|
|
||||||
useToggle: jest.fn().mockImplementation((val) => [
|
|
||||||
val,
|
|
||||||
jest.fn().mockName('useToggle.setTrue'),
|
|
||||||
jest.fn().mockName('useToggle.setFalse'),
|
|
||||||
]),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('tracking', () => ({
|
|
||||||
filter: {
|
|
||||||
filterClicked: jest.fn(),
|
|
||||||
filterOptionSelected: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useAddFilter: jest.fn(),
|
|
||||||
useRemoveFilter: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const state = new MockUseState(hooks);
|
|
||||||
|
|
||||||
describe('CourseFilterControls hooks', () => {
|
|
||||||
let out;
|
|
||||||
const filters = ['a', 'b', 'c'];
|
|
||||||
const setSortBy = jest.fn();
|
|
||||||
|
|
||||||
const removeFilter = jest.fn();
|
|
||||||
reduxHooks.useRemoveFilter.mockReturnValue(removeFilter);
|
|
||||||
const addFilter = jest.fn();
|
|
||||||
reduxHooks.useAddFilter.mockReturnValue(addFilter);
|
|
||||||
|
|
||||||
const toggleOpen = jest.fn();
|
|
||||||
const toggleClose = jest.fn();
|
|
||||||
|
|
||||||
describe('state values', () => {
|
|
||||||
state.testGetter(state.keys.target);
|
|
||||||
});
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useCourseFilterControlsData', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
useToggle.mockReturnValueOnce([false, toggleOpen, toggleClose]);
|
|
||||||
state.mock();
|
|
||||||
out = hooks.useCourseFilterControlsData({
|
|
||||||
filters,
|
|
||||||
setSortBy,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
afterEach(state.restore);
|
|
||||||
|
|
||||||
test('default state', () => {
|
|
||||||
expect(out.isOpen).toEqual(false);
|
|
||||||
expect(out.target).toEqual(state.stateVals.target);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('open calls toggleOpen and track.filter.filterClicked', () => {
|
|
||||||
out.open();
|
|
||||||
expect(toggleOpen).toHaveBeenCalled();
|
|
||||||
expect(track.filter.filterClicked).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('close calls toggleClose and track.filter.filterOptionSelected', () => {
|
|
||||||
out.close();
|
|
||||||
expect(toggleClose).toHaveBeenCalled();
|
|
||||||
expect(track.filter.filterOptionSelected).toHaveBeenCalledWith(filters);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isOpen is true when target is set', () => {
|
|
||||||
useToggle.mockReturnValueOnce([true, toggleOpen, toggleClose]);
|
|
||||||
expect(out.target).toEqual(null);
|
|
||||||
state.mockVal(state.keys.target, 'foo');
|
|
||||||
out = hooks.useCourseFilterControlsData({
|
|
||||||
filters,
|
|
||||||
setSortBy,
|
|
||||||
});
|
|
||||||
expect(out.isOpen).toEqual(true);
|
|
||||||
expect(out.target).toEqual('foo');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handle filter change', () => {
|
|
||||||
const value = 'a';
|
|
||||||
out.handleFilterChange({
|
|
||||||
target: {
|
|
||||||
checked: true,
|
|
||||||
value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(addFilter).toHaveBeenCalledWith(value);
|
|
||||||
out.handleFilterChange({
|
|
||||||
target: {
|
|
||||||
checked: false,
|
|
||||||
value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(removeFilter).toHaveBeenCalledWith(value);
|
|
||||||
});
|
|
||||||
test('handle sort change', () => {
|
|
||||||
const value = 'a';
|
|
||||||
out.handleSortChange({
|
|
||||||
target: {
|
|
||||||
value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(setSortBy).toHaveBeenCalledWith(value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -11,14 +11,15 @@ import { useIsCollapsed } from './hooks';
|
|||||||
|
|
||||||
export const CourseList = ({ courseListData }) => {
|
export const CourseList = ({ courseListData }) => {
|
||||||
const {
|
const {
|
||||||
filterOptions, setPageNumber, numPages, showFilters, visibleList,
|
setPageNumber, numPages, visibleList, showFilters,
|
||||||
} = courseListData;
|
} = courseListData;
|
||||||
|
|
||||||
const isCollapsed = useIsCollapsed();
|
const isCollapsed = useIsCollapsed();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showFilters && (
|
{showFilters && (
|
||||||
<div id="course-list-active-filters-container">
|
<div id="course-list-active-filters-container">
|
||||||
<ActiveCourseFilters {...filterOptions} />
|
<ActiveCourseFilters />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="d-flex flex-column flex-grow-1">
|
<div className="d-flex flex-column flex-grow-1">
|
||||||
@@ -42,7 +43,6 @@ export const CourseList = ({ courseListData }) => {
|
|||||||
export const courseListDataShape = PropTypes.shape({
|
export const courseListDataShape = PropTypes.shape({
|
||||||
showFilters: PropTypes.bool.isRequired,
|
showFilters: PropTypes.bool.isRequired,
|
||||||
visibleList: PropTypes.arrayOf(PropTypes.shape()).isRequired,
|
visibleList: PropTypes.arrayOf(PropTypes.shape()).isRequired,
|
||||||
filterOptions: PropTypes.shape().isRequired,
|
|
||||||
numPages: PropTypes.number.isRequired,
|
numPages: PropTypes.number.isRequired,
|
||||||
setPageNumber: PropTypes.func.isRequired,
|
setPageNumber: PropTypes.func.isRequired,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import { Search } from '@openedx/paragon/icons';
|
|||||||
import { baseAppUrl } from 'data/services/lms/urls';
|
import { baseAppUrl } from 'data/services/lms/urls';
|
||||||
|
|
||||||
import emptyCourseSVG from 'assets/empty-course.svg';
|
import emptyCourseSVG from 'assets/empty-course.svg';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
export const NoCoursesView = () => {
|
export const NoCoursesView = () => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
const { data: learnerData } = useInitializeLearnerHome();
|
||||||
|
const courseSearchUrl = learnerData?.platformSettings?.courseSearchUrl || '';
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="no-courses-content-view"
|
id="no-courses-content-view"
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import messages from './messages';
|
|||||||
|
|
||||||
const courseSearchUrl = '/course-search-url';
|
const courseSearchUrl = '/course-search-url';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('data/hooks', () => ({
|
||||||
reduxHooks: {
|
useInitializeLearnerHome: jest.fn(() => ({
|
||||||
usePlatformSettingsData: jest.fn(() => ({
|
data: {
|
||||||
courseSearchUrl,
|
platformSettings: {
|
||||||
})),
|
courseSearchUrl,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('NoCoursesView', () => {
|
describe('NoCoursesView', () => {
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { ListPageSize, SortKeys } from 'data/constants/app';
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import { StrictDict } from 'utils';
|
|
||||||
|
|
||||||
import * as module from './hooks';
|
|
||||||
|
|
||||||
export const state = StrictDict({
|
|
||||||
sortBy: (val) => React.useState(val), // eslint-disable-line
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters are fetched from the store and used to generate a list of "visible" courses.
|
|
||||||
* Other values returned and used for the layout of the CoursesPanel component are:
|
|
||||||
* the current page number, the sorting method, and whether or not to enable filters and pagination.
|
|
||||||
*
|
|
||||||
* @returns data for the CoursesPanel component
|
|
||||||
*/
|
|
||||||
export const useCourseListData = () => {
|
|
||||||
const filters = reduxHooks.useFilters();
|
|
||||||
const removeFilter = reduxHooks.useRemoveFilter();
|
|
||||||
const pageNumber = reduxHooks.usePageNumber();
|
|
||||||
const setPageNumber = reduxHooks.useSetPageNumber();
|
|
||||||
|
|
||||||
const [sortBy, setSortBy] = module.state.sortBy(SortKeys.enrolled);
|
|
||||||
|
|
||||||
const querySearch = new URLSearchParams(window.location.search);
|
|
||||||
const disablePagination = querySearch.get('disable_pagination');
|
|
||||||
|
|
||||||
const { numPages, visibleList } = reduxHooks.useCurrentCourseList({
|
|
||||||
sortBy,
|
|
||||||
filters,
|
|
||||||
pageSize: Number(disablePagination) === 1 ? 0 : ListPageSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleRemoveFilter = (filter) => () => removeFilter(filter);
|
|
||||||
|
|
||||||
return {
|
|
||||||
pageNumber,
|
|
||||||
numPages,
|
|
||||||
setPageNumber,
|
|
||||||
visibleList,
|
|
||||||
filterOptions: {
|
|
||||||
sortBy,
|
|
||||||
setSortBy,
|
|
||||||
filters,
|
|
||||||
handleRemoveFilter,
|
|
||||||
},
|
|
||||||
showFilters: filters.length > 0,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useCourseListData;
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { MockUseState } from 'testUtils';
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import { ListPageSize, SortKeys } from 'data/constants/app';
|
|
||||||
import * as hooks from './hooks';
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useCurrentCourseList: jest.fn(),
|
|
||||||
usePageNumber: jest.fn(() => 23),
|
|
||||||
useSetPageNumber: jest.fn(),
|
|
||||||
useFilters: jest.fn(),
|
|
||||||
useRemoveFilter: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockGet = jest.fn(() => ({}));
|
|
||||||
|
|
||||||
global.URLSearchParams = jest.fn().mockImplementation(() => ({
|
|
||||||
get: mockGet,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const state = new MockUseState(hooks);
|
|
||||||
|
|
||||||
const testList = ['a', 'b'];
|
|
||||||
const testListData = {
|
|
||||||
numPages: 52,
|
|
||||||
visibleList: testList,
|
|
||||||
};
|
|
||||||
const testSortBy = 'fake sort option';
|
|
||||||
const testFilters = ['some', 'fake', 'filters'];
|
|
||||||
|
|
||||||
const setPageNumber = jest.fn(val => ({ setPageNumber: val }));
|
|
||||||
reduxHooks.useSetPageNumber.mockReturnValue(setPageNumber);
|
|
||||||
|
|
||||||
const removeFilter = jest.fn();
|
|
||||||
reduxHooks.useRemoveFilter.mockReturnValue(removeFilter);
|
|
||||||
reduxHooks.useFilters.mockReturnValue(['some', 'fake', 'filters']);
|
|
||||||
|
|
||||||
describe('CourseList hooks', () => {
|
|
||||||
let out;
|
|
||||||
|
|
||||||
reduxHooks.useCurrentCourseList.mockReturnValue(testListData);
|
|
||||||
|
|
||||||
describe('state values', () => {
|
|
||||||
state.testGetter(state.keys.sortBy);
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useCourseListData', () => {
|
|
||||||
afterEach(state.restore);
|
|
||||||
beforeEach(() => {
|
|
||||||
state.mock();
|
|
||||||
state.mockVal(state.keys.sortBy, testSortBy);
|
|
||||||
out = hooks.useCourseListData();
|
|
||||||
});
|
|
||||||
describe('behavior', () => {
|
|
||||||
it('initializes sort with enrollment date', () => {
|
|
||||||
state.expectInitializedWith(state.keys.sortBy, SortKeys.enrolled);
|
|
||||||
});
|
|
||||||
it('loads current course list with sortBy, filters, and page size', () => {
|
|
||||||
expect(reduxHooks.useCurrentCourseList).toHaveBeenCalledWith({
|
|
||||||
sortBy: testSortBy,
|
|
||||||
filters: testFilters,
|
|
||||||
pageSize: ListPageSize,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('loads current course list with page size 0 if/when there is query param disable_pagination=1', () => {
|
|
||||||
state.mock();
|
|
||||||
state.mockVal(state.keys.sortBy, testSortBy);
|
|
||||||
mockGet.mockReturnValueOnce('1');
|
|
||||||
out = hooks.useCourseListData();
|
|
||||||
expect(reduxHooks.useCurrentCourseList).toHaveBeenCalledWith({
|
|
||||||
sortBy: testSortBy,
|
|
||||||
filters: testFilters,
|
|
||||||
pageSize: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('output', () => {
|
|
||||||
test('pageNumber loads from usePageNumber hook', () => {
|
|
||||||
expect(out.pageNumber).toEqual(reduxHooks.usePageNumber());
|
|
||||||
});
|
|
||||||
test('numPages and visible list load from useCurrentCourseList hook', () => {
|
|
||||||
expect(out.numPages).toEqual(testListData.numPages);
|
|
||||||
expect(out.visibleList).toEqual(testListData.visibleList);
|
|
||||||
});
|
|
||||||
test('showFilters is true iff filters is not empty', () => {
|
|
||||||
expect(out.showFilters).toEqual(true);
|
|
||||||
state.mockVal(state.keys.sortBy, testSortBy);
|
|
||||||
reduxHooks.useFilters.mockReturnValueOnce([]);
|
|
||||||
out = hooks.useCourseListData();
|
|
||||||
// don't show filter when list is empty.
|
|
||||||
expect(out.showFilters).toEqual(false);
|
|
||||||
});
|
|
||||||
describe('filterOptions', () => {
|
|
||||||
test('sortBy and setSortBy are connected to the state value', () => {
|
|
||||||
expect(out.filterOptions.sortBy).toEqual(testSortBy);
|
|
||||||
expect(out.filterOptions.setSortBy).toEqual(state.setState.sortBy);
|
|
||||||
});
|
|
||||||
test('filters passed by useFilters hook', () => {
|
|
||||||
expect(out.filterOptions.filters).toEqual(testFilters);
|
|
||||||
});
|
|
||||||
test('handleRemoveFilter creates callback to call setFilter.remove', () => {
|
|
||||||
const cb = out.filterOptions.handleRemoveFilter(testFilters[0]);
|
|
||||||
expect(removeFilter).not.toHaveBeenCalled();
|
|
||||||
cb();
|
|
||||||
expect(removeFilter).toHaveBeenCalledWith(testFilters[0]);
|
|
||||||
});
|
|
||||||
test('setPageNumber dispatches setPageNumber action with passed value', () => {
|
|
||||||
expect(out.setPageNumber(2)).toEqual(setPageNumber(2));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import {
|
import {
|
||||||
CourseFilterControls,
|
CourseFilterControls,
|
||||||
} from 'containers/CourseFilterControls';
|
} from 'containers/CourseFilterControls';
|
||||||
import CourseListSlot from 'plugin-slots/CourseListSlot';
|
import CourseListSlot from 'plugin-slots/CourseListSlot';
|
||||||
import NoCoursesViewSlot from 'plugin-slots/NoCoursesViewSlot';
|
import NoCoursesViewSlot from 'plugin-slots/NoCoursesViewSlot';
|
||||||
|
import { useFilters } from 'data/context';
|
||||||
|
|
||||||
import { useCourseListData } from './hooks';
|
import { getVisibleList, getTransformedCourseDataList } from 'utils/dataTransformers';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
@@ -22,14 +22,46 @@ import './index.scss';
|
|||||||
*/
|
*/
|
||||||
export const CoursesPanel = () => {
|
export const CoursesPanel = () => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const hasCourses = reduxHooks.useHasCourses();
|
const { data } = useInitializeLearnerHome();
|
||||||
const courseListData = useCourseListData();
|
const hasCourses = useMemo(() => data?.courses?.length > 0, [data]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
filters, sortBy, pageNumber, setPageNumber,
|
||||||
|
} = useFilters();
|
||||||
|
const { visibleList, numPages } = useMemo(() => {
|
||||||
|
let transformedCourses = [];
|
||||||
|
if (data?.courses?.length) {
|
||||||
|
transformedCourses = getTransformedCourseDataList(data.courses);
|
||||||
|
}
|
||||||
|
return getVisibleList(
|
||||||
|
transformedCourses,
|
||||||
|
filters,
|
||||||
|
sortBy,
|
||||||
|
pageNumber,
|
||||||
|
);
|
||||||
|
}, [data, filters, sortBy, pageNumber]);
|
||||||
|
|
||||||
|
// Clamp page number when filtered/mutated list shrinks
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (numPages > 0 && pageNumber > numPages) {
|
||||||
|
setPageNumber(1);
|
||||||
|
}
|
||||||
|
}, [numPages, pageNumber, setPageNumber]);
|
||||||
|
|
||||||
|
const courseListData = {
|
||||||
|
filterOptions: filters,
|
||||||
|
setPageNumber,
|
||||||
|
numPages,
|
||||||
|
visibleList,
|
||||||
|
showFilters: filters.length > 0,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="course-list-container">
|
<div className="course-list-container">
|
||||||
<div className="course-list-heading-container">
|
<div className="course-list-heading-container">
|
||||||
<h2 className="course-list-title">{formatMessage(messages.myCourses)}</h2>
|
<h2 className="course-list-title">{formatMessage(messages.myCourses)}</h2>
|
||||||
<div className="course-filter-controls-container">
|
<div className="course-filter-controls-container">
|
||||||
<CourseFilterControls {...courseListData.filterOptions} />
|
<CourseFilterControls />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hasCourses ? <CourseListSlot courseListData={courseListData} /> : <NoCoursesViewSlot />}
|
{hasCourses ? <CourseListSlot courseListData={courseListData} /> : <NoCoursesViewSlot />}
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { FilterKeys } from 'data/constants/app';
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useFilters } from 'data/context';
|
||||||
|
import * as dataTransformers from 'utils/dataTransformers';
|
||||||
import messagesNoCourses from 'containers/CoursesPanel/NoCoursesView/messages';
|
import messagesNoCourses from 'containers/CoursesPanel/NoCoursesView/messages';
|
||||||
import { useCourseListData } from './hooks';
|
|
||||||
import CoursesPanel from '.';
|
import CoursesPanel from '.';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const courseSearchUrl = '/course-search-url';
|
jest.mock('data/hooks', () => ({
|
||||||
|
useInitializeLearnerHome: jest.fn(() => ({
|
||||||
jest.mock('hooks', () => ({
|
data: {
|
||||||
reduxHooks: {
|
courses: [{ id: 1 }, { id: 2 }],
|
||||||
useHasCourses: jest.fn(),
|
},
|
||||||
usePlatformSettingsData: jest.fn(() => ({
|
})),
|
||||||
courseSearchUrl,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('data/context', () => ({
|
||||||
useCourseListData: jest.fn(),
|
useFilters: jest.fn(() => ({
|
||||||
|
filters: [],
|
||||||
|
sortBy: 'enrolled',
|
||||||
|
pageNumber: 1,
|
||||||
|
setPageNumber: jest.fn(),
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('containers/CourseCard', () => jest.fn(() => <div>CourseCard</div>));
|
jest.mock('containers/CourseCard', () => jest.fn(() => <div>CourseCard</div>));
|
||||||
@@ -33,30 +34,14 @@ jest.mock('@openedx/frontend-plugin-framework', () => ({
|
|||||||
PluginSlot: 'PluginSlot',
|
PluginSlot: 'PluginSlot',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const filters = Object.values(FilterKeys);
|
|
||||||
|
|
||||||
reduxHooks.useHasCourses.mockReturnValue(true);
|
|
||||||
|
|
||||||
describe('CoursesPanel', () => {
|
describe('CoursesPanel', () => {
|
||||||
const defaultCourseListData = {
|
|
||||||
filterOptions: { filters, handleRemoveFilter: jest.fn() },
|
|
||||||
numPages: 1,
|
|
||||||
setPageNumber: jest.fn().mockName('setPageNumber'),
|
|
||||||
showFilters: false,
|
|
||||||
visibleList: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const createWrapper = (courseListData) => {
|
const createWrapper = (courseListData) => {
|
||||||
useCourseListData.mockReturnValue({
|
useInitializeLearnerHome.mockReturnValue({ data: { courses: courseListData?.visibleList || [] } });
|
||||||
...defaultCourseListData,
|
|
||||||
...courseListData,
|
|
||||||
});
|
|
||||||
return render(<IntlProvider locale="en"><CoursesPanel /></IntlProvider>);
|
return render(<IntlProvider locale="en"><CoursesPanel /></IntlProvider>);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('no courses', () => {
|
describe('no courses', () => {
|
||||||
it('should render no courses view slot', () => {
|
it('should render no courses view slot', () => {
|
||||||
reduxHooks.useHasCourses.mockReturnValue(false);
|
|
||||||
createWrapper();
|
createWrapper();
|
||||||
const imgNoCourses = screen.getByRole('img', { name: messagesNoCourses.bannerAlt.defaultMessage });
|
const imgNoCourses = screen.getByRole('img', { name: messagesNoCourses.bannerAlt.defaultMessage });
|
||||||
expect(imgNoCourses).toBeInTheDocument();
|
expect(imgNoCourses).toBeInTheDocument();
|
||||||
@@ -67,23 +52,106 @@ describe('CoursesPanel', () => {
|
|||||||
describe('with courses', () => {
|
describe('with courses', () => {
|
||||||
it('should render courselist', () => {
|
it('should render courselist', () => {
|
||||||
const visibleList = [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }];
|
const visibleList = [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }];
|
||||||
reduxHooks.useHasCourses.mockReturnValue(true);
|
|
||||||
createWrapper({ visibleList });
|
createWrapper({ visibleList });
|
||||||
const courseCards = screen.getAllByText('CourseCard');
|
const courseCards = screen.getAllByText('CourseCard');
|
||||||
expect(courseCards.length).toEqual(visibleList.length);
|
expect(courseCards.length).toEqual(visibleList.length);
|
||||||
});
|
});
|
||||||
it('displays course filter controls', () => {
|
it('displays course filter controls', () => {
|
||||||
reduxHooks.useHasCourses.mockReturnValue(true);
|
|
||||||
createWrapper();
|
createWrapper();
|
||||||
expect(screen.getByText('CourseFilterControls')).toBeInTheDocument();
|
expect(screen.getByText('CourseFilterControls')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays course list slot when courses exist', () => {
|
it('displays course list slot when courses exist', () => {
|
||||||
reduxHooks.useHasCourses.mockReturnValue(true);
|
|
||||||
const visibleList = [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }];
|
const visibleList = [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }];
|
||||||
createWrapper({ visibleList });
|
createWrapper({ visibleList });
|
||||||
const heading = screen.getByText(messages.myCourses.defaultMessage);
|
const heading = screen.getByText(messages.myCourses.defaultMessage);
|
||||||
expect(heading).toBeInTheDocument();
|
expect(heading).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('page number clamping', () => {
|
||||||
|
const mockSetPageNumber = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.spyOn(dataTransformers, 'getTransformedCourseDataList');
|
||||||
|
jest.spyOn(dataTransformers, 'getVisibleList');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clamps page number to 1 when current page exceeds total pages', () => {
|
||||||
|
useFilters.mockReturnValue({
|
||||||
|
filters: [],
|
||||||
|
sortBy: 'enrolled',
|
||||||
|
pageNumber: 5, // User is on page 5
|
||||||
|
setPageNumber: mockSetPageNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
dataTransformers.getTransformedCourseDataList.mockReturnValue([{ id: 1 }, { id: 2 }]);
|
||||||
|
dataTransformers.getVisibleList.mockReturnValue({
|
||||||
|
visibleList: [{ id: 1 }],
|
||||||
|
numPages: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
createWrapper({ visibleList: [{ id: 1 }] });
|
||||||
|
|
||||||
|
expect(mockSetPageNumber).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not clamp page number when current page is valid', () => {
|
||||||
|
useFilters.mockReturnValue({
|
||||||
|
filters: [],
|
||||||
|
sortBy: 'enrolled',
|
||||||
|
pageNumber: 2,
|
||||||
|
setPageNumber: mockSetPageNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
dataTransformers.getTransformedCourseDataList.mockReturnValue([{ id: 1 }, { id: 2 }]);
|
||||||
|
dataTransformers.getVisibleList.mockReturnValue({
|
||||||
|
visibleList: [{ id: 1 }],
|
||||||
|
numPages: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
createWrapper({ visibleList: [{ id: 1 }] });
|
||||||
|
|
||||||
|
expect(mockSetPageNumber).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not clamp when numPages is 0', () => {
|
||||||
|
useFilters.mockReturnValue({
|
||||||
|
filters: [],
|
||||||
|
sortBy: 'enrolled',
|
||||||
|
pageNumber: 2,
|
||||||
|
setPageNumber: mockSetPageNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
dataTransformers.getTransformedCourseDataList.mockReturnValue([]);
|
||||||
|
dataTransformers.getVisibleList.mockReturnValue({
|
||||||
|
visibleList: [],
|
||||||
|
numPages: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
createWrapper({ visibleList: [] });
|
||||||
|
|
||||||
|
expect(mockSetPageNumber).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles edge case when pageNumber equals numPages', () => {
|
||||||
|
useFilters.mockReturnValue({
|
||||||
|
filters: [],
|
||||||
|
sortBy: 'enrolled',
|
||||||
|
pageNumber: 2,
|
||||||
|
setPageNumber: mockSetPageNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
dataTransformers.getTransformedCourseDataList.mockReturnValue([{ id: 1 }, { id: 2 }]);
|
||||||
|
dataTransformers.getVisibleList.mockReturnValue({
|
||||||
|
visibleList: [{ id: 1 }],
|
||||||
|
numPages: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
createWrapper({ visibleList: [{ id: 1 }] });
|
||||||
|
|
||||||
|
expect(mockSetPageNumber).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
|
|||||||
import hooks from './hooks';
|
import hooks from './hooks';
|
||||||
import DashboardLayout from './DashboardLayout';
|
import DashboardLayout from './DashboardLayout';
|
||||||
|
|
||||||
|
jest.mock('data/hooks', () => ({
|
||||||
|
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||||
|
data: {
|
||||||
|
platformSettings: {
|
||||||
|
courseSearchUrl: '/courses',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('./hooks', () => ({
|
||||||
useDashboardLayoutData: jest.fn(),
|
useDashboardLayoutData: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { apiHooks } from 'hooks';
|
|
||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
|
|
||||||
import appMessages from 'messages';
|
import appMessages from 'messages';
|
||||||
@@ -11,11 +10,6 @@ export const state = StrictDict({
|
|||||||
sidebarShowing: (val) => React.useState(val), // eslint-disable-line
|
sidebarShowing: (val) => React.useState(val), // eslint-disable-line
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useInitializeDashboard = () => {
|
|
||||||
const initialize = apiHooks.useInitializeApp();
|
|
||||||
React.useEffect(() => { initialize(); }, []); // eslint-disable-line
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDashboardMessages = () => {
|
export const useDashboardMessages = () => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
return {
|
return {
|
||||||
@@ -37,6 +31,5 @@ export const useDashboardLayoutData = () => {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
useDashboardLayoutData,
|
useDashboardLayoutData,
|
||||||
useInitializeDashboard,
|
|
||||||
useDashboardMessages,
|
useDashboardMessages,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||||
|
|
||||||
import { apiHooks } from 'hooks';
|
|
||||||
import { MockUseState } from 'testUtils';
|
import { MockUseState } from 'testUtils';
|
||||||
|
|
||||||
import appMessages from 'messages';
|
import appMessages from 'messages';
|
||||||
@@ -25,12 +22,6 @@ jest.mock('@edx/frontend-platform/i18n', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
apiHooks: {
|
|
||||||
useInitializeApp: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('react', () => ({
|
jest.mock('react', () => ({
|
||||||
...jest.requireActual('react'),
|
...jest.requireActual('react'),
|
||||||
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
|
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
|
||||||
@@ -38,8 +29,6 @@ jest.mock('react', () => ({
|
|||||||
|
|
||||||
const state = new MockUseState(hooks);
|
const state = new MockUseState(hooks);
|
||||||
|
|
||||||
const initializeApp = jest.fn();
|
|
||||||
apiHooks.useInitializeApp.mockReturnValue(initializeApp);
|
|
||||||
useWindowSize.mockReturnValue({ width: 20 });
|
useWindowSize.mockReturnValue({ width: 20 });
|
||||||
breakpoints.large = { maxWidth: 30 };
|
breakpoints.large = { maxWidth: 30 };
|
||||||
describe('CourseCard hooks', () => {
|
describe('CourseCard hooks', () => {
|
||||||
@@ -77,16 +66,6 @@ describe('CourseCard hooks', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('useInitializeDashboard', () => {
|
|
||||||
it('dispatches initialize thunk action on component load', () => {
|
|
||||||
hooks.useInitializeDashboard();
|
|
||||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
|
||||||
expect(prereqs).toEqual([]);
|
|
||||||
expect(initializeApp).not.toHaveBeenCalled();
|
|
||||||
cb();
|
|
||||||
expect(initializeApp).toHaveBeenCalledWith();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('useDashboardMessages', () => {
|
describe('useDashboardMessages', () => {
|
||||||
it('returns spinner screen reader text', () => {
|
it('returns spinner screen reader text', () => {
|
||||||
expect(hooks.useDashboardMessages().spinnerScreenReaderText).toEqual(
|
expect(hooks.useDashboardMessages().spinnerScreenReaderText).toEqual(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useSelectSessionModal } from 'data/context';
|
||||||
import { RequestKeys } from 'data/constants/requests';
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import SelectSessionModal from 'containers/SelectSessionModal';
|
import SelectSessionModal from 'containers/SelectSessionModal';
|
||||||
import CoursesPanel from 'containers/CoursesPanel';
|
import CoursesPanel from 'containers/CoursesPanel';
|
||||||
import DashboardModalSlot from 'plugin-slots/DashboardModalSlot';
|
import DashboardModalSlot from 'plugin-slots/DashboardModalSlot';
|
||||||
@@ -12,23 +12,24 @@ import hooks from './hooks';
|
|||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
export const Dashboard = () => {
|
export const Dashboard = () => {
|
||||||
hooks.useInitializeDashboard();
|
const { data, isPending } = useInitializeLearnerHome();
|
||||||
const { pageTitle } = hooks.useDashboardMessages();
|
const { pageTitle } = hooks.useDashboardMessages();
|
||||||
const hasCourses = reduxHooks.useHasCourses();
|
const { selectSessionModal } = useSelectSessionModal();
|
||||||
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
|
const showSelectSessionModal = selectSessionModal.cardId !== null;
|
||||||
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
|
|
||||||
|
const hasCourses = useMemo(() => data?.courses?.length > 0, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="dashboard-container" className="d-flex flex-column p-2 pt-0">
|
<div id="dashboard-container" className="d-flex flex-column p-2 pt-0">
|
||||||
<h1 className="sr-only">{pageTitle}</h1>
|
<h1 className="sr-only">{pageTitle}</h1>
|
||||||
{!initIsPending && (
|
{!isPending && (
|
||||||
<>
|
<>
|
||||||
<DashboardModalSlot />
|
<DashboardModalSlot />
|
||||||
{(hasCourses && showSelectSessionModal) && <SelectSessionModal />}
|
{(hasCourses && showSelectSessionModal) && <SelectSessionModal />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div id="dashboard-content" data-testid="dashboard-content">
|
<div id="dashboard-content" data-testid="dashboard-content">
|
||||||
{initIsPending
|
{isPending
|
||||||
? (<LoadingView />)
|
? (<LoadingView />)
|
||||||
: (
|
: (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { useSelectSessionModal } from 'data/context';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
|
||||||
import hooks from './hooks';
|
import hooks from './hooks';
|
||||||
import Dashboard from '.';
|
import Dashboard from '.';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('data/context', () => ({
|
||||||
reduxHooks: {
|
useSelectSessionModal: jest.fn(),
|
||||||
useHasCourses: jest.fn(),
|
}));
|
||||||
useShowSelectSessionModal: jest.fn(),
|
|
||||||
useRequestIsPending: jest.fn(),
|
jest.mock('data/hooks', () => ({
|
||||||
},
|
useInitializeLearnerHome: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('./hooks', () => ({
|
||||||
@@ -34,9 +35,9 @@ describe('Dashboard', () => {
|
|||||||
showSelectSessionModal = true,
|
showSelectSessionModal = true,
|
||||||
} = props;
|
} = props;
|
||||||
hooks.useDashboardMessages.mockReturnValue({ pageTitle });
|
hooks.useDashboardMessages.mockReturnValue({ pageTitle });
|
||||||
reduxHooks.useHasCourses.mockReturnValue(hasCourses);
|
const dataMocked = { data: hasCourses ? { courses: [1, 2] } : { courses: [] }, isPending: initIsPending };
|
||||||
reduxHooks.useRequestIsPending.mockReturnValue(initIsPending);
|
useInitializeLearnerHome.mockReturnValue(dataMocked);
|
||||||
reduxHooks.useShowSelectSessionModal.mockReturnValue(showSelectSessionModal);
|
useSelectSessionModal.mockReturnValue({ selectSessionModal: showSelectSessionModal ? { cardId: 1 } : null });
|
||||||
return render(<IntlProvider locale="en"><Dashboard /></IntlProvider>);
|
return render(<IntlProvider locale="en"><Dashboard /></IntlProvider>);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
import { reduxHooks, apiHooks } from 'hooks';
|
import { useUpdateEmailSettings } from 'data/hooks';
|
||||||
|
import { useCourseData } from 'hooks';
|
||||||
|
|
||||||
import * as module from './hooks';
|
import * as module from './hooks';
|
||||||
|
|
||||||
@@ -13,12 +14,14 @@ export const useEmailData = ({
|
|||||||
closeModal,
|
closeModal,
|
||||||
cardId,
|
cardId,
|
||||||
}) => {
|
}) => {
|
||||||
const { hasOptedOutOfEmail } = reduxHooks.useCardEnrollmentData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
|
const hasOptedOutOfEmail = courseData?.enrollment?.hasOptedOutOfEmail || false;
|
||||||
|
const courseId = courseData?.courseRun?.courseId;
|
||||||
const [isOptedOut, setIsOptedOut] = module.state.toggle(hasOptedOutOfEmail);
|
const [isOptedOut, setIsOptedOut] = module.state.toggle(hasOptedOutOfEmail);
|
||||||
const updateEmailSettings = apiHooks.useUpdateEmailSettings(cardId);
|
const { mutate: updateEmailSettings } = useUpdateEmailSettings();
|
||||||
const onToggle = () => setIsOptedOut(!isOptedOut);
|
const onToggle = () => setIsOptedOut(!isOptedOut);
|
||||||
const save = () => {
|
const save = () => {
|
||||||
updateEmailSettings(!isOptedOut);
|
updateEmailSettings({ courseId, enable: !isOptedOut });
|
||||||
closeModal();
|
closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { MockUseState } from 'testUtils';
|
|
||||||
import { reduxHooks, apiHooks } from 'hooks';
|
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useCardEnrollmentData: jest.fn(),
|
|
||||||
},
|
|
||||||
apiHooks: {
|
|
||||||
useUpdateEmailSettings: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cardId = 'my-test-course-number';
|
|
||||||
const closeModal = jest.fn();
|
|
||||||
const updateEmailSettings = jest.fn();
|
|
||||||
apiHooks.useUpdateEmailSettings.mockReturnValue(updateEmailSettings);
|
|
||||||
|
|
||||||
const state = new MockUseState(hooks);
|
|
||||||
|
|
||||||
describe('EmailSettingsModal hooks', () => {
|
|
||||||
let out;
|
|
||||||
describe('state values', () => {
|
|
||||||
state.testGetter(state.keys.toggle);
|
|
||||||
});
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
describe('useEmailData', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
state.mock();
|
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasOptedOutOfEmail: true });
|
|
||||||
out = hooks.useEmailData({ closeModal, cardId });
|
|
||||||
});
|
|
||||||
afterEach(state.restore);
|
|
||||||
|
|
||||||
it('loads enrollment data based on course number', () => {
|
|
||||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('initializes toggle value to cardData.hasOptedOutOfEmail', () => {
|
|
||||||
state.expectInitializedWith(state.keys.toggle, true);
|
|
||||||
expect(out.isOptedOut).toEqual(true);
|
|
||||||
|
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasOptedOutOfEmail: false });
|
|
||||||
out = hooks.useEmailData({ closeModal, cardId });
|
|
||||||
state.expectInitializedWith(state.keys.toggle, false);
|
|
||||||
expect(out.isOptedOut).toEqual(false);
|
|
||||||
});
|
|
||||||
it('initializes email settings hok with cardId', () => {
|
|
||||||
expect(apiHooks.useUpdateEmailSettings).toHaveBeenCalledWith(cardId);
|
|
||||||
});
|
|
||||||
describe('onToggle - returned callback', () => {
|
|
||||||
it('sets toggle state value to opposite current value', () => {
|
|
||||||
out.onToggle();
|
|
||||||
expect(state.setState.toggle).toHaveBeenCalledWith(!out.isOptedOut);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('save', () => {
|
|
||||||
it('calls updateEmailSettings', () => {
|
|
||||||
out.save();
|
|
||||||
expect(updateEmailSettings).toHaveBeenCalledWith(!out.isOptedOut);
|
|
||||||
});
|
|
||||||
it('calls closeModal', () => {
|
|
||||||
out.save();
|
|
||||||
expect(closeModal).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
134
src/containers/EmailSettingsModal/hooks.test.jsx
Normal file
134
src/containers/EmailSettingsModal/hooks.test.jsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { useCourseData } from 'hooks';
|
||||||
|
import * as api from 'data/services/lms/api';
|
||||||
|
|
||||||
|
import { useEmailData } from './hooks';
|
||||||
|
|
||||||
|
jest.mock('hooks', () => ({
|
||||||
|
useCourseData: jest.fn(() => ({
|
||||||
|
enrollment: {},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('data/services/lms/api', () => ({
|
||||||
|
updateEmailSettings: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const createWrapper = () => {
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||||
|
});
|
||||||
|
const wrapper = ({ children }) => <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardId = 'my-test-course-number';
|
||||||
|
const closeModal = jest.fn();
|
||||||
|
|
||||||
|
describe('EmailSettingsModal hooks', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useEmailData', () => {
|
||||||
|
it('loads enrollment data based on course number', () => {
|
||||||
|
useCourseData.mockReturnValue({ enrollment: { hasOptedOutOfEmail: true } });
|
||||||
|
|
||||||
|
renderHook(() => useEmailData({ closeModal, cardId }), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes toggle value to cardData.hasOptedOutOfEmail when true', () => {
|
||||||
|
useCourseData.mockReturnValue({ enrollment: { hasOptedOutOfEmail: true } });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useEmailData({ closeModal, cardId }), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isOptedOut).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes toggle value to cardData.hasOptedOutOfEmail when false', () => {
|
||||||
|
useCourseData.mockReturnValue({ enrollment: { hasOptedOutOfEmail: false } });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useEmailData({ closeModal, cardId }), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isOptedOut).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes toggle value to false when hasOptedOutOfEmail is undefined', () => {
|
||||||
|
useCourseData.mockReturnValue({ enrollment: {} });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useEmailData({ closeModal, cardId }), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isOptedOut).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles state value when onToggle is called', () => {
|
||||||
|
useCourseData.mockReturnValue({ enrollment: { hasOptedOutOfEmail: true } });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useEmailData({ closeModal, cardId }), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isOptedOut).toEqual(true);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.onToggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isOptedOut).toEqual(false);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.onToggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isOptedOut).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls updateEmailSettings api and closeModal when save is called', async () => {
|
||||||
|
const courseId = 'test-course-id';
|
||||||
|
useCourseData.mockReturnValue({ enrollment: { hasOptedOutOfEmail: true }, courseRun: { courseId } });
|
||||||
|
api.updateEmailSettings.mockResolvedValue({});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useEmailData({ closeModal, cardId }), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedArg = { courseId, enable: !result.current.isOptedOut };
|
||||||
|
expect(api.updateEmailSettings).toHaveBeenCalledWith(expectedArg);
|
||||||
|
expect(closeModal).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls updateEmailSettings with enable:true when isOptedOut is false', async () => {
|
||||||
|
const courseId = 'test-course-id';
|
||||||
|
useCourseData.mockReturnValue({ enrollment: { hasOptedOutOfEmail: false }, courseRun: { courseId } });
|
||||||
|
api.updateEmailSettings.mockResolvedValue({});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useEmailData({ closeModal, cardId }), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(api.updateEmailSettings).toHaveBeenCalledWith({
|
||||||
|
courseId,
|
||||||
|
enable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
import { apiHooks, reduxHooks } from 'hooks';
|
import { useInitializeLearnerHome, useSendConfirmEmail } from 'data/hooks';
|
||||||
|
|
||||||
import * as module from './hooks';
|
import * as module from './hooks';
|
||||||
|
|
||||||
@@ -11,13 +11,15 @@ export const state = StrictDict({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const useConfirmEmailBannerData = () => {
|
export const useConfirmEmailBannerData = () => {
|
||||||
const { isNeeded } = reduxHooks.useEmailConfirmationData();
|
const { data: learnerData } = useInitializeLearnerHome();
|
||||||
|
const isNeeded = learnerData?.emailConfirmation?.isNeeded || false;
|
||||||
|
const sendEmailUrl = learnerData?.emailConfirmation?.sendEmailUrl || '';
|
||||||
|
const { mutate: sendConfirmEmail } = useSendConfirmEmail(sendEmailUrl);
|
||||||
const [showPageBanner, setShowPageBanner] = module.state.showPageBanner(isNeeded);
|
const [showPageBanner, setShowPageBanner] = module.state.showPageBanner(isNeeded);
|
||||||
const [showConfirmModal, setShowConfirmModal] = module.state.showConfirmModal(false);
|
const [showConfirmModal, setShowConfirmModal] = module.state.showConfirmModal(false);
|
||||||
const closePageBanner = () => setShowPageBanner(false);
|
const closePageBanner = () => setShowPageBanner(false);
|
||||||
const closeConfirmModal = () => setShowConfirmModal(false);
|
const closeConfirmModal = () => setShowConfirmModal(false);
|
||||||
const openConfirmModal = () => setShowConfirmModal(true);
|
const openConfirmModal = () => setShowConfirmModal(true);
|
||||||
const sendConfirmEmail = apiHooks.useSendConfirmEmail();
|
|
||||||
|
|
||||||
const openConfirmModalButtonClick = () => {
|
const openConfirmModalButtonClick = () => {
|
||||||
sendConfirmEmail();
|
sendConfirmEmail();
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import { MockUseState } from 'testUtils';
|
|
||||||
import { reduxHooks, apiHooks } from 'hooks';
|
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useEmailConfirmationData: jest.fn(),
|
|
||||||
},
|
|
||||||
apiHooks: {
|
|
||||||
useSendConfirmEmail: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const sendConfirmEmail = jest.fn();
|
|
||||||
apiHooks.useSendConfirmEmail.mockReturnValue(sendConfirmEmail);
|
|
||||||
|
|
||||||
const emailConfirmation = {
|
|
||||||
isNeeded: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = new MockUseState(hooks);
|
|
||||||
|
|
||||||
describe('ConfirmEmailBanner hooks', () => {
|
|
||||||
let out;
|
|
||||||
describe('state values', () => {
|
|
||||||
state.testGetter(state.keys.showPageBanner);
|
|
||||||
state.testGetter(state.keys.showConfirmModal);
|
|
||||||
});
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
describe('useEmailConfirmationData', () => {
|
|
||||||
beforeEach(() => state.mock());
|
|
||||||
afterEach(state.restore);
|
|
||||||
|
|
||||||
test('show page banner on unverified email', () => {
|
|
||||||
reduxHooks.useEmailConfirmationData.mockReturnValueOnce({ ...emailConfirmation });
|
|
||||||
out = hooks.useConfirmEmailBannerData();
|
|
||||||
expect(out.isNeeded).toEqual(emailConfirmation.isNeeded);
|
|
||||||
reduxHooks.useEmailConfirmationData.mockReturnValueOnce({ isNeeded: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('hide page banner on verified email', () => {
|
|
||||||
reduxHooks.useEmailConfirmationData.mockReturnValueOnce({ isNeeded: false });
|
|
||||||
out = hooks.useConfirmEmailBannerData();
|
|
||||||
expect(out.isNeeded).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('behavior', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
state.mock();
|
|
||||||
reduxHooks.useEmailConfirmationData.mockReturnValueOnce({ ...emailConfirmation });
|
|
||||||
out = hooks.useConfirmEmailBannerData();
|
|
||||||
});
|
|
||||||
afterEach(state.restore);
|
|
||||||
test('closePageBanner', () => {
|
|
||||||
out.closePageBanner();
|
|
||||||
expect(state.values.showPageBanner).toEqual(false);
|
|
||||||
});
|
|
||||||
test('closeConfirmModal', () => {
|
|
||||||
out.closeConfirmModal();
|
|
||||||
expect(state.values.showConfirmModal).toEqual(false);
|
|
||||||
});
|
|
||||||
test('openConfirmModalButtonClick', () => {
|
|
||||||
out.openConfirmModalButtonClick();
|
|
||||||
expect(state.values.showConfirmModal).toEqual(true);
|
|
||||||
expect(sendConfirmEmail).toBeCalled();
|
|
||||||
});
|
|
||||||
test('userConfirmEmailButtonClick', () => {
|
|
||||||
out.userConfirmEmailButtonClick();
|
|
||||||
expect(state.values.showConfirmModal).toEqual(false);
|
|
||||||
expect(state.values.showPageBanner).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
import * as api from 'data/services/lms/api';
|
||||||
|
import * as hooks from './hooks';
|
||||||
|
|
||||||
|
jest.mock('data/hooks', () => ({
|
||||||
|
...jest.requireActual('data/hooks'),
|
||||||
|
useInitializeLearnerHome: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('data/services/lms/api', () => ({
|
||||||
|
sendConfirmEmail: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const emailConfirmation = {
|
||||||
|
isNeeded: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createWrapper = () => {
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||||
|
});
|
||||||
|
const wrapper = ({ children }) => <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ConfirmEmailBanner hooks', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
api.sendConfirmEmail.mockResolvedValue({});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useEmailConfirmationData', () => {
|
||||||
|
it('show page banner on unverified email', () => {
|
||||||
|
useInitializeLearnerHome.mockReturnValue({ data: { emailConfirmation: { ...emailConfirmation } } });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => hooks.useConfirmEmailBannerData(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isNeeded).toEqual(emailConfirmation.isNeeded);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hide page banner on verified email', () => {
|
||||||
|
useInitializeLearnerHome.mockReturnValue({ data: { emailConfirmation: { isNeeded: false } } });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => hooks.useConfirmEmailBannerData(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isNeeded).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('behavior', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useInitializeLearnerHome.mockReturnValue({ data: { emailConfirmation: { ...emailConfirmation } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closePageBanner', () => {
|
||||||
|
const { result } = renderHook(() => hooks.useConfirmEmailBannerData(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.closePageBanner();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.showPageBanner).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closeConfirmModal', () => {
|
||||||
|
const { result } = renderHook(() => hooks.useConfirmEmailBannerData(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.closeConfirmModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.showConfirmModal).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('openConfirmModalButtonClick', async () => {
|
||||||
|
const { result } = renderHook(() => hooks.useConfirmEmailBannerData(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.openConfirmModalButtonClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.showConfirmModal).toEqual(true);
|
||||||
|
expect(api.sendConfirmEmail).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('userConfirmEmailButtonClick', () => {
|
||||||
|
const { result } = renderHook(() => hooks.useConfirmEmailBannerData(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.userConfirmEmailButtonClick();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.showConfirmModal).toEqual(false);
|
||||||
|
expect(result.current.showPageBanner).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import MasqueradeBar from 'containers/MasqueradeBar';
|
import MasqueradeBar from 'containers/MasqueradeBar';
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
import Header from '@edx/frontend-component-header';
|
import Header from '@edx/frontend-component-header';
|
||||||
import { reduxHooks } from 'hooks';
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import urls from 'data/services/lms/urls';
|
import urls from 'data/services/lms/urls';
|
||||||
|
|
||||||
import ConfirmEmailBanner from './ConfirmEmailBanner';
|
import ConfirmEmailBanner from './ConfirmEmailBanner';
|
||||||
@@ -14,7 +14,8 @@ import './index.scss';
|
|||||||
|
|
||||||
export const LearnerDashboardHeader = () => {
|
export const LearnerDashboardHeader = () => {
|
||||||
const { authenticatedUser } = React.useContext(AppContext);
|
const { authenticatedUser } = React.useContext(AppContext);
|
||||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
const { data: learnerData } = useInitializeLearnerHome();
|
||||||
|
const courseSearchUrl = learnerData?.platformSettings?.courseSearchUrl || '';
|
||||||
|
|
||||||
const exploreCoursesClick = () => {
|
const exploreCoursesClick = () => {
|
||||||
findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
|
findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { findCoursesNavClicked } from './hooks';
|
|||||||
|
|
||||||
const courseSearchUrl = '/course-search-url';
|
const courseSearchUrl = '/course-search-url';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('data/hooks', () => ({
|
||||||
reduxHooks: {
|
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||||
usePlatformSettingsData: jest.fn(() => ({
|
data: {
|
||||||
courseSearchUrl,
|
platformSettings: {
|
||||||
})),
|
courseSearchUrl,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('./hooks', () => ({
|
||||||
...jest.requireActual('./hooks'),
|
...jest.requireActual('./hooks'),
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import { apiHooks, reduxHooks } from 'hooks';
|
|
||||||
import { StrictDict } from 'utils';
|
|
||||||
import * as module from './hooks';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
export const state = StrictDict({
|
|
||||||
masqueradeInput: (val) => React.useState(val), // eslint-disable-line
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useMasqueradeInput = () => {
|
|
||||||
const [masqueradeInput, setMasqueradeInput] = module.state.masqueradeInput('');
|
|
||||||
const handleMasqueradeInputChange = (e) => setMasqueradeInput(e.target.value);
|
|
||||||
return {
|
|
||||||
handleMasqueradeInputChange,
|
|
||||||
masqueradeInput,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const masqueradeErrorMessageMap = {
|
|
||||||
404: messages.NoStudentFound,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMasqueradeErrorMessage = (errorStatus) => {
|
|
||||||
if (errorStatus == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return masqueradeErrorMessageMap[errorStatus] || messages.UnknownError;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useMasqueradeBarData = ({
|
|
||||||
authenticatedUser,
|
|
||||||
}) => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const handleMasqueradeAs = apiHooks.useMasqueradeAs();
|
|
||||||
const handleClearMasquerade = apiHooks.useClearMasquerade();
|
|
||||||
|
|
||||||
const {
|
|
||||||
isMasquerading,
|
|
||||||
isMasqueradingFailed,
|
|
||||||
isMasqueradingPending,
|
|
||||||
masqueradeErrorStatus,
|
|
||||||
} = reduxHooks.useMasqueradeData();
|
|
||||||
const { masqueradeInput, handleMasqueradeInputChange } = module.useMasqueradeInput();
|
|
||||||
|
|
||||||
const masqueradeErrorMessage = getMasqueradeErrorMessage(masqueradeErrorStatus);
|
|
||||||
const handleMasqueradeSubmit = (user) => (e) => {
|
|
||||||
handleMasqueradeAs(user);
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
canMasquerade: authenticatedUser?.administrator,
|
|
||||||
isMasquerading,
|
|
||||||
isMasqueradingFailed,
|
|
||||||
isMasqueradingPending,
|
|
||||||
masqueradeErrorMessage,
|
|
||||||
masqueradeInput,
|
|
||||||
handleMasqueradeSubmit,
|
|
||||||
handleClearMasquerade,
|
|
||||||
handleMasqueradeInputChange,
|
|
||||||
formatMessage,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useMasqueradeBarData;
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { MockUseState } from 'testUtils';
|
|
||||||
import { apiHooks, reduxHooks } from 'hooks';
|
|
||||||
|
|
||||||
import * as hooks from './hooks';
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
|
||||||
const { formatMessage } = jest.requireActual('testUtils');
|
|
||||||
return {
|
|
||||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
|
||||||
useIntl: () => ({
|
|
||||||
formatMessage,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
apiHooks: {
|
|
||||||
useMasqueradeAs: jest.fn(),
|
|
||||||
useClearMasquerade: jest.fn(),
|
|
||||||
},
|
|
||||||
reduxHooks: {
|
|
||||||
useMasqueradeData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const masqueradeAs = jest.fn();
|
|
||||||
const clearMasquerade = jest.fn();
|
|
||||||
apiHooks.useMasqueradeAs.mockReturnValue(masqueradeAs);
|
|
||||||
apiHooks.useClearMasquerade.mockReturnValue(clearMasquerade);
|
|
||||||
const state = new MockUseState(hooks);
|
|
||||||
const testValue = 'test-value';
|
|
||||||
|
|
||||||
describe('MasqueradeBar hooks', () => {
|
|
||||||
const authenticatedUser = {
|
|
||||||
administrator: true,
|
|
||||||
};
|
|
||||||
const defaultMasqueradeData = {
|
|
||||||
isMasquerading: false,
|
|
||||||
isMasqueradingFailed: false,
|
|
||||||
isMasqueradingPending: false,
|
|
||||||
masqueradeErrorStatus: null,
|
|
||||||
};
|
|
||||||
const createHook = (masqueradeData = {}, user = undefined) => {
|
|
||||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({
|
|
||||||
...defaultMasqueradeData,
|
|
||||||
...masqueradeData,
|
|
||||||
});
|
|
||||||
return hooks.useMasqueradeBarData({ authenticatedUser: user || authenticatedUser });
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('state values', () => {
|
|
||||||
state.testGetter(state.keys.masqueradeInput);
|
|
||||||
});
|
|
||||||
describe('useMasqueradeBarData', () => {
|
|
||||||
beforeEach(() => state.mock());
|
|
||||||
afterEach(state.restore);
|
|
||||||
test('canMasquerade', () => {
|
|
||||||
const out = createHook();
|
|
||||||
expect(out.canMasquerade).toEqual(true);
|
|
||||||
});
|
|
||||||
test('cannotMasquerade', () => {
|
|
||||||
const out = createHook({}, { administrator: false });
|
|
||||||
expect(out.canMasquerade).toEqual(false);
|
|
||||||
});
|
|
||||||
test('masqueradeErrorStatus', () => {
|
|
||||||
let out = createHook();
|
|
||||||
expect(out.masqueradeErrorMessage).toBeNull();
|
|
||||||
out = createHook({ masqueradeErrorStatus: 0 });
|
|
||||||
expect(out.masqueradeErrorMessage).not.toBeNull();
|
|
||||||
});
|
|
||||||
test('isMasqueradePending', () => {
|
|
||||||
let out = createHook();
|
|
||||||
expect(out.isMasqueradingPending).toEqual(false);
|
|
||||||
out = createHook({ isMasqueradingPending: true });
|
|
||||||
expect(out.isMasqueradingPending).toEqual(true);
|
|
||||||
});
|
|
||||||
test('handleMasqueradeInputChange', () => {
|
|
||||||
const out = createHook();
|
|
||||||
expect(state.stateVals.masqueradeInput).toEqual('');
|
|
||||||
out.handleMasqueradeInputChange({ target: { value: testValue } });
|
|
||||||
expect(state.setState.masqueradeInput).toHaveBeenCalledWith(testValue);
|
|
||||||
});
|
|
||||||
test('handleMasqueradeSubmit', () => {
|
|
||||||
const out = createHook();
|
|
||||||
const preventDefault = jest.fn();
|
|
||||||
// make sure submit doesn't refresh the page
|
|
||||||
out.handleMasqueradeSubmit(testValue)({
|
|
||||||
preventDefault,
|
|
||||||
});
|
|
||||||
expect(masqueradeAs).toHaveBeenCalledWith(testValue);
|
|
||||||
expect(preventDefault).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
test('handleClearMasquerade', () => {
|
|
||||||
const out = createHook();
|
|
||||||
out.handleClearMasquerade();
|
|
||||||
expect(clearMasquerade).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getMasqueradeErrorMessage', () => {
|
|
||||||
test('null', () => {
|
|
||||||
expect(hooks.getMasqueradeErrorMessage()).toBeNull();
|
|
||||||
});
|
|
||||||
test('404', () => {
|
|
||||||
expect(hooks.getMasqueradeErrorMessage(404)).toEqual(messages.NoStudentFound);
|
|
||||||
});
|
|
||||||
test('default', () => {
|
|
||||||
expect(hooks.getMasqueradeErrorMessage(500)).toEqual(messages.UnknownError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -13,25 +14,39 @@ import {
|
|||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
import { Close, PersonSearch } from '@openedx/paragon/icons';
|
import { Close, PersonSearch } from '@openedx/paragon/icons';
|
||||||
|
|
||||||
|
import { useMasquerade } from 'data/context';
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { useMasqueradeBarData } from './hooks';
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
export const MasqueradeBar = () => {
|
export const MasqueradeBar = () => {
|
||||||
const { authenticatedUser } = React.useContext(AppContext);
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
canMasquerade,
|
isError, error, isPending,
|
||||||
isMasquerading,
|
} = useInitializeLearnerHome();
|
||||||
isMasqueradingFailed,
|
const { authenticatedUser } = React.useContext(AppContext);
|
||||||
isMasqueradingPending,
|
const [masqueradeInput, setMasqueradeInput] = React.useState('');
|
||||||
masqueradeInput,
|
const canMasquerade = authenticatedUser?.administrator;
|
||||||
masqueradeErrorMessage,
|
const { masqueradeUser, setMasqueradeUser } = useMasquerade();
|
||||||
handleMasqueradeInputChange,
|
const handleMasqueradeInputChange = (e) => setMasqueradeInput(e.target.value);
|
||||||
handleClearMasquerade,
|
const handleClearMasquerade = () => {
|
||||||
handleMasqueradeSubmit,
|
setMasqueradeUser(undefined);
|
||||||
formatMessage,
|
setMasqueradeInput('');
|
||||||
} = useMasqueradeBarData({ authenticatedUser });
|
};
|
||||||
|
const handleMasqueradeSubmit = (user) => (e) => {
|
||||||
|
setMasqueradeUser(user);
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMasqueradingFailed = !!masqueradeUser && masqueradeInput && isError;
|
||||||
|
const isMasqueradingPending = !!masqueradeUser && isPending;
|
||||||
|
const isMasquerading = !!masqueradeUser && !isError && !isPending;
|
||||||
|
const masqueradeErrorMessage = useMemo(() => {
|
||||||
|
if (masqueradeUser && error) {
|
||||||
|
return (error.customAttributes?.httpErrorStatus === 404 ? messages.NoStudentFound : messages.UnknownError);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [error, masqueradeUser]);
|
||||||
|
|
||||||
if (!canMasquerade) { return null; }
|
if (!canMasquerade) { return null; }
|
||||||
|
|
||||||
@@ -72,7 +87,7 @@ export const MasqueradeBar = () => {
|
|||||||
)}
|
)}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<StatefulButton
|
<StatefulButton
|
||||||
disabled={!masqueradeInput.length}
|
disabled={!masqueradeInput?.length}
|
||||||
variant="brand"
|
variant="brand"
|
||||||
onClick={handleMasqueradeSubmit(masqueradeInput)}
|
onClick={handleMasqueradeSubmit(masqueradeInput)}
|
||||||
labels={{
|
labels={{
|
||||||
|
|||||||
@@ -1,32 +1,55 @@
|
|||||||
|
import { useContext, useState } from 'react';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { formatMessage } from 'testUtils';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { useInitializeLearnerHome } from 'data/hooks';
|
||||||
|
import { useMasquerade } from 'data/context';
|
||||||
import MasqueradeBar from '.';
|
import MasqueradeBar from '.';
|
||||||
import hooks from './hooks';
|
// import hooks from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('data/context', () => ({
|
||||||
useMasqueradeBarData: jest.fn(),
|
useMasquerade: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('MasqueradeBar', () => {
|
jest.mock('react', () => {
|
||||||
const masqueradeMockData = {
|
const ActualReact = jest.requireActual('react');
|
||||||
canMasquerade: true,
|
return {
|
||||||
isMasquerading: false,
|
...ActualReact,
|
||||||
isMasqueradingFailed: false,
|
useState: jest.fn(ActualReact.useState),
|
||||||
isMasqueradingPending: false,
|
useContext: jest.fn((context) => {
|
||||||
masqueradeInput: '',
|
// eslint-disable-next-line global-require
|
||||||
masqueradeErrorMessage: '',
|
if (context === require('@edx/frontend-platform/react').AppContext) {
|
||||||
handleMasqueradeInputChange: jest.fn().mockName('handleMasqueradeInputChange'),
|
return {
|
||||||
handleClearMasquerade: jest.fn().mockName('handleClearMasquerade'),
|
authenticatedUser: { administrator: true },
|
||||||
handleMasqueradeSubmit: jest.fn().mockName('handleMasqueradeSubmit'),
|
};
|
||||||
formatMessage,
|
}
|
||||||
|
return ActualReact.useContext(context);
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('data/hooks', () => ({
|
||||||
|
useInitializeLearnerHome: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockDefaults = () => {
|
||||||
|
useMasquerade.mockReturnValue({
|
||||||
|
masqueradeUser: null,
|
||||||
|
setMasqueradeUser: jest.fn(),
|
||||||
|
});
|
||||||
|
useInitializeLearnerHome.mockReturnValue({
|
||||||
|
isError: false,
|
||||||
|
error: null,
|
||||||
|
isPending: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('MasqueradeBar', () => {
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
it('can masquerade', () => {
|
it('can masquerade', () => {
|
||||||
hooks.useMasqueradeBarData.mockReturnValueOnce(masqueradeMockData);
|
mockDefaults();
|
||||||
render(<MasqueradeBar />);
|
render(<IntlProvider lang="en"><MasqueradeBar /></IntlProvider>);
|
||||||
const labelStudentName = screen.getByText(messages.StudentNameInput.defaultMessage);
|
const labelStudentName = screen.getByText(messages.StudentNameInput.defaultMessage);
|
||||||
expect(labelStudentName).toBeInTheDocument();
|
expect(labelStudentName).toBeInTheDocument();
|
||||||
const submitButton = screen.getByRole('button', { name: messages.SubmitButton.defaultMessage });
|
const submitButton = screen.getByRole('button', { name: messages.SubmitButton.defaultMessage });
|
||||||
@@ -34,56 +57,106 @@ describe('MasqueradeBar', () => {
|
|||||||
});
|
});
|
||||||
it('can masquerade with input', () => {
|
it('can masquerade with input', () => {
|
||||||
const masqueradeInput = 'test';
|
const masqueradeInput = 'test';
|
||||||
hooks.useMasqueradeBarData.mockReturnValueOnce({
|
mockDefaults();
|
||||||
...masqueradeMockData,
|
useState.mockReturnValue([masqueradeInput, jest.fn()]);
|
||||||
masqueradeInput,
|
render(<IntlProvider lang="en"><MasqueradeBar /></IntlProvider>);
|
||||||
});
|
|
||||||
render(<MasqueradeBar />);
|
|
||||||
const valueMasqueradeInput = screen.getByRole('textbox', { value: masqueradeInput });
|
const valueMasqueradeInput = screen.getByRole('textbox', { value: masqueradeInput });
|
||||||
expect(valueMasqueradeInput).toBeInTheDocument();
|
expect(valueMasqueradeInput).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('cannot masquerade', () => {
|
|
||||||
hooks.useMasqueradeBarData.mockReturnValueOnce({
|
|
||||||
...masqueradeMockData,
|
|
||||||
canMasquerade: false,
|
|
||||||
});
|
|
||||||
render(<MasqueradeBar />);
|
|
||||||
const labelStudentName = screen.queryByText(messages.StudentNameInput.defaultMessage);
|
|
||||||
expect(labelStudentName).toBeNull();
|
|
||||||
});
|
|
||||||
it('is masquerading with input', () => {
|
it('is masquerading with input', () => {
|
||||||
const masqueradeInput = 'test';
|
const masqueradeInput = 'test';
|
||||||
hooks.useMasqueradeBarData.mockReturnValueOnce({
|
useInitializeLearnerHome.mockReturnValue({
|
||||||
...masqueradeMockData,
|
isError: false,
|
||||||
isMasquerading: true,
|
error: null,
|
||||||
masqueradeInput,
|
isPending: false,
|
||||||
});
|
});
|
||||||
render(<MasqueradeBar />);
|
useMasquerade.mockReturnValue({
|
||||||
|
masqueradeUser: masqueradeInput,
|
||||||
|
setMasqueradeUser: jest.fn(),
|
||||||
|
});
|
||||||
|
useState.mockReturnValue([masqueradeInput, jest.fn()]);
|
||||||
|
render(<IntlProvider lang="en"><MasqueradeBar /></IntlProvider>);
|
||||||
const chipMasqueradeInput = screen.getByText(masqueradeInput);
|
const chipMasqueradeInput = screen.getByText(masqueradeInput);
|
||||||
expect(chipMasqueradeInput).toBeInTheDocument();
|
expect(chipMasqueradeInput).toBeInTheDocument();
|
||||||
expect(chipMasqueradeInput.parentElement).toHaveClass('masquerade-chip');
|
expect(chipMasqueradeInput.parentElement).toHaveClass('masquerade-chip');
|
||||||
});
|
});
|
||||||
it('is masquerading failed with error', () => {
|
it('is masquerading failed with error', () => {
|
||||||
const masqueradeErrorMessage = 'test-error';
|
|
||||||
const masqueradeInput = 'test';
|
const masqueradeInput = 'test';
|
||||||
hooks.useMasqueradeBarData.mockReturnValueOnce({
|
useInitializeLearnerHome.mockReturnValue({
|
||||||
...masqueradeMockData,
|
isError: true,
|
||||||
isMasqueradingFailed: true,
|
error: { customAttributes: { httpErrorStatus: 404 } },
|
||||||
masqueradeErrorMessage,
|
isPending: false,
|
||||||
});
|
});
|
||||||
render(<MasqueradeBar />);
|
useMasquerade.mockReturnValue({
|
||||||
|
masqueradeUser: masqueradeInput,
|
||||||
|
setMasqueradeUser: jest.fn(),
|
||||||
|
});
|
||||||
|
useState.mockReturnValue([masqueradeInput, jest.fn()]);
|
||||||
|
render(<IntlProvider lang="en"><MasqueradeBar /></IntlProvider>);
|
||||||
const valueMasqueradeInput = screen.getByRole('textbox', { value: masqueradeInput });
|
const valueMasqueradeInput = screen.getByRole('textbox', { value: masqueradeInput });
|
||||||
expect(valueMasqueradeInput).toHaveClass('is-invalid');
|
expect(valueMasqueradeInput).toHaveClass('is-invalid');
|
||||||
});
|
});
|
||||||
it('is masquerading pending', () => {
|
it('is masquerading pending', () => {
|
||||||
hooks.useMasqueradeBarData.mockReturnValueOnce({
|
useInitializeLearnerHome.mockReturnValue({
|
||||||
...masqueradeMockData,
|
isError: false,
|
||||||
isMasqueradingPending: true,
|
error: null,
|
||||||
|
isPending: true,
|
||||||
});
|
});
|
||||||
render(<MasqueradeBar />);
|
useMasquerade.mockReturnValue({
|
||||||
|
masqueradeUser: 'test',
|
||||||
|
setMasqueradeUser: jest.fn(),
|
||||||
|
});
|
||||||
|
render(<IntlProvider lang="en"><MasqueradeBar /></IntlProvider>);
|
||||||
const pendingButton = screen.getByRole('button', { name: messages.SubmitButton.defaultMessage });
|
const pendingButton = screen.getByRole('button', { name: messages.SubmitButton.defaultMessage });
|
||||||
expect(pendingButton).toBeInTheDocument();
|
expect(pendingButton).toBeInTheDocument();
|
||||||
expect(pendingButton).toHaveAttribute('aria-disabled', 'true');
|
expect(pendingButton).toHaveAttribute('aria-disabled', 'true');
|
||||||
});
|
});
|
||||||
|
it('handle clear masquerade', () => {
|
||||||
|
const setMasqueradeUser = jest.fn();
|
||||||
|
const masqueradeInput = 'test';
|
||||||
|
useInitializeLearnerHome.mockReturnValue({
|
||||||
|
isError: false,
|
||||||
|
error: null,
|
||||||
|
isPending: false,
|
||||||
|
});
|
||||||
|
useMasquerade.mockReturnValue({
|
||||||
|
masqueradeUser: masqueradeInput,
|
||||||
|
setMasqueradeUser,
|
||||||
|
});
|
||||||
|
useState.mockReturnValue([masqueradeInput, jest.fn()]);
|
||||||
|
render(<IntlProvider lang="en"><MasqueradeBar /></IntlProvider>);
|
||||||
|
const chipMasqueradeInput = screen.getByText(masqueradeInput);
|
||||||
|
expect(chipMasqueradeInput).toBeInTheDocument();
|
||||||
|
chipMasqueradeInput.click();
|
||||||
|
expect(setMasqueradeUser).toHaveBeenCalledWith(undefined);
|
||||||
|
});
|
||||||
|
it('handle masquerade submit', () => {
|
||||||
|
const setMasqueradeUser = jest.fn();
|
||||||
|
const masqueradeInput = 'test';
|
||||||
|
useInitializeLearnerHome.mockReturnValue({
|
||||||
|
isError: false,
|
||||||
|
error: null,
|
||||||
|
isPending: false,
|
||||||
|
});
|
||||||
|
useMasquerade.mockReturnValue({
|
||||||
|
masqueradeUser: null,
|
||||||
|
setMasqueradeUser,
|
||||||
|
});
|
||||||
|
useState.mockReturnValue([masqueradeInput, jest.fn()]);
|
||||||
|
render(<IntlProvider lang="en"><MasqueradeBar /></IntlProvider>);
|
||||||
|
const submitButton = screen.getByRole('button', { name: messages.SubmitButton.defaultMessage });
|
||||||
|
expect(submitButton).toBeInTheDocument();
|
||||||
|
submitButton.click();
|
||||||
|
expect(setMasqueradeUser).toHaveBeenCalledWith(masqueradeInput);
|
||||||
|
});
|
||||||
|
it('cannot masquerade', () => {
|
||||||
|
mockDefaults();
|
||||||
|
useContext.mockReturnValue({
|
||||||
|
authenticatedUser: { administrator: false },
|
||||||
|
});
|
||||||
|
render(<IntlProvider lang="en"><MasqueradeBar /></IntlProvider>);
|
||||||
|
const labelStudentName = screen.queryByText(messages.StudentNameInput.defaultMessage);
|
||||||
|
expect(labelStudentName).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { reduxHooks } from 'hooks';
|
|
||||||
|
|
||||||
export const useProgramData = ({
|
|
||||||
cardId,
|
|
||||||
}) => ({
|
|
||||||
courseTitle: reduxHooks.useCardCourseData(cardId).title,
|
|
||||||
relatedPrograms: reduxHooks.useCardRelatedProgramsData(cardId).list,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default useProgramData;
|
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
Container, Row, Col, ModalDialog,
|
Container, Row, Col, ModalDialog,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import ProgramCard from './components/ProgramCard';
|
import ProgramCard from './components/ProgramCard';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
@@ -18,8 +18,9 @@ export const RelatedProgramsModal = ({
|
|||||||
cardId,
|
cardId,
|
||||||
}) => {
|
}) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
const courseData = useCourseData(cardId);
|
||||||
const relatedPrograms = reduxHooks.useCardRelatedProgramsData(cardId).list;
|
const courseName = courseData?.course?.courseName;
|
||||||
|
const relatedPrograms = courseData?.programs?.relatedPrograms || [];
|
||||||
return (
|
return (
|
||||||
<ModalDialog
|
<ModalDialog
|
||||||
title={formatMessage(messages.header)}
|
title={formatMessage(messages.header)}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
import RelatedProgramsModal from '.';
|
import RelatedProgramsModal from '.';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
jest.mock('./components/ProgramCard', () => jest.fn(() => <div>ProgramCard</div>));
|
jest.mock('./components/ProgramCard', () => jest.fn(() => <div>ProgramCard</div>));
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
useCourseData: jest.fn(),
|
||||||
useCardCourseData: jest.fn(),
|
|
||||||
useCardRelatedProgramsData: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
@@ -42,13 +39,19 @@ const props = {
|
|||||||
|
|
||||||
describe('RelatedProgramsModal', () => {
|
describe('RelatedProgramsModal', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
reduxHooks.useCardCourseData.mockReturnValueOnce(courseData);
|
const mockedData = {
|
||||||
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce(programData);
|
course: {
|
||||||
|
courseName: courseData.courseName,
|
||||||
|
},
|
||||||
|
programs: {
|
||||||
|
relatedPrograms: programData.list,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
useCourseData.mockReturnValueOnce(mockedData);
|
||||||
});
|
});
|
||||||
it('initializes hooks with cardId', () => {
|
it('initializes hooks with cardId', () => {
|
||||||
render(<IntlProvider locale="en"><RelatedProgramsModal {...props} /></IntlProvider>);
|
render(<IntlProvider locale="en"><RelatedProgramsModal {...props} /></IntlProvider>);
|
||||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
|
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||||
expect(reduxHooks.useCardRelatedProgramsData).toHaveBeenCalledWith(cardId);
|
|
||||||
});
|
});
|
||||||
describe('renders', () => {
|
describe('renders', () => {
|
||||||
beforeEach(() => render(<IntlProvider locale="en"><RelatedProgramsModal {...props} /></IntlProvider>));
|
beforeEach(() => render(<IntlProvider locale="en"><RelatedProgramsModal {...props} /></IntlProvider>));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
@@ -6,8 +6,9 @@ import { StrictDict } from 'utils';
|
|||||||
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
|
|
||||||
import { reduxHooks, apiHooks } from 'hooks';
|
import { useCourseData } from 'hooks';
|
||||||
|
import { useDeleteEntitlementEnrollment, useUpdateEntitlementEnrollment } from 'data/hooks';
|
||||||
|
import { useSelectSessionModal } from 'data/context';
|
||||||
import { LEAVE_OPTION } from './constants';
|
import { LEAVE_OPTION } from './constants';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import * as module from './hooks';
|
import * as module from './hooks';
|
||||||
@@ -18,18 +19,31 @@ export const state = StrictDict({
|
|||||||
|
|
||||||
export const useSelectSessionModalData = () => {
|
export const useSelectSessionModalData = () => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const { selectSessionModal, closeSelectSessionModal: closeSessionModal } = useSelectSessionModal();
|
||||||
const selectedCardId = reduxHooks.useSelectSessionModalData().cardId;
|
const selectedCardId = selectSessionModal.cardId;
|
||||||
|
const courseData = useCourseData(selectedCardId);
|
||||||
const {
|
const {
|
||||||
availableSessions,
|
availableSessions,
|
||||||
isFulfilled,
|
isFulfilled,
|
||||||
} = reduxHooks.useCardEntitlementData(selectedCardId);
|
courseTitle,
|
||||||
const { title: courseTitle } = reduxHooks.useCardCourseData(selectedCardId);
|
courseId,
|
||||||
const { courseId } = reduxHooks.useCardCourseRunData(selectedCardId) || {};
|
isEnrolled,
|
||||||
const { isEnrolled } = reduxHooks.useCardEnrollmentData(selectedCardId);
|
uuid,
|
||||||
const leaveEntitlementSession = apiHooks.useLeaveEntitlementSession(selectedCardId);
|
isRefundable,
|
||||||
const switchEntitlementEnrollment = apiHooks.useSwitchEntitlementEnrollment(selectedCardId);
|
} = useMemo(() => ({
|
||||||
const newEntitlementEnrollment = apiHooks.useNewEntitlementEnrollment(selectedCardId);
|
availableSessions: courseData?.entitlement?.availableSessions || [],
|
||||||
|
isFulfilled: courseData?.entitlement?.isFulfilled || false,
|
||||||
|
courseTitle: courseData?.course?.title || courseData?.course?.courseName || '',
|
||||||
|
courseId: courseData?.courseRun?.courseId || null,
|
||||||
|
isEnrolled: courseData?.enrollment?.isEnrolled || false,
|
||||||
|
uuid: courseData?.entitlement?.uuid || null,
|
||||||
|
isRefundable: courseData?.entitlement?.isRefundable || false,
|
||||||
|
}), [courseData]);
|
||||||
|
const { mutate: leaveEntitlement } = useDeleteEntitlementEnrollment();
|
||||||
|
const leaveEntitlementSession = () => leaveEntitlement({ uuid, isRefundable });
|
||||||
|
const { mutate: switchEntitlementMutation } = useUpdateEntitlementEnrollment();
|
||||||
|
const switchEntitlementEnrollment = (selectedId) => switchEntitlementMutation({ uuid, courseId: selectedId });
|
||||||
|
const newEntitlementEnrollment = (selectedId) => switchEntitlementMutation({ uuid, courseId: selectedId });
|
||||||
|
|
||||||
const [selectedSession, setSelectedSession] = module.state.selectedSession(courseId || null);
|
const [selectedSession, setSelectedSession] = module.state.selectedSession(courseId || null);
|
||||||
|
|
||||||
@@ -42,7 +56,6 @@ export const useSelectSessionModalData = () => {
|
|||||||
header = formatMessage(messages.selectSessionHeader, { courseTitle });
|
header = formatMessage(messages.selectSessionHeader, { courseTitle });
|
||||||
hint = formatMessage(messages.selectSessionHint);
|
hint = formatMessage(messages.selectSessionHint);
|
||||||
}
|
}
|
||||||
const closeSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(null);
|
|
||||||
|
|
||||||
const trackNewSession = track.entitlements.newSession(selectedSession);
|
const trackNewSession = track.entitlements.newSession(selectedSession);
|
||||||
const trackSwitchSession = track.entitlements.switchSession(selectedCardId, selectedSession);
|
const trackSwitchSession = track.entitlements.switchSession(selectedCardId, selectedSession);
|
||||||
|
|||||||
@@ -1,204 +0,0 @@
|
|||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import track from 'tracking';
|
|
||||||
|
|
||||||
import { MockUseState } from 'testUtils';
|
|
||||||
import { reduxHooks, apiHooks } from 'hooks';
|
|
||||||
|
|
||||||
import { LEAVE_OPTION } from './constants';
|
|
||||||
import messages from './messages';
|
|
||||||
import * as hooks from './hooks';
|
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
|
||||||
const { formatMessage } = jest.requireActual('testUtils');
|
|
||||||
return {
|
|
||||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
|
||||||
useIntl: () => ({
|
|
||||||
formatMessage,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('tracking', () => ({
|
|
||||||
entitlements: {
|
|
||||||
newSession: jest.fn(),
|
|
||||||
switchSession: jest.fn(),
|
|
||||||
leaveSession: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('hooks', () => ({
|
|
||||||
reduxHooks: {
|
|
||||||
useCardCourseData: jest.fn(),
|
|
||||||
useCardCourseRunData: jest.fn(),
|
|
||||||
useCardEnrollmentData: jest.fn(),
|
|
||||||
useCardEntitlementData: jest.fn(),
|
|
||||||
useSelectSessionModalData: jest.fn(),
|
|
||||||
useUpdateSelectSessionModalCallback: jest.fn(),
|
|
||||||
},
|
|
||||||
apiHooks: {
|
|
||||||
useSwitchEntitlementEnrollment: jest.fn((...args) => ({ switchEntitlementEnrollment: args })),
|
|
||||||
useLeaveEntitlementSession: jest.fn((...args) => ({ leaveEntitlementSession: args })),
|
|
||||||
useNewEntitlementEnrollment: jest.fn((...args) => ({ newEntitlementEnrollment: args })),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const updateSelectSessionModalCallback = jest.fn();
|
|
||||||
reduxHooks.useUpdateSelectSessionModalCallback.mockReturnValue(updateSelectSessionModalCallback);
|
|
||||||
const newEntitlementEnrollment = jest.fn();
|
|
||||||
apiHooks.useNewEntitlementEnrollment.mockReturnValue(newEntitlementEnrollment);
|
|
||||||
const switchEntitlementEnrollment = jest.fn();
|
|
||||||
apiHooks.useSwitchEntitlementEnrollment.mockReturnValue(switchEntitlementEnrollment);
|
|
||||||
const leaveEntitlementSession = jest.fn();
|
|
||||||
apiHooks.useLeaveEntitlementSession.mockReturnValue(leaveEntitlementSession);
|
|
||||||
const trackNewSession = jest.fn();
|
|
||||||
track.entitlements.newSession.mockReturnValue(trackNewSession);
|
|
||||||
const trackLeaveSession = jest.fn();
|
|
||||||
track.entitlements.leaveSession.mockReturnValue(trackLeaveSession);
|
|
||||||
const trackSwitchSession = jest.fn();
|
|
||||||
track.entitlements.switchSession.mockReturnValue(trackSwitchSession);
|
|
||||||
|
|
||||||
const state = new MockUseState(hooks);
|
|
||||||
const selectedCardId = 'test-selected-card-id';
|
|
||||||
const courseTitle = 'course-title: brown fox';
|
|
||||||
const uuid = 'test-uuid';
|
|
||||||
|
|
||||||
const entitlementData = {
|
|
||||||
availableSessions: [
|
|
||||||
{ startDate: '1/2/2000', endDate: '1/2/2020', cardId: 'session-id-1' },
|
|
||||||
{ startDate: '2/3/2000', endDate: '2/3/2020', cardId: 'session-id-2' },
|
|
||||||
{ startDate: '3/4/2000', endDate: '3/4/2020', cardId: 'session-id-3' },
|
|
||||||
],
|
|
||||||
isFullfilled: false,
|
|
||||||
uuid,
|
|
||||||
};
|
|
||||||
|
|
||||||
const testValue = 'test-value';
|
|
||||||
|
|
||||||
const courseId = 'test-course-id';
|
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
|
|
||||||
|
|
||||||
describe('SelectSessionModal hooks', () => {
|
|
||||||
let out;
|
|
||||||
|
|
||||||
describe('state values', () => {
|
|
||||||
state.testGetter(state.keys.selectedSession);
|
|
||||||
});
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
describe('useSelectSessionModalData', () => {
|
|
||||||
const runHook = ({
|
|
||||||
course = {},
|
|
||||||
courseRun = {},
|
|
||||||
enrollment = {},
|
|
||||||
entitlement = {},
|
|
||||||
selectSession = {},
|
|
||||||
}) => {
|
|
||||||
reduxHooks.useCardCourseData.mockReturnValueOnce({ title: courseTitle, ...course });
|
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ courseId, ...courseRun });
|
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false, ...enrollment });
|
|
||||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
|
|
||||||
reduxHooks.useSelectSessionModalData.mockReturnValueOnce({ cardId: selectedCardId, ...selectSession });
|
|
||||||
out = hooks.useSelectSessionModalData();
|
|
||||||
};
|
|
||||||
beforeEach(() => {
|
|
||||||
state.mock();
|
|
||||||
runHook({});
|
|
||||||
});
|
|
||||||
describe('initialization', () => {
|
|
||||||
it('loads redux data based on selected card id', () => {
|
|
||||||
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(selectedCardId);
|
|
||||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(selectedCardId);
|
|
||||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(selectedCardId);
|
|
||||||
});
|
|
||||||
it('initializes enrollment hooks with selected card id', () => {
|
|
||||||
expect(apiHooks.useLeaveEntitlementSession).toHaveBeenCalledWith(selectedCardId);
|
|
||||||
expect(apiHooks.useNewEntitlementEnrollment).toHaveBeenCalledWith(selectedCardId);
|
|
||||||
expect(apiHooks.useSwitchEntitlementEnrollment).toHaveBeenCalledWith(selectedCardId);
|
|
||||||
});
|
|
||||||
it('initializes selected session with courseId if available', () => {
|
|
||||||
state.expectInitializedWith(state.keys.selectedSession, courseId);
|
|
||||||
});
|
|
||||||
it('initializes selected session with null if courseId not available', () => {
|
|
||||||
runHook({ courseRun: { courseId: undefined } });
|
|
||||||
state.expectInitializedWith(state.keys.selectedSession, null);
|
|
||||||
});
|
|
||||||
it('initializes update callback with null', () => {
|
|
||||||
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(null);
|
|
||||||
});
|
|
||||||
it('initializes tracking methods', () => {
|
|
||||||
expect(track.entitlements.newSession).toHaveBeenCalledWith(courseId);
|
|
||||||
expect(track.entitlements.leaveSession).toHaveBeenCalledWith(selectedCardId);
|
|
||||||
expect(track.entitlements.switchSession).toHaveBeenCalledWith(selectedCardId, courseId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('output', () => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
describe('selectedSession', () => {
|
|
||||||
it('defaults to current courseId if enrolled', () => {
|
|
||||||
expect(out.selectedSession).toEqual(courseId);
|
|
||||||
});
|
|
||||||
it('defaults to null if not enrolled', () => {
|
|
||||||
runHook({ enrollment: { isEnrolled: false }, courseRun: { courseId: undefined } });
|
|
||||||
expect(out.selectedSession).toEqual(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('handleSelection', () => {
|
|
||||||
it('sets selected session with event target value', () => {
|
|
||||||
out.handleSelection({ target: { value: testValue } });
|
|
||||||
expect(state.setState.selectedSession).toHaveBeenCalledWith(testValue);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('handleSubmit', () => {
|
|
||||||
describe('if LEAVE_OPTION is selected', () => {
|
|
||||||
it('calls and tracks leaveEntitlementSession', () => {
|
|
||||||
state.mockVal(state.keys.selectedSession, LEAVE_OPTION);
|
|
||||||
runHook({});
|
|
||||||
out.handleSubmit();
|
|
||||||
expect(leaveEntitlementSession).toHaveBeenCalledWith();
|
|
||||||
expect(trackLeaveSession).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('if not enrolled in a session yet', () => {
|
|
||||||
it('calls and tracks newEntitlementEnrollment with selected card ID and session', () => {
|
|
||||||
state.mockVal(state.keys.selectedSession, testValue);
|
|
||||||
runHook({});
|
|
||||||
out.handleSubmit();
|
|
||||||
expect(newEntitlementEnrollment).toHaveBeenCalledWith(testValue);
|
|
||||||
expect(trackNewSession).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('if enrolled in a session already, selecting a new session', () => {
|
|
||||||
it('calls and tracks swtichEntitlementEnrollment w/ selected card ID and session', () => {
|
|
||||||
state.mockVal(state.keys.selectedSession, testValue);
|
|
||||||
runHook({ enrollment: { isEnrolled: true } });
|
|
||||||
out.handleSubmit();
|
|
||||||
expect(switchEntitlementEnrollment).toHaveBeenCalledWith(testValue);
|
|
||||||
expect(trackSwitchSession).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('showModal returns true if selectedCardId is not null or undefined', () => {
|
|
||||||
expect(out.showModal).toEqual(true);
|
|
||||||
runHook({ selectSession: { cardId: null } });
|
|
||||||
expect(out.showModal).toEqual(false);
|
|
||||||
runHook({ selectSession: { cardId: undefined } });
|
|
||||||
expect(out.showModal).toEqual(false);
|
|
||||||
});
|
|
||||||
test('displays change or leave header and hint if fulfilled', () => {
|
|
||||||
expect(out.header).toEqual(formatMessage(messages.selectSessionHeader, { courseTitle }));
|
|
||||||
expect(out.hint).toEqual(formatMessage(messages.selectSessionHint));
|
|
||||||
});
|
|
||||||
test('displays select session header (w/ courseTitle) and hint if unfulfilled', () => {
|
|
||||||
runHook({ entitlement: { isFulfilled: true } });
|
|
||||||
expect(out.header).toEqual(formatMessage(messages.changeOrLeaveHeader));
|
|
||||||
expect(out.hint).toEqual(formatMessage(messages.changeOrLeaveHint));
|
|
||||||
});
|
|
||||||
test('closeSessionModal returns update callback wth dispatch and null card id', () => {
|
|
||||||
expect(out.closeSessionModal()).toEqual(
|
|
||||||
reduxHooks.useUpdateSelectSessionModalCallback(null)(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user