Compare commits
109 Commits
aurora/use
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a98439635b | ||
|
|
141d4900ae | ||
|
|
f9eef109c1 | ||
|
|
ad25abc222 | ||
|
|
a4f14da17a | ||
|
|
81ce59eab7 | ||
|
|
d1bf6f9c91 | ||
|
|
5ca1e9dc1f | ||
|
|
b83f128f81 | ||
|
|
c2a20af9b8 | ||
|
|
8f2ed779ca | ||
|
|
0cedeb0809 | ||
|
|
1a51ac07a2 | ||
|
|
4b2d65c44c | ||
|
|
c44db75273 | ||
|
|
a98fd50788 | ||
|
|
fd57523b2e | ||
|
|
2cf6e5a23e | ||
|
|
3cdcc1fe61 | ||
|
|
82ff0d7ddb | ||
|
|
7375c8f27b | ||
|
|
1478956e34 | ||
|
|
0cf98c9b78 | ||
|
|
f049712430 | ||
|
|
c977de2df9 | ||
|
|
4b20c5bbdd | ||
|
|
0c1fa2f030 | ||
|
|
91117cce6a | ||
|
|
e6dba8bdc2 | ||
|
|
1d67ac5f24 | ||
|
|
60d2f22c50 | ||
|
|
5dc89d7404 | ||
|
|
0f24d3a52d | ||
|
|
fc885d02dc | ||
|
|
2e09d3632e | ||
|
|
d8cb46da60 | ||
|
|
199d6e7c60 | ||
|
|
64563d58f9 | ||
|
|
1e9a0a87b6 | ||
|
|
d42d0cdc59 | ||
|
|
8fef92d94d | ||
|
|
b41eee47c9 | ||
|
|
909f3f1f47 | ||
|
|
ce269e8c8f | ||
|
|
86a4573405 | ||
|
|
be2258e409 | ||
|
|
be8cb85773 | ||
|
|
a2c003e542 | ||
|
|
f1cfe3de68 | ||
|
|
d43c17a663 | ||
|
|
c01042f1df | ||
|
|
ed2368222f | ||
|
|
103a67654c | ||
|
|
58c3720087 | ||
|
|
4e47018a81 | ||
|
|
e7d9255fe5 | ||
|
|
2c7e10ffc2 | ||
|
|
43aa5b088e | ||
|
|
86b1f5df1a | ||
|
|
5c52b6861e | ||
|
|
a358a6014f | ||
|
|
6ebc94506b | ||
|
|
59ab63807f | ||
|
|
322a79afaa | ||
|
|
c458f4942f | ||
|
|
93a4dfb4d9 | ||
|
|
f92bd9c8f9 | ||
|
|
5db95b0029 | ||
|
|
a479b7ead6 | ||
|
|
e43a49b431 | ||
|
|
4643e0b130 | ||
|
|
8c29abd0c8 | ||
|
|
d44b123815 | ||
|
|
8829f756d8 | ||
|
|
176a803f94 | ||
|
|
309a07ffa9 | ||
|
|
e3784d36f1 | ||
|
|
5048fffd04 | ||
|
|
5ca3036849 | ||
|
|
e57f44068b | ||
|
|
a4d10b6c72 | ||
|
|
5769629250 | ||
|
|
a59ff5e7e8 | ||
|
|
9a9c0583ca | ||
|
|
2f409e5168 | ||
|
|
cf35c7d611 | ||
|
|
4a2eee2a1d | ||
|
|
0ed2b10b13 | ||
|
|
01f67265f6 | ||
|
|
8a73043368 | ||
|
|
b09c36e13e | ||
|
|
14f7389900 | ||
|
|
895e867b91 | ||
|
|
6bc60bad33 | ||
|
|
5e716ece2d | ||
|
|
320f6acc21 | ||
|
|
af51373e2c | ||
|
|
5dd00e9f24 | ||
|
|
63eaa00ee1 | ||
|
|
e25610c66e | ||
|
|
5724d051b2 | ||
|
|
9a5ac5ddf7 | ||
|
|
145c18d9ed | ||
|
|
b4bb924659 | ||
|
|
45e8113553 | ||
|
|
cfb9bfdb6b | ||
|
|
6a73054a9c | ||
|
|
5d88e8d1ec | ||
|
|
19d7aa3e33 |
8
.env
8
.env
@@ -2,6 +2,7 @@ NODE_ENV='production'
|
||||
NODE_PATH=./src
|
||||
BASE_URL=''
|
||||
LMS_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
LOGIN_URL=''
|
||||
LOGOUT_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
@@ -35,3 +36,10 @@ ZENDESK_KEY=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
ACCOUNT_PROFILE_URL=''
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
|
||||
@@ -2,6 +2,7 @@ NODE_ENV='development'
|
||||
PORT=1996
|
||||
BASE_URL='localhost:1996'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
@@ -42,3 +43,9 @@ ZENDESK_KEY=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
|
||||
@@ -2,6 +2,7 @@ NODE_ENV='test'
|
||||
PORT=1996
|
||||
BASE_URL='localhost:1996'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
@@ -41,3 +42,10 @@ ZENDESK_KEY='test-zendesk-key'
|
||||
HOTJAR_APP_ID='hot-jar-app-id'
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
|
||||
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY='SDK Key'
|
||||
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
||||
|
||||
@@ -5,6 +5,7 @@ const config = createConfig('eslint', {
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'import/no-import-module-exports': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
},
|
||||
});
|
||||
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -11,17 +11,17 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -49,9 +49,9 @@ jobs:
|
||||
server_port: 465
|
||||
username: ${{ secrets.EDX_SMTP_USERNAME }}
|
||||
password: ${{ secrets.EDX_SMTP_PASSWORD }}
|
||||
subject: Upgrade python requirements workflow failed in ${{github.repository}}
|
||||
subject: CI workflow failed in ${{github.repository}}
|
||||
to: masters-grades@edx.org
|
||||
from: github-actions <github-actions@edx.org>
|
||||
body: Upgrade python requirements workflow in ${{github.repository}} failed!
|
||||
body: CI workflow in ${{github.repository}} failed!
|
||||
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
|
||||
}}"
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
9
.github/workflows/npm-publish.yml
vendored
9
.github/workflows/npm-publish.yml
vendored
@@ -10,14 +10,17 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
30
Makefile
30
Makefile
@@ -1,18 +1,16 @@
|
||||
npm-install-%: ## install specified % npm package
|
||||
npm install $* --save-dev
|
||||
git add package.json
|
||||
export TRANSIFEX_RESOURCE = frontend-app-learner-dashboard
|
||||
transifex_langs = "ar,fr,fr_CA,es_419,pt_BR,zh_CN"
|
||||
|
||||
transifex_resource = frontend-app-learner-dashboard
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
@@ -49,15 +47,29 @@ push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
|
||||
|
||||
$(intl_imports) paragon frontend-component-footer frontend-app-learner-dashboard
|
||||
endif
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
39
README.md
39
README.md
@@ -21,6 +21,45 @@ Some guidelines for writing widgets:
|
||||
* You can load data from the redux store, but should not add or modify fields in that structure.
|
||||
* Network events should be managed in component hooks, though can use our `data/constants/requests:requestStates` for ease of tracking the request states.
|
||||
|
||||
## License
|
||||
|
||||
The code in this repository is licensed under the AGPLv3 unless otherwise
|
||||
noted.
|
||||
|
||||
Please see `LICENSE <LICENSE>`_ for details.
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you're having trouble, we have discussion forums at
|
||||
https://discuss.openedx.org where you can connect with others in the community.
|
||||
|
||||
Our real-time conversations are on Slack. You can request a `Slack
|
||||
invitation`_, then join our `community Slack workspace`_. Because this is a
|
||||
frontend repository, the best place to discuss it would be in the `#wg-frontend
|
||||
channel`_.
|
||||
|
||||
For anything non-trivial, the best path is to open an issue in this repository
|
||||
with as many details about the issue you are facing as you can provide.
|
||||
|
||||
https://github.com/openedx/frontend-app-learner-dashboard/issues
|
||||
|
||||
For more information about these options, see the `Getting Help`_ page.
|
||||
|
||||
.. _Slack invitation: https://openedx.org/slack
|
||||
.. _community Slack workspace: https://openedx.slack.com/
|
||||
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
||||
.. _Getting Help: https://openedx.org/community/connect
|
||||
|
||||
## Resources
|
||||
|
||||
* [Learner Home project info at the Open edX Wiki](https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3575906333/Learner+Home)
|
||||
|
||||
## The Open edX Code of Conduct
|
||||
|
||||
All community members are expected to follow the `Open edX Code of Conduct`_.
|
||||
|
||||
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
|
||||
41268
package-lock.json
generated
41268
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"semantic-release": "semantic-release",
|
||||
@@ -16,6 +16,7 @@
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"quality": "npm run lint-fix && npm run test",
|
||||
"watch-tests": "jest --watch",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"author": "edX",
|
||||
@@ -25,16 +26,18 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/browserslist-config": "^1.1.0",
|
||||
"@edx/frontend-component-footer": "^11.4.1",
|
||||
"@edx/frontend-enterprise-hotjar": "^1.2.0",
|
||||
"@edx/frontend-platform": "^2.6.2",
|
||||
"@edx/paragon": "20.19.0",
|
||||
"@edx/frontend-component-footer": "^12.2.1",
|
||||
"@edx/frontend-enterprise-hotjar": "^2.0.0",
|
||||
"@edx/frontend-platform": "^5.5.4",
|
||||
"@edx/paragon": "^20.44.0",
|
||||
"@edx/react-unit-test-utils": "^1.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.15",
|
||||
"@optimizely/react-sdk": "^2.9.2",
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@@ -51,17 +54,18 @@
|
||||
"history": "5.0.1",
|
||||
"html-react-parser": "^1.3.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest-when": "^3.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "7.0.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^5.20.9",
|
||||
"react-pdf": "^5.5.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "5.3.3",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-share": "^4.4.0",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.1.1",
|
||||
@@ -76,19 +80,20 @@
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "11.0.1",
|
||||
"@edx/frontend-build": "13.0.1",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"enzyme-adapter-react-16": "^1.15.6",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"fetch-mock": "^9.11.0",
|
||||
"husky": "^7.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest-expect-message": "^1.0.2",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
"react-test-renderer": "^16.14.0",
|
||||
"reactifex": "1.1.1",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"semantic-release": "^17.4.5"
|
||||
"semantic-release": "^20.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us" dir="ltr">
|
||||
<head>
|
||||
<title>Learner Dashboard | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
63
src/App.jsx
63
src/App.jsx
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -19,14 +18,17 @@ import {
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import ZendeskFab from 'components/ZendeskFab';
|
||||
import { ExperimentProvider } from 'ExperimentContext';
|
||||
|
||||
import track from 'tracking';
|
||||
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
import LearnerDashboardHeaderVariant from './containers/LearnerDashboardHeaderVariant';
|
||||
|
||||
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
import './App.scss';
|
||||
|
||||
export const App = () => {
|
||||
@@ -40,8 +42,21 @@ export const App = () => {
|
||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
||||
const loadData = reduxHooks.useLoadData();
|
||||
|
||||
const optimizelyScript = () => {
|
||||
if (getConfig().OPTIMIZELY_URL) {
|
||||
return <script src={getConfig().OPTIMIZELY_URL} />;
|
||||
} if (getConfig().OPTIMIZELY_PROJECT_ID) {
|
||||
return (
|
||||
<script
|
||||
src={`${getConfig().MARKETING_SITE_BASE_URL}/optimizelyjs/${getConfig().OPTIMIZELY_PROJECT_ID}.js`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') {
|
||||
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
|
||||
window.loadEmptyData = () => {
|
||||
loadData({ ...fakeData.globalData, courses: [] });
|
||||
};
|
||||
@@ -59,12 +74,12 @@ export const App = () => {
|
||||
window.actions = actions;
|
||||
window.track = track;
|
||||
}
|
||||
if (process.env.HOTJAR_APP_ID) {
|
||||
if (getConfig().HOTJAR_APP_ID) {
|
||||
try {
|
||||
initializeHotjar({
|
||||
hotjarId: process.env.HOTJAR_APP_ID,
|
||||
hotjarVersion: process.env.HOTJAR_VERSION,
|
||||
hotjarDebug: !!process.env.HOTJAR_DEBUG,
|
||||
hotjarId: getConfig().HOTJAR_APP_ID,
|
||||
hotjarVersion: getConfig().HOTJAR_VERSION,
|
||||
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
@@ -72,24 +87,32 @@ export const App = () => {
|
||||
}
|
||||
}, [authenticatedUser, loadData]);
|
||||
return (
|
||||
<Router>
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages.pageTitle)}</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
{optimizelyScript()}
|
||||
</Helmet>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
) : (<Dashboard />)}
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
) : (
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
)}
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<Footer logo={getConfig().LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Router>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
124
src/App.test.jsx
124
src/App.test.jsx
@@ -1,26 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import LearnerDashboardHeaderVariant from 'containers/LearnerDashboardHeaderVariant';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
||||
import { ExperimentProvider } from 'ExperimentContext';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeaderVariant', () => 'LearnerDashboardHeaderVariant');
|
||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
|
||||
jest.mock('ExperimentContext', () => ({
|
||||
ExperimentProvider: 'ExperimentProvider',
|
||||
}));
|
||||
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: 'redux.selectors',
|
||||
actions: 'redux.actions',
|
||||
@@ -35,76 +38,131 @@ jest.mock('hooks', () => ({
|
||||
}));
|
||||
jest.mock('data/store', () => 'data/store');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
const loadData = jest.fn();
|
||||
reduxHooks.useLoadData.mockReturnValue(loadData);
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
|
||||
const supportEmail = 'test-support-url';
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
||||
|
||||
describe('App router component', () => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
const { formatMessage } = useIntl();
|
||||
describe('component', () => {
|
||||
const runBasicTests = () => {
|
||||
test('snapshot', () => { expect(el).toMatchSnapshot(); });
|
||||
test('snapshot', () => { expect(el.snapshot).toMatchSnapshot(); });
|
||||
it('displays title in helmet component', () => {
|
||||
expect(el.find(Helmet).find('title').text()).toEqual(useIntl().formatMessage(messages.pageTitle));
|
||||
const control = el.instance
|
||||
.findByType(Helmet)[0]
|
||||
.findByType('title')[0];
|
||||
expect(control.children[0].el).toEqual(formatMessage(messages.pageTitle));
|
||||
});
|
||||
it('displays learner dashboard header', () => {
|
||||
expect(el.find(LearnerDashboardHeaderVariant).length).toEqual(1);
|
||||
});
|
||||
it('wraps the page in a browser router', () => {
|
||||
expect(el.find(Router)).toMatchObject(el);
|
||||
expect(el.instance.findByType(LearnerDashboardHeader).length).toEqual(1);
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(el.find(Footer).props().logo).toEqual(logo);
|
||||
expect(el.instance.findByType(Footer)[0].props.logo).toEqual(logo);
|
||||
});
|
||||
it('wraps the header and main components in an AppWrapper widget container', () => {
|
||||
const container = el.instance.findByType(AppWrapper)[0];
|
||||
expect(container.children[0].type).toEqual('LearnerDashboardHeader');
|
||||
expect(container.children[1].type).toEqual('main');
|
||||
});
|
||||
};
|
||||
describe('no network failure', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
expect(el.find('main')).toMatchObject(shallow(
|
||||
<main><Dashboard /></main>,
|
||||
));
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const expProvider = main.children[0];
|
||||
expect(expProvider.type).toEqual('ExperimentProvider');
|
||||
expect(expProvider.children.length).toEqual(1);
|
||||
expect(
|
||||
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('no network failure with optimizely url', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo, OPTIMIZELY_URL: 'fake.url' });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const expProvider = main.children[0];
|
||||
expect(expProvider.type).toEqual('ExperimentProvider');
|
||||
expect(expProvider.children.length).toEqual(1);
|
||||
expect(
|
||||
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('no network failure with optimizely project id', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo, OPTIMIZELY_PROJECT_ID: 'fakeId' });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const expProvider = main.children[0];
|
||||
expect(expProvider.type).toEqual('ExperimentProvider');
|
||||
expect(expProvider.children.length).toEqual(1);
|
||||
expect(
|
||||
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('initialize failure', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
|
||||
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads error page', () => {
|
||||
expect(el.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
</main>,
|
||||
));
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const alert = main.children[0];
|
||||
expect(alert.type).toEqual('Alert');
|
||||
expect(alert.children.length).toEqual(1);
|
||||
const errorPage = alert.children[0];
|
||||
expect(errorPage.type).toEqual('ErrorPage');
|
||||
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
|
||||
});
|
||||
});
|
||||
describe('refresh failure', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
|
||||
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads error page', () => {
|
||||
expect(el.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
</main>,
|
||||
));
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const alert = main.children[0];
|
||||
expect(alert.type).toEqual('Alert');
|
||||
expect(alert.children.length).toEqual(1);
|
||||
const errorPage = alert.children[0];
|
||||
expect(errorPage.type).toEqual('ErrorPage');
|
||||
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
64
src/ExperimentContext.jsx
Normal file
64
src/ExperimentContext.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import { StrictDict } from 'utils';
|
||||
import api from 'widgets/ProductRecommendations/api';
|
||||
import * as module from './ExperimentContext';
|
||||
|
||||
export const state = StrictDict({
|
||||
experiment: (val) => React.useState(val), // eslint-disable-line
|
||||
countryCode: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useCountryCode = (setCountryCode) => {
|
||||
React.useEffect(() => {
|
||||
api
|
||||
.fetchRecommendationsContext()
|
||||
.then((response) => {
|
||||
setCountryCode(response.data.countryCode);
|
||||
})
|
||||
.catch(() => {
|
||||
setCountryCode('');
|
||||
});
|
||||
/* eslint-disable */
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const ExperimentContext = React.createContext();
|
||||
|
||||
export const ExperimentProvider = ({ children }) => {
|
||||
const [countryCode, setCountryCode] = module.state.countryCode(null);
|
||||
const [experiment, setExperiment] = module.state.experiment({
|
||||
isExperimentActive: false,
|
||||
inRecommendationsVariant: true,
|
||||
});
|
||||
|
||||
module.useCountryCode(setCountryCode);
|
||||
const { width } = useWindowSize();
|
||||
const isMobile = width < breakpoints.small.minWidth;
|
||||
|
||||
const contextValue = React.useMemo(
|
||||
() => ({
|
||||
experiment,
|
||||
countryCode,
|
||||
setExperiment,
|
||||
setCountryCode,
|
||||
isMobile,
|
||||
}),
|
||||
[experiment, countryCode, setExperiment, setCountryCode, isMobile]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExperimentContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ExperimentContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useExperimentContext = () => React.useContext(ExperimentContext);
|
||||
|
||||
ExperimentProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default { useCountryCode, useExperimentContext };
|
||||
123
src/ExperimentContext.test.jsx
Normal file
123
src/ExperimentContext.test.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { useWindowSize } from '@edx/paragon';
|
||||
|
||||
import api from 'widgets/ProductRecommendations/api';
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import * as experiment from 'ExperimentContext';
|
||||
|
||||
const state = new MockUseState(experiment);
|
||||
|
||||
jest.unmock('react');
|
||||
jest.spyOn(React, 'useEffect').mockImplementation((cb, prereqs) => ({ useEffect: { cb, prereqs } }));
|
||||
|
||||
jest.mock('widgets/ProductRecommendations/api', () => ({
|
||||
fetchRecommendationsContext: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('experiments context', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('useCountryCode', () => {
|
||||
describe('behaviour', () => {
|
||||
describe('useEffect call', () => {
|
||||
let calls;
|
||||
let cb;
|
||||
const setCountryCode = jest.fn();
|
||||
const successfulFetch = { data: { countryCode: 'ZA' } };
|
||||
|
||||
beforeEach(() => {
|
||||
experiment.useCountryCode(setCountryCode);
|
||||
|
||||
({ calls } = React.useEffect.mock);
|
||||
[[cb]] = calls;
|
||||
});
|
||||
|
||||
it('calls useEffect once', () => {
|
||||
expect(calls.length).toEqual(1);
|
||||
});
|
||||
describe('successfull fetch', () => {
|
||||
it('sets the country code', async () => {
|
||||
let resolveFn;
|
||||
api.fetchRecommendationsContext.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveFn = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
cb();
|
||||
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
|
||||
expect(setCountryCode).not.toHaveBeenCalled();
|
||||
resolveFn(successfulFetch);
|
||||
await waitFor(() => {
|
||||
expect(setCountryCode).toHaveBeenCalledWith(successfulFetch.data.countryCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('unsuccessfull fetch', () => {
|
||||
it('sets the country code to an empty string', async () => {
|
||||
let rejectFn;
|
||||
api.fetchRecommendationsContext.mockReturnValueOnce(
|
||||
new Promise((resolve, reject) => {
|
||||
rejectFn = reject;
|
||||
}),
|
||||
);
|
||||
cb();
|
||||
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
|
||||
expect(setCountryCode).not.toHaveBeenCalled();
|
||||
rejectFn();
|
||||
await waitFor(() => {
|
||||
expect(setCountryCode).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ExperimentProvider', () => {
|
||||
const { ExperimentProvider } = experiment;
|
||||
|
||||
const TestComponent = () => {
|
||||
const {
|
||||
experiment: exp,
|
||||
setExperiment,
|
||||
countryCode,
|
||||
setCountryCode,
|
||||
isMobile,
|
||||
} = experiment.useExperimentContext();
|
||||
|
||||
expect(exp.isExperimentActive).toBeFalsy();
|
||||
expect(exp.inRecommendationsVariant).toBeTruthy();
|
||||
expect(countryCode).toBeNull();
|
||||
expect(isMobile).toBe(false);
|
||||
expect(setExperiment).toBeDefined();
|
||||
expect(setCountryCode).toBeDefined();
|
||||
|
||||
return (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
|
||||
it('allows access to child components with the context stateful values', () => {
|
||||
const countryCodeSpy = jest.spyOn(experiment, 'useCountryCode').mockImplementationOnce(() => {});
|
||||
useWindowSize.mockImplementationOnce(() => ({ width: 577, height: 943 }));
|
||||
|
||||
state.mock();
|
||||
|
||||
mount(
|
||||
<ExperimentProvider>
|
||||
<TestComponent />
|
||||
</ExperimentProvider>,
|
||||
);
|
||||
|
||||
expect(countryCodeSpy).toHaveBeenCalledWith(state.setState.countryCode);
|
||||
state.expectInitializedWith(state.keys.countryCode, null);
|
||||
state.expectInitializedWith(state.keys.experiment, { isExperimentActive: false, inRecommendationsVariant: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component component initialize failure snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
@@ -9,28 +9,34 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
<ErrorPage
|
||||
message="If you experience repeated failures, please email support at test-support-url"
|
||||
/>
|
||||
</Alert>
|
||||
</main>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
<ErrorPage
|
||||
message="If you experience repeated failures, please email support at test-support-url"
|
||||
/>
|
||||
</Alert>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`App router component component no network failure snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
@@ -38,22 +44,98 @@ exports[`App router component component no network failure snapshot 1`] = `
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
<Dashboard />
|
||||
</main>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`App router component component no network failure with optimizely project id snapshot 1`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<script
|
||||
src="undefined/optimizelyjs/fakeId.js"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`App router component component no network failure with optimizely url snapshot 1`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<script
|
||||
src="fake.url"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`App router component component refresh failure snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
@@ -61,22 +143,28 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
<ErrorPage
|
||||
message="If you experience repeated failures, please email support at test-support-url"
|
||||
/>
|
||||
</Alert>
|
||||
</main>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
<ErrorPage
|
||||
message="If you experience repeated failures, please email support at test-support-url"
|
||||
/>
|
||||
</Alert>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -1,36 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
>
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
</IntlProvider>
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
>
|
||||
<AppProvider
|
||||
store={
|
||||
Object {
|
||||
"redux": "store",
|
||||
}
|
||||
<AppProvider
|
||||
store={
|
||||
Object {
|
||||
"redux": "store",
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<PageRoute
|
||||
}
|
||||
wrapWithRouter={true}
|
||||
>
|
||||
<NoticesWrapper>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<App />
|
||||
</PageWrap>
|
||||
}
|
||||
path="/"
|
||||
>
|
||||
<App />
|
||||
</PageRoute>
|
||||
<Redirect
|
||||
to="/"
|
||||
/>
|
||||
</Switch>
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
<Route
|
||||
element={
|
||||
<Navigate
|
||||
replace={true}
|
||||
to="/"
|
||||
/>
|
||||
}
|
||||
path="*"
|
||||
/>
|
||||
</Routes>
|
||||
</NoticesWrapper>
|
||||
</AppProvider>
|
||||
`;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
|
||||
export const EmailLink = ({ address }) => {
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MailtoLink to={address}>{address}</MailtoLink>
|
||||
);
|
||||
};
|
||||
EmailLink.defaultProps = { address: null };
|
||||
EmailLink.propTypes = { address: PropTypes.string };
|
||||
|
||||
export default EmailLink;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import EmailLink from './EmailLink';
|
||||
|
||||
describe('EmailLink', () => {
|
||||
it('renders null when no address is provided', () => {
|
||||
const wrapper = shallow(<EmailLink />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
it('renders a MailtoLink when an address is provided', () => {
|
||||
const wrapper = shallow(<EmailLink address="test@email.com" />);
|
||||
expect(wrapper.find('MailtoLink').length).toEqual(1);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
26
src/components/NoticesWrapper/api.js
Normal file
26
src/components/NoticesWrapper/api.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
|
||||
export const error404Message = 'This probably happened because the notices plugin is not installed on platform.';
|
||||
|
||||
export const getNotices = ({ onLoad }) => {
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
|
||||
const handleError = async (e) => {
|
||||
// Error probably means that notices is not installed, which is fine.
|
||||
const { customAttributes: { httpErrorStatus } } = e;
|
||||
if (httpErrorStatus === 404) {
|
||||
logInfo(`${e}. ${error404Message}`);
|
||||
} else {
|
||||
logError(e);
|
||||
}
|
||||
};
|
||||
if (authenticatedUser) {
|
||||
return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default { getNotices };
|
||||
65
src/components/NoticesWrapper/api.test.js
Normal file
65
src/components/NoticesWrapper/api.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({
|
||||
LMS_BASE_URL: 'test-lms-url',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
getAuthenticatedUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
logError: jest.fn(),
|
||||
logInfo: jest.fn(),
|
||||
}));
|
||||
|
||||
const testData = 'test-data';
|
||||
const successfulGet = () => Promise.resolve(testData);
|
||||
const error404 = { customAttributes: { httpErrorStatus: 404 }, test: 'error' };
|
||||
const error404Get = () => Promise.reject(error404);
|
||||
const error500 = { customAttributes: { httpErrorStatus: 500 }, test: 'error' };
|
||||
const error500Get = () => Promise.reject(error500);
|
||||
|
||||
const get = jest.fn().mockImplementation(successfulGet);
|
||||
getAuthenticatedHttpClient.mockReturnValue({ get });
|
||||
const authenticatedUser = { fake: 'user' };
|
||||
getAuthenticatedUser.mockReturnValue(authenticatedUser);
|
||||
|
||||
const onLoad = jest.fn();
|
||||
describe('getNotices api method', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('not authenticated', () => {
|
||||
it('does not fetch anything', () => {
|
||||
getAuthenticatedUser.mockReturnValueOnce(null);
|
||||
api.getNotices({ onLoad });
|
||||
expect(get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('authenticated', () => {
|
||||
it('fetches noticesUrl with onLoad behavior', async () => {
|
||||
await api.getNotices({ onLoad });
|
||||
expect(get).toHaveBeenCalledWith(api.noticesUrl, {});
|
||||
expect(onLoad).toHaveBeenCalledWith(testData);
|
||||
});
|
||||
it('calls logInfo if fetch fails with 404', async () => {
|
||||
get.mockImplementation(error404Get);
|
||||
await api.getNotices({ onLoad });
|
||||
expect(logInfo).toHaveBeenCalledWith(`${error404}. ${api.error404Message}`);
|
||||
});
|
||||
it('calls logError if fetch fails with non-404 error', async () => {
|
||||
get.mockImplementation(error500Get);
|
||||
await api.getNotices({ onLoad });
|
||||
expect(logError).toHaveBeenCalledWith(error500);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
35
src/components/NoticesWrapper/hooks.js
Normal file
35
src/components/NoticesWrapper/hooks.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { getNotices } from './api';
|
||||
import * as module from './hooks';
|
||||
|
||||
/**
|
||||
* This component uses the platform-plugin-notices plugin to function.
|
||||
* If the user has an unacknowledged notice, they will be rerouted off
|
||||
* course home and onto a full-screen notice page. If the plugin is not
|
||||
* installed, or there are no notices, we just passthrough this component.
|
||||
*/
|
||||
export const state = StrictDict({
|
||||
isRedirected: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useNoticesWrapperData = () => {
|
||||
const [isRedirected, setIsRedirected] = module.state.isRedirected();
|
||||
React.useEffect(() => {
|
||||
if (getConfig().ENABLE_NOTICES) {
|
||||
getNotices({
|
||||
onLoad: (data) => {
|
||||
if (data?.data?.results?.length > 0) {
|
||||
setIsRedirected(true);
|
||||
window.location.replace(`${data.data.results[0]}?next=${window.location.href}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [setIsRedirected]);
|
||||
return { isRedirected };
|
||||
};
|
||||
|
||||
export default useNoticesWrapperData;
|
||||
83
src/components/NoticesWrapper/hooks.test.js
Normal file
83
src/components/NoticesWrapper/hooks.test.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getNotices } from './api';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
|
||||
jest.mock('./api', () => ({ getNotices: jest.fn() }));
|
||||
|
||||
getConfig.mockReturnValue({ ENABLE_NOTICES: true });
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
let hook;
|
||||
describe('NoticesWrapper hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.isRedirected);
|
||||
});
|
||||
describe('useNoticesWrapperData', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes state hooks', () => {
|
||||
hooks.useNoticesWrapperData();
|
||||
expect(hooks.state.isRedirected).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('effects', () => {
|
||||
it('does not call notices if not enabled', () => {
|
||||
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
|
||||
hooks.useNoticesWrapperData();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
cb();
|
||||
expect(getNotices).not.toHaveBeenCalled();
|
||||
});
|
||||
describe('getNotices call (if enabled) onLoad behavior', () => {
|
||||
it('does not redirect if there are no results', () => {
|
||||
hooks.useNoticesWrapperData();
|
||||
expect(React.useEffect).toHaveBeenCalled();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
cb();
|
||||
expect(getNotices).toHaveBeenCalled();
|
||||
const { onLoad } = getNotices.mock.calls[0][0];
|
||||
onLoad({});
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
onLoad({ data: {} });
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
onLoad({ data: { results: [] } });
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
});
|
||||
it('redirects and set isRedirected if results are returned', () => {
|
||||
delete window.location;
|
||||
window.location = { replace: jest.fn(), href: 'test-old-href' };
|
||||
hooks.useNoticesWrapperData();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
cb();
|
||||
expect(getNotices).toHaveBeenCalled();
|
||||
const { onLoad } = getNotices.mock.calls[0][0];
|
||||
const target = 'url-target';
|
||||
onLoad({ data: { results: [target] } });
|
||||
expect(state.setState.isRedirected).toHaveBeenCalledWith(true);
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`${target}?next=${window.location.href}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards isRedirected from state call', () => {
|
||||
hook = hooks.useNoticesWrapperData();
|
||||
expect(hook.isRedirected).toEqual(state.stateVals.isRedirected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
25
src/components/NoticesWrapper/index.jsx
Normal file
25
src/components/NoticesWrapper/index.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
|
||||
/**
|
||||
* This component uses the platform-plugin-notices plugin to function.
|
||||
* If the user has an unacknowledged notice, they will be rerouted off
|
||||
* course home and onto a full-screen notice page. If the plugin is not
|
||||
* installed, or there are no notices, we just passthrough this component.
|
||||
*/
|
||||
const NoticesWrapper = ({ children }) => {
|
||||
const { isRedirected } = useNoticesWrapperData();
|
||||
return (
|
||||
<div>
|
||||
{isRedirected === true ? null : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoticesWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default NoticesWrapper;
|
||||
34
src/components/NoticesWrapper/index.test.jsx
Normal file
34
src/components/NoticesWrapper/index.test.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
import NoticesWrapper from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = { isRedirected: false };
|
||||
useNoticesWrapperData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
const children = [<b key={1}>some</b>, <i key={2}>children</i>];
|
||||
describe('NoticesWrapper component', () => {
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(useNoticesWrapperData).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('does not show children if redirected', () => {
|
||||
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
|
||||
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(el.children().length).toEqual(0);
|
||||
});
|
||||
it('shows children if not redirected', () => {
|
||||
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(el.children().length).toEqual(2);
|
||||
expect(el.children().at(0).matchesElement(children[0])).toEqual(true);
|
||||
expect(el.children().at(1).matchesElement(children[1])).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,17 @@ exports[`ZendeskFab snapshot 1`] = `
|
||||
},
|
||||
},
|
||||
"chat": Object {
|
||||
"departments": Object {
|
||||
"enabled": Array [
|
||||
"account settings",
|
||||
"billing and payments",
|
||||
"certificates",
|
||||
"deadlines",
|
||||
"errors and technical issues",
|
||||
"other",
|
||||
"proctoring",
|
||||
],
|
||||
},
|
||||
"suppress": false,
|
||||
},
|
||||
"contactForm": Object {
|
||||
|
||||
@@ -16,6 +16,9 @@ const ZendeskFab = () => {
|
||||
},
|
||||
chat: {
|
||||
suppress: false,
|
||||
departments: {
|
||||
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
|
||||
},
|
||||
},
|
||||
contactForm: {
|
||||
ticketForms: [
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmailLink renders a MailtoLink when an address is provided 1`] = `
|
||||
<MailtoLink
|
||||
to="test@email.com"
|
||||
>
|
||||
test@email.com
|
||||
</MailtoLink>
|
||||
`;
|
||||
|
||||
exports[`EmailLink renders null when no address is provided 1`] = `""`;
|
||||
@@ -1,6 +1,7 @@
|
||||
const configuration = {
|
||||
// BASE_URL: process.env.BASE_URL,
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
|
||||
// LOGIN_URL: process.env.LOGIN_URL,
|
||||
// LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
|
||||
@@ -13,6 +14,10 @@ const configuration = {
|
||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
|
||||
ZENDESK_KEY: process.env.ZENDESK_KEY,
|
||||
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
||||
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
||||
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
@@ -5,22 +5,24 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const BeginCourseButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||
const { disableBeginCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
homeUrl + execEdTrackingParam,
|
||||
);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={isMasquerading || !hasAccess}
|
||||
disabled={disableBeginCourse}
|
||||
as="a"
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
@@ -13,19 +14,22 @@ jest.mock('tracking', () => ({
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'home-url' })),
|
||||
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
let wrapper;
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData();
|
||||
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, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
);
|
||||
|
||||
describe('BeginCourseButton', () => {
|
||||
const props = {
|
||||
@@ -34,37 +38,49 @@ describe('BeginCourseButton', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('renders default button when learner has access to the course', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data with cardId', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('initializes enrollment data with cardId', () => {
|
||||
it('loads exec education path param', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('learner does not have access', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
it('loads disabled states for begin action from action hooks', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
describe('disabled', () => {
|
||||
beforeEach(() => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should be disabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
});
|
||||
describe('enabled', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should be enabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
it('should track enter course clicked event on click, with exec ed param', () => {
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
homeUrl + execEdPath(props.cardId),
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,22 +5,24 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const ResumeButton = ({ cardId }) => {
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess, isAudit, isAuditAccessExpired } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||
const { disableResumeCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
resumeUrl,
|
||||
resumeUrl + execEdTrackingParam,
|
||||
);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired)}
|
||||
disabled={disableResumeCourse}
|
||||
as="a"
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -3,80 +3,82 @@ import { shallow } from 'enzyme';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ResumeButton from './ResumeButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ resumeUrl: 'resumeUrl' })),
|
||||
useCardEnrollmentData: jest.fn(() => ({
|
||||
hasAccess: true,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: false,
|
||||
})),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useTrackCourseEvent: (eventName, cardId, url) => jest
|
||||
.fn()
|
||||
.mockName(`useTrackCourseEvent('${eventName}', '${cardId}', '${url}')`),
|
||||
},
|
||||
}));
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: 'enterCourseClicked',
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData();
|
||||
const resumeUrl = 'resume-url';
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
|
||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
);
|
||||
|
||||
let wrapper;
|
||||
|
||||
describe('ResumeButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('renders default button when learner has access to the course', () => {
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick).getMockName()).toContain(
|
||||
'useTrackCourseEvent',
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
resumeUrl,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data based on cardId', () => {
|
||||
shallow(<ResumeButton {...props} />);
|
||||
it('initializes course run data with cardId', () => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('initializes course enrollment data based on cardId', () => {
|
||||
shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
|
||||
it('loads exec education path param', () => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
it('loads disabled states for resume action from action hooks', () => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
describe('disabled', () => {
|
||||
beforeEach(() => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should be disabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner does not have access', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
hasAccess: false,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: false,
|
||||
});
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
describe('enabled', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
});
|
||||
test('audit access expired', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
hasAccess: true,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: true,
|
||||
});
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should be enabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
it('should track enter course clicked event on click, with exec ed param', () => {
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
resumeUrl + execEdPath(props.cardId),
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,18 +4,17 @@ import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const SelectSessionButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { canChange, hasSessions } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const { disableSelectSession } = useActionDisabledState(cardId);
|
||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={isMasquerading || !hasAccess || (!canChange || !hasSessions)}
|
||||
disabled={disableSelectSession}
|
||||
onClick={openSessionModal}
|
||||
>
|
||||
{formatMessage(messages.selectSession)}
|
||||
|
||||
@@ -2,66 +2,34 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
|
||||
useCardEntitlementData: jest.fn(() => ({ canChange: true, hasSessions: true })),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useUpdateSelectSessionModalCallback: () => jest.fn().mockName('mockOpenSessionModal'),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
let wrapper;
|
||||
|
||||
describe('SelectSessionButton', () => {
|
||||
const props = { cardId: 'cardId' };
|
||||
describe('snapshot', () => {
|
||||
test('renders default button', () => {
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('renders disabled button when user does not have access to the course', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('renders disabled button if masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('default render', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick).getMockName()).toEqual(
|
||||
reduxHooks.useUpdateSelectSessionModalCallback().getMockName(),
|
||||
);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('default render', () => {
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick).getMockName())
|
||||
.toEqual(reduxHooks.useUpdateSelectSessionModalCallback().getMockName());
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('learner does not have access', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner cannot change sessions', () => {
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ canChange: false, hasSessions: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('entitlement does not have available sessions', () => {
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ canChange: true, hasSessions: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('user is masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
test('disabled states', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true });
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
@@ -14,15 +15,14 @@ export const UpgradeButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { canUpgrade } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { disableUpgradeCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.upgradeClicked,
|
||||
cardId,
|
||||
upgradeUrl,
|
||||
);
|
||||
|
||||
const isEnabled = (!isMasquerading && canUpgrade);
|
||||
const enabledProps = {
|
||||
as: 'a',
|
||||
href: upgradeUrl,
|
||||
@@ -32,8 +32,8 @@ export const UpgradeButton = ({ cardId }) => {
|
||||
<ActionButton
|
||||
iconBefore={Locked}
|
||||
variant="outline-primary"
|
||||
disabled={!isEnabled}
|
||||
{...isEnabled && enabledProps}
|
||||
disabled={disableUpgradeCourse}
|
||||
{...!disableUpgradeCourse && enabledProps}
|
||||
>
|
||||
{formatMessage(messages.upgrade)}
|
||||
</ActionButton>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
@@ -13,15 +14,13 @@ jest.mock('tracking', () => ({
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableUpgradeCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
describe('UpgradeButton', () => {
|
||||
@@ -42,13 +41,7 @@ describe('UpgradeButton', () => {
|
||||
));
|
||||
});
|
||||
test('cannot upgrade', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
|
||||
@@ -5,23 +5,23 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const ViewCourseButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess, isAudit, isAuditAccessExpired } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { disableViewCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
);
|
||||
// disabled on no access or (is audit track but audit access was expired)
|
||||
const disabledViewCourseButton = !hasAccess || (isAudit && isAuditAccessExpired);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={disabledViewCourseButton}
|
||||
disabled={disableViewCourse}
|
||||
as="a"
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
import track from 'tracking';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
@@ -13,73 +14,33 @@ jest.mock('tracking', () => ({
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
let wrapper;
|
||||
const defaultProps = { cardId: 'cardId' };
|
||||
const homeUrl = 'homeUrl';
|
||||
|
||||
const createWrapper = ({
|
||||
hasAccess = false,
|
||||
isAudit = false,
|
||||
isAuditAccessExpired = false,
|
||||
isEntitlement = false,
|
||||
isExpired = false,
|
||||
propsOveride = {},
|
||||
}) => {
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess, isAudit, isAuditAccessExpired });
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
|
||||
return shallow(<ViewCourseButton {...defaultProps} {...propsOveride} />);
|
||||
};
|
||||
|
||||
describe('ViewCourseButton', () => {
|
||||
describe('learner has access to course', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ hasAccess: true });
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('links to home URL', () => {
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
defaultProps.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
});
|
||||
test('link is enabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
test('link is disabled when audit access is expired', () => {
|
||||
wrapper = createWrapper({ hasAccess: true, isAudit: true, isAuditAccessExpired: true });
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner can view course', () => {
|
||||
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
defaultProps.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
describe('learner does not have access to course', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ hasAccess: false });
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('links to home URL', () => {
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
defaultProps.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
});
|
||||
test('link is disabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner cannot view course', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
|
||||
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BeginCourseButton snapshot renders default button when learner has access to the course 1`] = `
|
||||
exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Begin Course
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
@@ -10,7 +29,7 @@ exports[`BeginCourseButton snapshot renders default button when learner has acce
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "home-url",
|
||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,38 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResumeButton snapshot renders default button when learner has access to the course 1`] = `
|
||||
exports[`ResumeButton snapshot disabled snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={[MockFunction useTrackCourseEvent('enterCourseClicked', 'cardId', 'resumeUrl')]}
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Resume
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`ResumeButton snapshot enabled snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Resume
|
||||
</ActionButton>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectSessionButton snapshot renders default button 1`] = `
|
||||
exports[`SelectSessionButton default render 1`] = `undefined`;
|
||||
|
||||
exports[`SelectSessionButton disabled states 1`] = `
|
||||
<ActionButton
|
||||
disabled={false}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
@@ -8,21 +10,3 @@ exports[`SelectSessionButton snapshot renders default button 1`] = `
|
||||
Select Session
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionButton snapshot renders disabled button if masquerading 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Select Session
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionButton snapshot renders disabled button when user does not have access to the course 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Select Session
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
@@ -30,13 +30,3 @@ exports[`UpgradeButton snapshot cannot upgrade 1`] = `
|
||||
Upgrade
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`UpgradeButton snapshot masquerading 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ViewCourseButton learner does not have access to course snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
View Course
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`ViewCourseButton learner has access to course snapshot 1`] = `
|
||||
exports[`ViewCourseButton learner can view course 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
@@ -37,3 +18,22 @@ exports[`ViewCourseButton learner has access to course snapshot 1`] = `
|
||||
View Course
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`ViewCourseButton learner cannot view course 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
View Course
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardActions snapshot show begin course button when verified and not entitlement and has started 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<BeginCourseButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show resume button when verified and not entitlement and has started 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<ResumeButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show select session button when not verified and entitlement 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<SelectSessionButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show upgrade button when not verified and not entitlement 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<UpgradeButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
<BeginCourseButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show view course button when not verified and entitlement and fulfilled 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<ViewCourseButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
@@ -13,21 +13,27 @@ import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
export const CourseCardActions = ({ cardId }) => {
|
||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const { isVerified, hasStarted } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const {
|
||||
isVerified,
|
||||
hasStarted,
|
||||
isExecEd2UCourse,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
||||
let PrimaryButton;
|
||||
if (isEntitlement) {
|
||||
PrimaryButton = isFulfilled ? ViewCourseButton : SelectSessionButton;
|
||||
} else if (isArchived) {
|
||||
PrimaryButton = ViewCourseButton;
|
||||
} else {
|
||||
PrimaryButton = hasStarted ? ResumeButton : BeginCourseButton;
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionRow data-test-id="CourseCardActions">
|
||||
{!(isEntitlement || isVerified) && <UpgradeButton cardId={cardId} />}
|
||||
<PrimaryButton cardId={cardId} />
|
||||
{!(isEntitlement || isVerified || isExecEd2UCourse) && <UpgradeButton cardId={cardId} />}
|
||||
{isEntitlement && (isFulfilled
|
||||
? <ViewCourseButton cardId={cardId} />
|
||||
: <SelectSessionButton cardId={cardId} />
|
||||
)}
|
||||
{(isArchived && !isEntitlement) && (
|
||||
<ViewCourseButton cardId={cardId} />
|
||||
)}
|
||||
{!(isArchived || isEntitlement) && (hasStarted
|
||||
? <ResumeButton cardId={cardId} />
|
||||
: <BeginCourseButton cardId={cardId} />
|
||||
)}
|
||||
</ActionRow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
import CourseCardActions from '.';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
@@ -9,6 +15,7 @@ jest.mock('hooks', () => ({
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -18,87 +25,92 @@ jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
|
||||
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
|
||||
jest.mock('./ResumeButton', () => 'ResumeButton');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const props = { cardId };
|
||||
|
||||
let el;
|
||||
describe('CourseCardActions', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
const createWrapper = ({
|
||||
isEntitlement, isFulfilled, isArchived, isVerified, hasStarted,
|
||||
}) => {
|
||||
const mockHooks = ({
|
||||
isEntitlement = false,
|
||||
isExecEd2UCourse = false,
|
||||
isFulfilled = false,
|
||||
isArchived = false,
|
||||
isVerified = false,
|
||||
hasStarted = false,
|
||||
isMasquerading = false,
|
||||
} = {}) => {
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isVerified, hasStarted });
|
||||
return shallow(<CourseCardActions {...props} />);
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted });
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('show upgrade button when not verified and not entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show select session button when not verified and entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show begin course button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show resume button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: true,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show view course button when not verified and entitlement and fulfilled', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: true, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const render = () => {
|
||||
el = shallow(<CourseCardActions {...props} />);
|
||||
};
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
mockHooks();
|
||||
render();
|
||||
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('show upgrade button when not verified and not entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
describe('output', () => {
|
||||
describe('Exec Ed course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isExecEd2UCourse: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
});
|
||||
expect(wrapper.find('UpgradeButton')).toHaveLength(1);
|
||||
});
|
||||
it('show select session button when not verified and entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
describe('entitlement course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isEntitlement: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
});
|
||||
it('renders ViewCourseButton if fulfilled', () => {
|
||||
mockHooks({ isEntitlement: true, isFulfilled: true });
|
||||
render();
|
||||
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
it('renders SelectSessionButton if not fulfilled', () => {
|
||||
mockHooks({ isEntitlement: true });
|
||||
render();
|
||||
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
expect(wrapper.find('SelectSessionButton')).toHaveLength(1);
|
||||
});
|
||||
it('show begin course button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: false,
|
||||
describe('verified course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isVerified: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
});
|
||||
expect(wrapper.find('BeginCourseButton')).toHaveLength(1);
|
||||
});
|
||||
it('show resume button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: true,
|
||||
describe('not entielement, verified, or exec ed', () => {
|
||||
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
|
||||
mockHooks({ isArchived: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
expect(wrapper.find('ResumeButton')).toHaveLength(1);
|
||||
});
|
||||
it('show view course button when not verified and entitlement and fulfilled', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: true, isArchived: false, isVerified: false, hasStarted: false,
|
||||
describe('unstarted courses', () => {
|
||||
it('renders UpgradeButton and BeginCourseButton', () => {
|
||||
mockHooks();
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
expect(wrapper.find('ViewCourseButton')).toHaveLength(1);
|
||||
});
|
||||
it('show view course button when not verified and entitlement and fulfilled and archived', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: true, isArchived: true, isVerified: false, hasStarted: false,
|
||||
describe('active courses (started, and not archived)', () => {
|
||||
it('renders UpgradeButton and ResumeButton', () => {
|
||||
mockHooks({ hasStarted: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(ResumeButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
expect(wrapper.find('ViewCourseButton')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,16 +26,28 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
const emailLink = address => address && <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
const emailLink = address => <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
|
||||
if (certificate.isRestricted) {
|
||||
return (
|
||||
<Banner variant="danger">
|
||||
{formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) })}
|
||||
{ supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
|
||||
{isVerified && ' '}
|
||||
{isVerified && formatMessage(
|
||||
messages.certRefundContactBilling,
|
||||
{ billingEmail: emailLink(billingEmail) },
|
||||
{isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isDownloadable) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
{formatMessage(messages.certReady)}
|
||||
{certificate.certPreviewUrl && (
|
||||
<>
|
||||
{' '}
|
||||
<Hyperlink isInline destination={certificate.certPreviewUrl}>
|
||||
{formatMessage(messages.viewCertificate)}
|
||||
</Hyperlink>
|
||||
</>
|
||||
)}
|
||||
</Banner>
|
||||
);
|
||||
@@ -63,17 +75,6 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isDownloadable) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
{formatMessage(messages.certReady)}
|
||||
{' '}
|
||||
<Hyperlink isInline destination={certificate.certPreviewUrl}>
|
||||
{formatMessage(messages.viewCertificate)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isEarnedButUnavailable) {
|
||||
return (
|
||||
<Banner>
|
||||
|
||||
@@ -21,10 +21,6 @@ jest.mock('components/Banner', () => 'Banner');
|
||||
|
||||
describe('CertificateBanner', () => {
|
||||
const props = { cardId: 'cardId' };
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValue({
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
});
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({
|
||||
minPassingGrade: 0.8,
|
||||
progressUrl: 'progressUrl',
|
||||
@@ -42,18 +38,22 @@ describe('CertificateBanner', () => {
|
||||
};
|
||||
const defaultCourseRun = { isArchived: false };
|
||||
const defaultGrade = { isPassing: false };
|
||||
const defaultPlatformSettings = {};
|
||||
const createWrapper = ({
|
||||
certificate = {},
|
||||
enrollment = {},
|
||||
grade = {},
|
||||
courseRun = {},
|
||||
platformSettings = {},
|
||||
}) => {
|
||||
reduxHooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
|
||||
reduxHooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings });
|
||||
return shallow(<CertificateBanner {...props} />);
|
||||
};
|
||||
/** TODO: Update tests to validate snapshots **/
|
||||
describe('snapshot', () => {
|
||||
test('is restricted', () => {
|
||||
const wrapper = createWrapper({
|
||||
@@ -63,6 +63,28 @@ describe('CertificateBanner', () => {
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted with support email', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
platformSettings: {
|
||||
supportEmail: 'suport@email',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted with billing email', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
platformSettings: {
|
||||
billingEmail: 'billing@email',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted and verified', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
@@ -74,6 +96,63 @@ describe('CertificateBanner', () => {
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted and verified with support email', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
enrollment: {
|
||||
isVerified: true,
|
||||
},
|
||||
platformSettings: {
|
||||
supportEmail: 'suport@email',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted and verified with billing email', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
enrollment: {
|
||||
isVerified: true,
|
||||
},
|
||||
platformSettings: {
|
||||
billingEmail: 'billing@email',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted and verified with support and billing email', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
enrollment: {
|
||||
isVerified: true,
|
||||
},
|
||||
platformSettings: {
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is passing and is downloadable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: { isPassing: true },
|
||||
certificate: { isDownloadable: true },
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('not passing and is downloadable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: { isPassing: false },
|
||||
certificate: { isDownloadable: true },
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('not passing and audit', () => {
|
||||
const wrapper = createWrapper({
|
||||
enrollment: {
|
||||
@@ -92,17 +171,6 @@ describe('CertificateBanner', () => {
|
||||
const wrapper = createWrapper({});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is passing and is downloadable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: {
|
||||
isPassing: true,
|
||||
},
|
||||
certificate: {
|
||||
isDownloadable: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is passing and is earned but unavailable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: {
|
||||
@@ -129,6 +197,10 @@ describe('CertificateBanner', () => {
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
platformSettings: {
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
},
|
||||
});
|
||||
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
|
||||
expect(bannerMessage).toEqual(messages.certRestricted.defaultMessage);
|
||||
@@ -142,6 +214,10 @@ describe('CertificateBanner', () => {
|
||||
enrollment: {
|
||||
isVerified: true,
|
||||
},
|
||||
platformSettings: {
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
},
|
||||
});
|
||||
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
|
||||
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
|
||||
|
||||
@@ -36,11 +36,9 @@ export const CourseBanner = ({ cardId }) => {
|
||||
<Banner>
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
{
|
||||
<Hyperlink isInline destination="">
|
||||
{formatMessage(messages.findAnotherCourse)}
|
||||
</Hyperlink>
|
||||
}
|
||||
<Hyperlink isInline destination="">
|
||||
{formatMessage(messages.findAnotherCourse)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
))}
|
||||
|
||||
|
||||
@@ -17,9 +17,11 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmailLink": <EmailLink
|
||||
address="test-support-email"
|
||||
/>,
|
||||
"supportEmailLink": <MailtoLink
|
||||
to="test-support-email"
|
||||
>
|
||||
test-support-email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -30,6 +32,21 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CreditBanner component render with error state with no email snapshot 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
<p
|
||||
className="credit-error-msg"
|
||||
>
|
||||
An error occurred with this transaction.
|
||||
</p>
|
||||
<ContentComponent
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CreditBanner component render with no error state snapshot 1`] = `
|
||||
<Banner>
|
||||
<ContentComponent
|
||||
|
||||
@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
import EmailLink from 'components/EmailLink';
|
||||
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
import hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -15,13 +15,14 @@ export const CreditBanner = ({ cardId }) => {
|
||||
if (hookData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ContentComponent, error, supportEmail } = hookData;
|
||||
const supportEmailLink = (<EmailLink address={supportEmail} />);
|
||||
const supportEmailLink = (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>);
|
||||
return (
|
||||
<Banner {...(error && { variant: 'danger' })}>
|
||||
{error && (
|
||||
<p className="credit-error-msg">
|
||||
{formatMessage(messages.error, { supportEmailLink })}
|
||||
{supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)}
|
||||
</p>
|
||||
)}
|
||||
<ContentComponent cardId={cardId} />
|
||||
|
||||
@@ -2,15 +2,13 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import EmailLink from 'components/EmailLink';
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
|
||||
import hooks from './hooks';
|
||||
import messages from './messages';
|
||||
import CreditBanner from '.';
|
||||
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
jest.mock('components/EmailLink', () => 'EmailLink');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useCreditBannerData: jest.fn(),
|
||||
@@ -54,7 +52,7 @@ describe('CreditBanner component', () => {
|
||||
it('includes credit-error-msg with support email link', () => {
|
||||
expect(el.find('.credit-error-msg').containsMatchingElement(
|
||||
formatMessage(messages.error, {
|
||||
supportEmailLink: (<EmailLink address={supportEmail} />),
|
||||
supportEmailLink: (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>),
|
||||
}),
|
||||
)).toEqual(true);
|
||||
});
|
||||
@@ -62,6 +60,25 @@ describe('CreditBanner component', () => {
|
||||
expect(el.find('ContentComponent').props().cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with error state with no email', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useCreditBannerData.mockReturnValue({
|
||||
error: true,
|
||||
ContentComponent,
|
||||
});
|
||||
el = shallow(<CreditBanner cardId={cardId} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('includes credit-error-msg without support email link', () => {
|
||||
expect(el.find('.credit-error-msg').containsMatchingElement(
|
||||
formatMessage(messages.errorNoEmail),
|
||||
)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no error state', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useCreditBannerData.mockReturnValue({
|
||||
|
||||
@@ -6,6 +6,11 @@ export const messages = StrictDict({
|
||||
description: '',
|
||||
defaultMessage: 'An error occurred with this transaction. For help, contact {supportEmailLink}.',
|
||||
},
|
||||
errorNoEmail: {
|
||||
id: 'learner-dash.courseCard.banners.credit.errorNoEmail',
|
||||
description: '',
|
||||
defaultMessage: 'An error occurred with this transaction.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -11,10 +11,11 @@ import messages from './messages';
|
||||
|
||||
export const ApprovedContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{ href, message: formatMessage(messages.viewCredit) }}
|
||||
action={{ href, message: formatMessage(messages.viewCredit), disabled: isMasquerading }}
|
||||
message={formatMessage(
|
||||
messages.approved,
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ import ApprovedContent from './ApprovedContent';
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
@@ -22,6 +23,7 @@ const credit = {
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
describe('ApprovedContent component', () => {
|
||||
beforeEach(() => {
|
||||
@@ -44,6 +46,9 @@ describe('ApprovedContent component', () => {
|
||||
test('action.message is formatted viewCredit message', () => {
|
||||
expect(component.props().action.message).toEqual(formatMessage(messages.viewCredit));
|
||||
});
|
||||
test('action.disabled is false', () => {
|
||||
expect(component.props().action.disabled).toEqual(false);
|
||||
});
|
||||
test('message is formatted approved message', () => {
|
||||
expect(component.props().message).toEqual(formatMessage(
|
||||
messages.approved,
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import hooks from './hooks';
|
||||
@@ -12,11 +13,13 @@ import messages from './messages';
|
||||
export const MustRequestContent = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
message: formatMessage(messages.requestCredit),
|
||||
onClick: createCreditRequest,
|
||||
disabled: isMasquerading,
|
||||
}}
|
||||
message={formatMessage(messages.mustRequest, {
|
||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import messages from './messages';
|
||||
import hooks from './hooks';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
@@ -11,6 +12,9 @@ import MustRequestContent from './MustRequestContent';
|
||||
jest.mock('./hooks', () => ({
|
||||
useCreditRequestData: jest.fn(),
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: { useMasqueradeData: jest.fn() },
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
@@ -20,7 +24,11 @@ let component;
|
||||
const cardId = 'test-card-id';
|
||||
const requestData = { test: 'requestData' };
|
||||
const createCreditRequest = jest.fn().mockName('createCreditRequest');
|
||||
hooks.useCreditRequestData.mockReturnValue({ requestData, createCreditRequest });
|
||||
hooks.useCreditRequestData.mockReturnValue({
|
||||
requestData,
|
||||
createCreditRequest,
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<MustRequestContent cardId={cardId} />);
|
||||
@@ -43,13 +51,18 @@ describe('MustRequestContent component', () => {
|
||||
expect(component.props().action.onClick).toEqual(createCreditRequest);
|
||||
});
|
||||
test('action.message is formatted requestCredit message', () => {
|
||||
expect(component.props().action.message).toEqual(formatMessage(messages.requestCredit));
|
||||
expect(component.props().action.message).toEqual(
|
||||
formatMessage(messages.requestCredit),
|
||||
);
|
||||
});
|
||||
test('action.disabled is false', () => {
|
||||
expect(component.props().action.disabled).toEqual(false);
|
||||
});
|
||||
test('message is formatted mustRequest message', () => {
|
||||
expect(component.props().message).toEqual(
|
||||
formatMessage(messages.mustRequest, {
|
||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||
requestCredit: (<b>{formatMessage(messages.requestCredit)}</b>),
|
||||
linkToProviderSite: <ProviderLink cardId={cardId} />,
|
||||
requestCredit: <b>{formatMessage(messages.requestCredit)}</b>,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,12 +9,14 @@ import messages from './messages';
|
||||
|
||||
export const PendingContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
href,
|
||||
message: formatMessage(messages.viewDetails),
|
||||
disabled: isMasquerading,
|
||||
}}
|
||||
message={formatMessage(messages.received, { providerName })}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,9 @@ import { reduxHooks } from 'hooks';
|
||||
import messages from './messages';
|
||||
import PendingContent from './PendingContent';
|
||||
|
||||
jest.mock('hooks', () => ({ reduxHooks: { useCardCreditData: jest.fn() } }));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
@@ -17,7 +19,11 @@ let component;
|
||||
const cardId = 'test-card-id';
|
||||
const providerName = 'test-credit-provider-name';
|
||||
const providerStatusUrl = 'test-credit-provider-status-url';
|
||||
reduxHooks.useCardCreditData.mockReturnValue({ providerName, providerStatusUrl });
|
||||
reduxHooks.useCardCreditData.mockReturnValue({
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<PendingContent cardId={cardId} />);
|
||||
@@ -40,7 +46,12 @@ describe('PendingContent component', () => {
|
||||
expect(component.props().action.href).toEqual(providerStatusUrl);
|
||||
});
|
||||
test('action.message is formatted requestCredit message', () => {
|
||||
expect(component.props().action.message).toEqual(formatMessage(messages.viewDetails));
|
||||
expect(component.props().action.message).toEqual(
|
||||
formatMessage(messages.viewDetails),
|
||||
);
|
||||
});
|
||||
test('action.disabled is false', () => {
|
||||
expect(component.props().action.disabled).toEqual(false);
|
||||
});
|
||||
test('message is formatted pending message', () => {
|
||||
expect(component.props().message).toEqual(
|
||||
|
||||
@@ -13,7 +13,9 @@ export const CreditContent = ({ action, message, requestData }) => (
|
||||
<ActionRow className="mt-4">
|
||||
<Button
|
||||
as="a"
|
||||
href={action.href}
|
||||
disabled={!!action.disabled}
|
||||
// make sure href is not undefined. Paragon won't disable the button if href is undefined.
|
||||
href={action.href || '#'}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
variant="outline-primary"
|
||||
@@ -36,6 +38,7 @@ CreditContent.propTypes = {
|
||||
href: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
message: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
}),
|
||||
message: PropTypes.node.isRequired,
|
||||
requestData: PropTypes.shape({
|
||||
|
||||
@@ -8,6 +8,7 @@ const action = {
|
||||
href: 'test-action-href',
|
||||
onClick: jest.fn().mockName('test-action-onClick'),
|
||||
message: 'test-action-message',
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
const message = 'test-message';
|
||||
@@ -27,6 +28,7 @@ describe('CreditContent component', () => {
|
||||
const buttonEl = el.find('ActionRow Button');
|
||||
expect(buttonEl.props().href).toEqual(action.href);
|
||||
expect(buttonEl.props().onClick).toEqual(action.onClick);
|
||||
expect(buttonEl.props().disabled).toEqual(action.disabled);
|
||||
expect(buttonEl.text()).toEqual(action.message);
|
||||
});
|
||||
it('loads message into credit-msg div', () => {
|
||||
@@ -35,6 +37,10 @@ describe('CreditContent component', () => {
|
||||
it('loads CreditRequestForm with passed requestData', () => {
|
||||
expect(el.find('CreditRequestForm').props().requestData).toEqual(requestData);
|
||||
});
|
||||
test('disables action button when action.disabled is true', () => {
|
||||
el.setProps({ action: { ...action, disabled: true } });
|
||||
expect(el.find('ActionRow Button').props().disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('without action', () => {
|
||||
test('snapshot', () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ exports[`CreditContent component render with action snapshot 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
className="border-gray-400"
|
||||
disabled={false}
|
||||
href="test-action-href"
|
||||
onClick={[MockFunction test-action-onClick]}
|
||||
rel="noopener"
|
||||
|
||||
@@ -14,7 +14,10 @@ export const useCreditRequestData = (cardId) => {
|
||||
const createCreditApiRequest = apiHooks.useCreateCreditRequest(cardId);
|
||||
const createCreditRequest = (e) => {
|
||||
e.preventDefault();
|
||||
createCreditApiRequest().then(setRequestData);
|
||||
createCreditApiRequest()
|
||||
.then((request) => {
|
||||
setRequestData(request.data);
|
||||
});
|
||||
};
|
||||
return { requestData, createCreditRequest };
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ jest.mock('hooks', () => ({
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const requestData = { test: 'request data' };
|
||||
const requestData = { data: 'request data' };
|
||||
const creditRequest = jest.fn().mockReturnValue(Promise.resolve(requestData));
|
||||
apiHooks.useCreateCreditRequest.mockReturnValue(creditRequest);
|
||||
const event = { preventDefault: jest.fn() };
|
||||
@@ -48,7 +48,7 @@ describe('Credit Banner view hooks', () => {
|
||||
it('calls api.createCreditRequest and sets requestData with the response', async () => {
|
||||
await out.createCreditRequest(event);
|
||||
expect(creditRequest).toHaveBeenCalledWith();
|
||||
expect(state.setState.creditRequestData).toHaveBeenCalledWith(requestData);
|
||||
expect(state.setState.creditRequestData).toHaveBeenCalledWith(requestData.data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,12 +6,6 @@ exports[`CertificateBanner snapshot is passing and is downloadable 1`] = `
|
||||
variant="success"
|
||||
>
|
||||
Congratulations. Your certificate is ready.
|
||||
|
||||
<Hyperlink
|
||||
isInline={true}
|
||||
>
|
||||
View Certificate.
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
@@ -27,20 +21,40 @@ exports[`CertificateBanner snapshot is restricted 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted and verified 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
|
||||
|
||||
If you would like a refund on your Certificate of Achievement, please contact us.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted and verified with billing email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
|
||||
|
||||
<format-message-function
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
|
||||
"description": "Restricted certificate warning message",
|
||||
"id": "learner-dash.courseCard.banners.certificateRestricted",
|
||||
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
|
||||
"description": "Message to learners to contact billing for certificate refunds",
|
||||
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmail": <MailtoLink
|
||||
to="suport@email"
|
||||
"billingEmail": <MailtoLink
|
||||
to="billing@email"
|
||||
>
|
||||
suport@email
|
||||
billing@email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
@@ -48,7 +62,7 @@ exports[`CertificateBanner snapshot is restricted 1`] = `
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted and verified 1`] = `
|
||||
exports[`CertificateBanner snapshot is restricted and verified with support and billing email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
@@ -92,6 +106,66 @@ exports[`CertificateBanner snapshot is restricted and verified 1`] = `
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted and verified with support email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
|
||||
"description": "Restricted certificate warning message",
|
||||
"id": "learner-dash.courseCard.banners.certificateRestricted",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmail": <MailtoLink
|
||||
to="suport@email"
|
||||
>
|
||||
suport@email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
If you would like a refund on your Certificate of Achievement, please contact us.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted with billing email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted with support email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
|
||||
"description": "Restricted certificate warning message",
|
||||
"id": "learner-dash.courseCard.banners.certificateRestricted",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmail": <MailtoLink
|
||||
to="suport@email"
|
||||
>
|
||||
suport@email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot not passing and audit 1`] = `
|
||||
<Banner>
|
||||
Grade required to pass the course: 0.8%
|
||||
@@ -113,6 +187,15 @@ exports[`CertificateBanner snapshot not passing and has finished 1`] = `
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot not passing and is downloadable 1`] = `
|
||||
<Banner
|
||||
icon={[MockFunction icons.CheckCircle]}
|
||||
variant="success"
|
||||
>
|
||||
Congratulations. Your certificate is ready.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot not passing and not audit and not finished 1`] = `
|
||||
<Banner
|
||||
variant="warning"
|
||||
|
||||
@@ -31,11 +31,21 @@ export const messages = StrictDict({
|
||||
description: 'Restricted certificate warning message',
|
||||
defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.',
|
||||
},
|
||||
certRestrictedNoEmail: {
|
||||
id: 'learner-dash.courseCard.banners.certificateRestrictedNoEmail',
|
||||
description: 'Restricted certificate warning message',
|
||||
defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.',
|
||||
},
|
||||
certRefundContactBilling: {
|
||||
id: 'learner-dash.courseCard.banners.certificateRefundContactBilling',
|
||||
description: 'Message to learners to contact billing for certificate refunds',
|
||||
defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}',
|
||||
},
|
||||
certRefundContactBillingNoEmail: {
|
||||
id: 'learner-dash.courseCard.banners.certificateRefundContactBillingNoEmail',
|
||||
description: 'Message to learners to contact billing for certificate refunds',
|
||||
defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact us.',
|
||||
},
|
||||
passingGrade: {
|
||||
id: 'learner-dash.courseCard.banners.passingGrade',
|
||||
description: 'Message to learners with minimum passing grade for the course',
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('CourseCardDetails hooks', () => {
|
||||
};
|
||||
const entitlementData = {
|
||||
isEntitlement: false,
|
||||
canViewCourse: false,
|
||||
disableViewCourse: false,
|
||||
isFulfilled: false,
|
||||
isExpired: false,
|
||||
canChange: false,
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Badge } from '@edx/paragon';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import verifiedRibbon from 'assets/verified-ribbon.png';
|
||||
import useActionDisabledState from './hooks';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
@@ -18,13 +18,13 @@ export const CourseCardImage = ({ cardId, orientation }) => {
|
||||
const { bannerImgSrc } = reduxHooks.useCardCourseData(cardId);
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isEntitlement } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const { disableCourseTitle } = useActionDisabledState(cardId);
|
||||
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
|
||||
const wrapperClassName = `pgn__card-wrapper-image-cap overflow-visible ${orientation}`;
|
||||
const image = (
|
||||
<>
|
||||
<img
|
||||
className="pgn__card-image-cap"
|
||||
className="pgn__card-image-cap show"
|
||||
src={bannerImgSrc}
|
||||
alt={formatMessage(messages.bannerAlt)}
|
||||
/>
|
||||
@@ -43,7 +43,7 @@ export const CourseCardImage = ({ cardId, orientation }) => {
|
||||
}
|
||||
</>
|
||||
);
|
||||
return isEntitlement
|
||||
return disableCourseTitle
|
||||
? (<div className={wrapperClassName}>{image}</div>)
|
||||
: (
|
||||
<a
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from './hooks';
|
||||
import CourseCardImage from './CourseCardImage';
|
||||
|
||||
const homeUrl = 'home-url';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
courseImageClicked: jest.fn().mockName('segment.courseImageClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(() => ({ bannerImgSrc: 'banner-img-src' })),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||
useCardEnrollmentData: jest.fn(() => ({ isVerified: true })),
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
|
||||
trackCourseEvent: { eventName, cardId, upgradeUrl },
|
||||
})),
|
||||
},
|
||||
}));
|
||||
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
|
||||
|
||||
describe('CourseCardImage', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
orientation: 'orientation',
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('renders clickable link course Image', () => {
|
||||
const wrapper = shallow(<CourseCardImage {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.type()).toBe('a');
|
||||
expect(wrapper.prop('onClick')).toEqual(
|
||||
reduxHooks.useTrackCourseEvent(
|
||||
track.course.courseImageClicked,
|
||||
props.cardId,
|
||||
homeUrl,
|
||||
),
|
||||
);
|
||||
});
|
||||
test('renders disabled link', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
|
||||
const wrapper = shallow(<CourseCardImage {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.type()).toBe('div');
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes', () => {
|
||||
shallow(<CourseCardImage {...props} />);
|
||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
|
||||
props.cardId,
|
||||
);
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as ReactShare from 'react-share';
|
||||
|
||||
import { StrictDict } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const testIds = StrictDict({
|
||||
emailSettingsModalToggle: 'emailSettingsModalToggle',
|
||||
});
|
||||
|
||||
export const SocialShareMenu = ({ cardId, emailSettings }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const { isEmailEnabled, isExecEd2UCourse } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isEmailEnabled && (
|
||||
<Dropdown.Item
|
||||
disabled={isMasquerading}
|
||||
onClick={emailSettings.show}
|
||||
data-testid={testIds.emailSettingsModalToggle}
|
||||
>
|
||||
{formatMessage(messages.emailSettings)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{facebook.isEnabled && (
|
||||
<ReactShare.FacebookShareButton
|
||||
url={facebook.shareUrl}
|
||||
onClick={handleFacebookShare}
|
||||
title={formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: facebook.socialBrand,
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
>
|
||||
{formatMessage(messages.shareToFacebook)}
|
||||
</ReactShare.FacebookShareButton>
|
||||
)}
|
||||
{twitter.isEnabled && (
|
||||
<ReactShare.TwitterShareButton
|
||||
url={twitter.shareUrl}
|
||||
onClick={handleTwitterShare}
|
||||
title={formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: twitter.socialBrand,
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
>
|
||||
{formatMessage(messages.shareToTwitter)}
|
||||
</ReactShare.TwitterShareButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
SocialShareMenu.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
emailSettings: PropTypes.shape({
|
||||
show: PropTypes.func,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default SocialShareMenu;
|
||||
@@ -0,0 +1,235 @@
|
||||
import { when } from 'jest-when';
|
||||
import * as ReactShare from 'react-share';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage, shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { useEmailSettings } from './hooks';
|
||||
import SocialShareMenu, { testIds } from './SocialShareMenu';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('react-share', () => ({
|
||||
FacebookShareButton: () => 'FacebookShareButton',
|
||||
TwitterShareButton: () => 'TwitterShareButton',
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
socialShare: 'test-social-share-key',
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
useIntl: jest.fn().mockReturnValue({
|
||||
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardSocialSettingsData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn((...args) => ({ trackCourseEvent: args })),
|
||||
},
|
||||
}));
|
||||
jest.mock('./hooks', () => ({
|
||||
useEmailSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
const props = {
|
||||
cardId: 'test-card-id',
|
||||
emailSettings: { show: jest.fn() },
|
||||
};
|
||||
|
||||
const mockHook = (fn, returnValue, options = {}) => {
|
||||
if (options.isCardHook) {
|
||||
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue);
|
||||
} else {
|
||||
when(fn).calledWith().mockReturnValueOnce(returnValue);
|
||||
}
|
||||
};
|
||||
|
||||
const courseName = 'test-course-name';
|
||||
|
||||
const socialShare = {
|
||||
facebook: {
|
||||
isEnabled: true,
|
||||
shareUrl: 'facebook-share-url',
|
||||
socialBrand: 'facebook-social-brand',
|
||||
},
|
||||
twitter: {
|
||||
isEnabled: true,
|
||||
shareUrl: 'twitter-share-url',
|
||||
socialBrand: 'twitter-social-brand',
|
||||
},
|
||||
};
|
||||
|
||||
const mockHooks = (returnVals = {}) => {
|
||||
mockHook(
|
||||
reduxHooks.useCardEnrollmentData,
|
||||
{
|
||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||
isExecEd2UCourse: !!returnVals.isExecEd2UCourse,
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
mockHook(reduxHooks.useCardCourseData, { courseName }, { isCardHook: true });
|
||||
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
|
||||
mockHook(
|
||||
reduxHooks.useCardSocialSettingsData,
|
||||
{
|
||||
facebook: { ...socialShare.facebook, isEnabled: !!returnVals.facebook?.isEnabled },
|
||||
twitter: { ...socialShare.twitter, isEnabled: !!returnVals.twitter?.isEnabled },
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
};
|
||||
|
||||
let el;
|
||||
const render = () => {
|
||||
el = shallow(<SocialShareMenu {...props} />);
|
||||
};
|
||||
|
||||
describe('SocialShareMenu', () => {
|
||||
describe('behavior', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks();
|
||||
render();
|
||||
});
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
it('initializes local hooks', () => {
|
||||
when(useEmailSettings).expectCalledWith();
|
||||
});
|
||||
it('initializes redux hook data ', () => {
|
||||
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useCardCourseData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useCardSocialSettingsData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
||||
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter');
|
||||
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook');
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
it('renders null if exec ed course', () => {
|
||||
mockHooks({ isExecEd2UCourse: true });
|
||||
render();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
const testEmailSettingsDropdown = (isMasquerading = false) => {
|
||||
describe('email settings dropdown', () => {
|
||||
const loadToggle = () => el.instance.findByTestId(testIds.emailSettingsModalToggle)[0];
|
||||
it('renders', () => {
|
||||
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(1);
|
||||
});
|
||||
if (isMasquerading) {
|
||||
it('is disabled', () => {
|
||||
expect(loadToggle().props.disabled).toEqual(true);
|
||||
});
|
||||
} else {
|
||||
it('is enabled', () => {
|
||||
expect(loadToggle().props.disabled).toEqual(false);
|
||||
});
|
||||
}
|
||||
test('show email settings modal on click', () => {
|
||||
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
|
||||
});
|
||||
});
|
||||
};
|
||||
const testFacebookShareButton = () => {
|
||||
test('renders facebook share button with courseName and brand', () => {
|
||||
const button = el.instance.findByType(ReactShare.FacebookShareButton)[0];
|
||||
expect(button.props.url).toEqual(socialShare.facebook.shareUrl);
|
||||
expect(button.props.onClick).toEqual(
|
||||
reduxHooks.useTrackCourseEvent(track.socialShare, props.cardId, 'facebook'),
|
||||
);
|
||||
expect(button.props.title).toEqual(formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: socialShare.facebook.socialBrand,
|
||||
}));
|
||||
});
|
||||
};
|
||||
const testTwitterShareButton = () => {
|
||||
test('renders twitter share button with courseName and brand', () => {
|
||||
const button = el.instance.findByType(ReactShare.TwitterShareButton)[0];
|
||||
expect(button.props.url).toEqual(socialShare.twitter.shareUrl);
|
||||
expect(button.props.onClick).toEqual(
|
||||
reduxHooks.useTrackCourseEvent(track.socialShare, props.cardId, 'twitter'),
|
||||
);
|
||||
expect(button.props.title).toEqual(formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: socialShare.twitter.socialBrand,
|
||||
}));
|
||||
});
|
||||
};
|
||||
describe('all enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({
|
||||
facebook: { isEnabled: true },
|
||||
twitter: { isEnabled: true },
|
||||
isEmailEnabled: true,
|
||||
});
|
||||
render();
|
||||
});
|
||||
describe('email settings dropdown', () => {
|
||||
const loadToggle = () => el.instance.findByTestId(testIds.emailSettingsModalToggle)[0];
|
||||
it('renders', () => {
|
||||
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(1);
|
||||
});
|
||||
it('is enabled', () => {
|
||||
expect(loadToggle().props.disabled).toEqual(false);
|
||||
});
|
||||
test('show email settings modal on click', () => {
|
||||
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
|
||||
});
|
||||
});
|
||||
testEmailSettingsDropdown();
|
||||
testFacebookShareButton();
|
||||
testTwitterShareButton();
|
||||
});
|
||||
describe('only email enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ isEmailEnabled: true });
|
||||
render();
|
||||
});
|
||||
testEmailSettingsDropdown();
|
||||
it('does not render facebook or twitter controls', () => {
|
||||
expect(el.instance.findByType(ReactShare.FacebookShareButton).length).toEqual(0);
|
||||
expect(el.instance.findByType(ReactShare.TwitterShareButton).length).toEqual(0);
|
||||
});
|
||||
describe('masquerading', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ isEmailEnabled: true, isMasquerading: true });
|
||||
render();
|
||||
});
|
||||
testEmailSettingsDropdown(true);
|
||||
});
|
||||
});
|
||||
describe('only facebook enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ facebook: { isEnabled: true } });
|
||||
render();
|
||||
});
|
||||
testFacebookShareButton();
|
||||
it('does not render email or twitter controls', () => {
|
||||
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(0);
|
||||
expect(el.instance.findByType(ReactShare.TwitterShareButton).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe('only twitter enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ twitter: { isEnabled: true } });
|
||||
render();
|
||||
});
|
||||
testTwitterShareButton();
|
||||
it('does not render email or facebook controls', () => {
|
||||
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(0);
|
||||
expect(el.instance.findByType(ReactShare.FacebookShareButton).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,44 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1`] = `
|
||||
exports[`CourseCardMenu render show dropdown hide unenroll item and disable email snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown>
|
||||
<Dropdown
|
||||
onToggle={[MockFunction hooks.handleToggleDropdown]}
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
alt="Course actions dropdown"
|
||||
as="IconButton"
|
||||
iconAs="Icon"
|
||||
id="course-actions-dropdown-test-card-id"
|
||||
src={[MockFunction icons.MoreVert]}
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<SocialShareMenu
|
||||
cardId="test-card-id"
|
||||
emailSettings={
|
||||
Object {
|
||||
"hide": [MockFunction emailSettingHide],
|
||||
"isVisible": false,
|
||||
"show": [MockFunction emailSettingShow],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
cardId="test-card-id"
|
||||
closeModal={[MockFunction unenrollHide]}
|
||||
show={false}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseCardMenu render show dropdown show unenroll and enable email snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown
|
||||
onToggle={[MockFunction hooks.handleToggleDropdown]}
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
alt="Course actions dropdown"
|
||||
as="IconButton"
|
||||
@@ -19,31 +55,16 @@ exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1
|
||||
>
|
||||
Unenroll
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid="emailSettingsModalToggle"
|
||||
disabled={false}
|
||||
onClick={[MockFunction emailSettingShow]}
|
||||
>
|
||||
Email settings
|
||||
</Dropdown.Item>
|
||||
<FacebookShareButton
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
onClick={[MockFunction facebookShareClick]}
|
||||
resetButtonStyle={false}
|
||||
title="I'm taking test-course-name online with facebook-social-brand. Check it out!"
|
||||
url="facebook-share-url"
|
||||
>
|
||||
Share to Facebook
|
||||
</FacebookShareButton>
|
||||
<TwitterShareButton
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
onClick={[MockFunction twitterShareClick]}
|
||||
resetButtonStyle={false}
|
||||
title="I'm taking test-course-name online with twitter-social-brand. Check it out!"
|
||||
url="twitter-share-url"
|
||||
>
|
||||
Share to Twitter
|
||||
</TwitterShareButton>
|
||||
<SocialShareMenu
|
||||
cardId="test-card-id"
|
||||
emailSettings={
|
||||
Object {
|
||||
"hide": [MockFunction emailSettingHide],
|
||||
"isVisible": false,
|
||||
"show": [MockFunction emailSettingShow],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
@@ -58,83 +79,3 @@ exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseCardMenu masquerading snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
alt="Course actions dropdown"
|
||||
as="IconButton"
|
||||
iconAs="Icon"
|
||||
id="course-actions-dropdown-test-card-id"
|
||||
src={[MockFunction icons.MoreVert]}
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
data-testid="unenrollModalToggle"
|
||||
disabled={true}
|
||||
onClick={[MockFunction unenrollShow]}
|
||||
>
|
||||
Unenroll
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid="emailSettingsModalToggle"
|
||||
disabled={true}
|
||||
onClick={[MockFunction emailSettingShow]}
|
||||
>
|
||||
Email settings
|
||||
</Dropdown.Item>
|
||||
<FacebookShareButton
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
onClick={[MockFunction facebookShareClick]}
|
||||
resetButtonStyle={false}
|
||||
title="I'm taking test-course-name online with facebook-social-brand. Check it out!"
|
||||
url="facebook-share-url"
|
||||
>
|
||||
Share to Facebook
|
||||
</FacebookShareButton>
|
||||
<TwitterShareButton
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
onClick={[MockFunction twitterShareClick]}
|
||||
resetButtonStyle={false}
|
||||
title="I'm taking test-course-name online with twitter-social-brand. Check it out!"
|
||||
url="twitter-share-url"
|
||||
>
|
||||
Share to Twitter
|
||||
</TwitterShareButton>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
cardId="test-card-id"
|
||||
closeModal={[MockFunction unenrollHide]}
|
||||
show={false}
|
||||
/>
|
||||
<EmailSettingsModal
|
||||
cardId="test-card-id"
|
||||
closeModal={[MockFunction emailSettingHide]}
|
||||
show={false}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseCardMenu not enrolled, share disabled, email setting disabled snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
alt="Course actions dropdown"
|
||||
as="IconButton"
|
||||
iconAs="Icon"
|
||||
id="course-actions-dropdown-test-card-id"
|
||||
src={[MockFunction icons.MoreVert]}
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu />
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
cardId="test-card-id"
|
||||
closeModal={[MockFunction unenrollHide]}
|
||||
show={false}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import React from 'react';
|
||||
import { StrictDict } from 'utils';
|
||||
import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
isUnenrollConfirmVisible: (val) => React.useState(val), // eslint-disable-line
|
||||
isEmailSettingsVisible: (val) => React.useState(val), // eslint-disable-line
|
||||
export const stateKeys = StrictDict({
|
||||
isUnenrollConfirmVisible: 'isUnenrollConfirmVisible',
|
||||
isEmailSettingsVisible: 'isEmailSettingsVisible',
|
||||
});
|
||||
|
||||
export const useUnenrollData = () => {
|
||||
const [isVisible, setIsVisible] = module.state.isUnenrollConfirmVisible(false);
|
||||
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isUnenrollConfirmVisible, false);
|
||||
return {
|
||||
show: () => setIsVisible(true),
|
||||
hide: () => setIsVisible(false),
|
||||
@@ -21,7 +18,7 @@ export const useUnenrollData = () => {
|
||||
};
|
||||
|
||||
export const useEmailSettings = () => {
|
||||
const [isVisible, setIsVisible] = module.state.isEmailSettingsVisible(false);
|
||||
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isEmailSettingsVisible, false);
|
||||
return {
|
||||
show: () => setIsVisible(true),
|
||||
hide: () => setIsVisible(false),
|
||||
@@ -30,9 +27,30 @@ export const useEmailSettings = () => {
|
||||
};
|
||||
|
||||
export const useHandleToggleDropdown = (cardId) => {
|
||||
const eventName = track.course.courseOptionsDropdownClicked;
|
||||
const trackCourseEvent = reduxHooks.useTrackCourseEvent(eventName, cardId);
|
||||
const trackCourseEvent = reduxHooks.useTrackCourseEvent(
|
||||
track.course.courseOptionsDropdownClicked,
|
||||
cardId,
|
||||
);
|
||||
return (isOpen) => {
|
||||
if (isOpen) { trackCourseEvent(); }
|
||||
};
|
||||
};
|
||||
|
||||
export const useOptionVisibility = (cardId) => {
|
||||
const { isEnrolled, isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
|
||||
const { isEarned } = reduxHooks.useCardCertificateData(cardId);
|
||||
|
||||
const shouldShowUnenrollItem = isEnrolled && !isEarned;
|
||||
const shouldShowDropdown = (
|
||||
shouldShowUnenrollItem
|
||||
|| isEmailEnabled
|
||||
|| facebook.isEnabled
|
||||
|| twitter.isEnabled
|
||||
);
|
||||
|
||||
return {
|
||||
shouldShowUnenrollItem,
|
||||
shouldShowDropdown,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
|
||||
@@ -6,71 +7,77 @@ import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCertificateData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardSocialSettingsData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const trackCourseEvent = jest.fn();
|
||||
reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
let out;
|
||||
|
||||
describe('CourseCardMenu hooks', () => {
|
||||
describe('state values', () => {
|
||||
state.testGetter(state.keys.isUnenrollConfirmVisible);
|
||||
state.testGetter(state.keys.isEmailSettingsVisible);
|
||||
});
|
||||
const state = mockUseKeyedState(hooks.stateKeys);
|
||||
|
||||
describe('CourseCardMenu hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
state.mock();
|
||||
});
|
||||
describe('useUnenrollData', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
state.mockVals({ isUnenrollConfirmVisible: true });
|
||||
out = hooks.useUnenrollData();
|
||||
});
|
||||
afterEach(state.restore);
|
||||
|
||||
test('default state', () => {
|
||||
expect(out.isVisible).toEqual(state.stateVals.isUnenrollConfirmVisible);
|
||||
describe('behavior', () => {
|
||||
it('initializes isUnenrollConfirmVisible state to false', () => {
|
||||
state.expectInitializedWith(state.keys.isUnenrollConfirmVisible, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('show', () => {
|
||||
out.show();
|
||||
state.expectSetStateCalledWith(state.keys.isUnenrollConfirmVisible, true);
|
||||
});
|
||||
|
||||
test('hide', () => {
|
||||
out.hide();
|
||||
state.expectSetStateCalledWith(state.keys.isUnenrollConfirmVisible, false);
|
||||
describe('output', () => {
|
||||
test('state is loaded from current state value', () => {
|
||||
expect(out.isVisible).toEqual(true);
|
||||
});
|
||||
test('show sets state value to true', () => {
|
||||
out.show();
|
||||
expect(state.setState.isUnenrollConfirmVisible).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('hide sets state value to false', () => {
|
||||
out.hide();
|
||||
expect(state.setState.isUnenrollConfirmVisible).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useEmailSettings', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
state.mockVals({ isEmailSettingsVisible: true });
|
||||
out = hooks.useEmailSettings();
|
||||
});
|
||||
afterEach(state.restore);
|
||||
|
||||
test('default state', () => {
|
||||
expect(out.isVisible).toEqual(state.stateVals.isEmailSettingsVisible);
|
||||
describe('behavior', () => {
|
||||
it('initializes isEmailSettingsVisible state to false', () => {
|
||||
state.expectInitializedWith(state.keys.isEmailSettingsVisible, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('show', () => {
|
||||
out.show();
|
||||
state.expectSetStateCalledWith(state.keys.isEmailSettingsVisible, true);
|
||||
});
|
||||
|
||||
test('hide', () => {
|
||||
out.hide();
|
||||
state.expectSetStateCalledWith(state.keys.isEmailSettingsVisible, false);
|
||||
describe('output', () => {
|
||||
test('state is loaded from current state value', () => {
|
||||
expect(out.isVisible).toEqual(state.values.isEmailSettingsVisible);
|
||||
});
|
||||
test('show sets state value to true', () => {
|
||||
out.show();
|
||||
expect(state.setState.isEmailSettingsVisible).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('hide sets state value to false', () => {
|
||||
out.hide();
|
||||
expect(state.setState.isEmailSettingsVisible).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useHandleToggleDropdown', () => {
|
||||
beforeEach(() => {
|
||||
out = hooks.useHandleToggleDropdown(cardId);
|
||||
});
|
||||
beforeEach(() => { out = hooks.useHandleToggleDropdown(cardId); });
|
||||
describe('behavior', () => {
|
||||
it('initializes course event tracker with event name and card ID', () => {
|
||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
||||
@@ -88,4 +95,59 @@ describe('CourseCardMenu hooks', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useOptionVisibility', () => {
|
||||
const mockReduxHooks = (returnVals = {}) => {
|
||||
reduxHooks.useCardSocialSettingsData.mockReturnValueOnce({
|
||||
facebook: { isEnabled: !!returnVals.facebook?.isEnabled },
|
||||
twitter: { isEnabled: !!returnVals.twitter?.isEnabled },
|
||||
});
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
isEnrolled: !!returnVals.isEnrolled,
|
||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||
});
|
||||
reduxHooks.useCardCertificateData.mockReturnValueOnce({
|
||||
isEarned: !!returnVals.isEarned,
|
||||
});
|
||||
};
|
||||
describe('shouldShowUnenrollItem', () => {
|
||||
it('returns true if enrolled and not earned', () => {
|
||||
mockReduxHooks({ isEnrolled: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(true);
|
||||
});
|
||||
it('returns false if not enrolled', () => {
|
||||
mockReduxHooks();
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(false);
|
||||
});
|
||||
it('returns false if enrolled but also earned', () => {
|
||||
mockReduxHooks({ isEarned: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldShowDropdown', () => {
|
||||
it('returns false if not enrolled and both email and socials are disabled', () => {
|
||||
mockReduxHooks();
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(false);
|
||||
});
|
||||
it('returns false if enrolled but already earned, and both email and socials are disabled', () => {
|
||||
mockReduxHooks({ isEnrolled: true, isEarned: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(false);
|
||||
});
|
||||
it('returns true if either social is enabled', () => {
|
||||
mockReduxHooks({ facebook: { isEnabled: true } });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
mockReduxHooks({ twitter: { isEnabled: true } });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
});
|
||||
it('returns true if email is enabled', () => {
|
||||
mockReduxHooks({ isEmailEnabled: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
});
|
||||
it('returns true if enrolled and not earned', () => {
|
||||
mockReduxHooks({ isEnrolled: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,44 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as ReactShare from 'react-share';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown, Icon, IconButton } from '@edx/paragon';
|
||||
import { MoreVert } from '@edx/paragon/icons';
|
||||
import { StrictDict } from '@edx/react-unit-test-utils';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import EmailSettingsModal from 'containers/EmailSettingsModal';
|
||||
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import SocialShareMenu from './SocialShareMenu';
|
||||
import {
|
||||
useEmailSettings,
|
||||
useUnenrollData,
|
||||
useHandleToggleDropdown,
|
||||
useOptionVisibility,
|
||||
} from './hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const testIds = StrictDict({
|
||||
unenrollModalToggle: 'unenrollModalToggle',
|
||||
});
|
||||
|
||||
export const CourseCardMenu = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const { isEnrolled, isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
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',
|
||||
);
|
||||
|
||||
const emailSettingsModal = useEmailSettings();
|
||||
const emailSettings = useEmailSettings();
|
||||
const unenrollModal = useUnenrollData();
|
||||
const handleToggleDropdown = useHandleToggleDropdown(cardId);
|
||||
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
|
||||
if (!shouldShowDropdown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -52,52 +49,16 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
alt={formatMessage(messages.dropdownAlt)}
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
{isEnrolled && (
|
||||
{shouldShowUnenrollItem && (
|
||||
<Dropdown.Item
|
||||
disabled={isMasquerading}
|
||||
onClick={unenrollModal.show}
|
||||
data-testid="unenrollModalToggle"
|
||||
data-testid={testIds.unenrollModalToggle}
|
||||
>
|
||||
{formatMessage(messages.unenroll)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{isEmailEnabled && (
|
||||
<Dropdown.Item
|
||||
disabled={isMasquerading}
|
||||
onClick={emailSettingsModal.show}
|
||||
data-testid="emailSettingsModalToggle"
|
||||
>
|
||||
{formatMessage(messages.emailSettings)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{facebook.isEnabled && (
|
||||
<ReactShare.FacebookShareButton
|
||||
url={facebook.shareUrl}
|
||||
onClick={handleFacebookShare}
|
||||
title={formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: facebook.socialBrand,
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
>
|
||||
{formatMessage(messages.shareToFacebook)}
|
||||
</ReactShare.FacebookShareButton>
|
||||
)}
|
||||
{twitter.isEnabled && (
|
||||
<ReactShare.TwitterShareButton
|
||||
url={twitter.shareUrl}
|
||||
onClick={handleTwitterShare}
|
||||
title={formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: twitter.socialBrand,
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
>
|
||||
{formatMessage(messages.shareToTwitter)}
|
||||
</ReactShare.TwitterShareButton>
|
||||
)}
|
||||
<SocialShareMenu cardId={cardId} emailSettings={emailSettings} />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
@@ -107,8 +68,8 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
/>
|
||||
{isEmailEnabled && (
|
||||
<EmailSettingsModal
|
||||
show={emailSettingsModal.isVisible}
|
||||
closeModal={emailSettingsModal.hide}
|
||||
show={emailSettings.isVisible}
|
||||
closeModal={emailSettings.hide}
|
||||
cardId={cardId}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,141 +1,213 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { when } from 'jest-when';
|
||||
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import EmailSettingsModal from 'containers/EmailSettingsModal';
|
||||
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useEmailSettings, useUnenrollData } from './hooks';
|
||||
import CourseCardMenu from '.';
|
||||
import SocialShareMenu from './SocialShareMenu';
|
||||
import * as hooks from './hooks';
|
||||
import CourseCardMenu, { testIds } from '.';
|
||||
|
||||
jest.mock('react-share', () => ({
|
||||
FacebookShareButton: () => 'FacebookShareButton',
|
||||
TwitterShareButton: () => 'TwitterShareButton',
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
useIntl: jest.fn().mockReturnValue({
|
||||
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
|
||||
}),
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardSocialSettingsData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
useTrackCourseEvent: (_, __, site) => jest.fn().mockName(`${site}ShareClick`),
|
||||
},
|
||||
reduxHooks: { useMasqueradeData: jest.fn(), useCardEnrollmentData: jest.fn() },
|
||||
}));
|
||||
jest.mock('./SocialShareMenu', () => 'SocialShareMenu');
|
||||
jest.mock('./hooks', () => ({
|
||||
useEmailSettings: jest.fn(),
|
||||
useUnenrollData: jest.fn(),
|
||||
useHandleToggleDropdown: jest.fn(),
|
||||
useOptionVisibility: jest.fn(),
|
||||
}));
|
||||
|
||||
const props = {
|
||||
cardId: 'test-card-id',
|
||||
};
|
||||
const defaultEmailSettingsModal = {
|
||||
|
||||
const emailSettings = {
|
||||
isVisible: false,
|
||||
show: jest.fn().mockName('emailSettingShow'),
|
||||
hide: jest.fn().mockName('emailSettingHide'),
|
||||
};
|
||||
const defaultUnenrollModal = {
|
||||
|
||||
const unenrollData = {
|
||||
isVisible: false,
|
||||
show: jest.fn().mockName('unenrollShow'),
|
||||
hide: jest.fn().mockName('unenrollHide'),
|
||||
};
|
||||
const defaultSocialShare = {
|
||||
facebook: {
|
||||
isEnabled: true,
|
||||
shareUrl: 'facebook-share-url',
|
||||
socialBrand: 'facebook-social-brand',
|
||||
},
|
||||
twitter: {
|
||||
isEnabled: true,
|
||||
shareUrl: 'twitter-share-url',
|
||||
socialBrand: 'twitter-social-brand',
|
||||
},
|
||||
};
|
||||
const courseName = 'test-course-name';
|
||||
let wrapper;
|
||||
|
||||
let el;
|
||||
|
||||
const mockHook = (fn, returnValue, options = {}) => {
|
||||
if (options.isCardHook) {
|
||||
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue);
|
||||
} else {
|
||||
when(fn).calledWith().mockReturnValueOnce(returnValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleDropdown = jest.fn().mockName('hooks.handleToggleDropdown');
|
||||
|
||||
const mockHooks = (returnVals = {}) => {
|
||||
mockHook(
|
||||
hooks.useEmailSettings,
|
||||
returnVals.emailSettings ? returnVals.emailSettings : emailSettings,
|
||||
);
|
||||
mockHook(
|
||||
hooks.useUnenrollData,
|
||||
returnVals.unenrollData ? returnVals.unenrollData : unenrollData,
|
||||
);
|
||||
mockHook(hooks.useHandleToggleDropdown, handleToggleDropdown, { isCardHook: true });
|
||||
mockHook(
|
||||
hooks.useOptionVisibility,
|
||||
{
|
||||
shouldShowUnenrollItem: !!returnVals.shouldShowUnenrollItem,
|
||||
shouldShowDropdown: !!returnVals.shouldShowDropdown,
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
|
||||
mockHook(
|
||||
reduxHooks.useCardEnrollmentData,
|
||||
{ isEmailEnabled: !!returnVals.isEmailEnabled },
|
||||
{ isCardHook: true },
|
||||
);
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<CourseCardMenu {...props} />);
|
||||
};
|
||||
|
||||
describe('CourseCardMenu', () => {
|
||||
beforeEach(() => {
|
||||
useEmailSettings.mockReturnValue(defaultEmailSettingsModal);
|
||||
useUnenrollData.mockReturnValue(defaultUnenrollModal);
|
||||
reduxHooks.useCardSocialSettingsData.mockReturnValue(defaultSocialShare);
|
||||
reduxHooks.useCardCourseData.mockReturnValue({ courseName });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: true, isEmailEnabled: true });
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
});
|
||||
describe('enrolled, share enabled, email setting enable', () => {
|
||||
describe('behavior', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<CourseCardMenu {...props} />);
|
||||
mockHooks();
|
||||
render();
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
it('renders share buttons', () => {
|
||||
el = wrapper.find('FacebookShareButton');
|
||||
expect(el.length).toEqual(1);
|
||||
expect(el.prop('url')).toEqual('facebook-share-url');
|
||||
el = wrapper.find('TwitterShareButton');
|
||||
expect(el.length).toEqual(1);
|
||||
expect(el.prop('url')).toEqual('twitter-share-url');
|
||||
it('initializes local hooks', () => {
|
||||
when(hooks.useEmailSettings).expectCalledWith();
|
||||
when(hooks.useUnenrollData).expectCalledWith();
|
||||
when(hooks.useHandleToggleDropdown).expectCalledWith(props.cardId);
|
||||
when(hooks.useOptionVisibility).expectCalledWith(props.cardId);
|
||||
});
|
||||
it('renders enabled unenroll modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
|
||||
expect(el.props().disabled).toEqual(false);
|
||||
});
|
||||
it('renders enabled email settings modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
|
||||
expect(el.props().disabled).toEqual(false);
|
||||
});
|
||||
it('renders enabled email settings modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
|
||||
expect(el.props().disabled).toEqual(false);
|
||||
it('initializes redux hook data ', () => {
|
||||
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
||||
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
describe('not enrolled, share disabled, email setting disabled', () => {
|
||||
beforeEach(() => {
|
||||
reduxHooks.useCardSocialSettingsData.mockReturnValueOnce({
|
||||
...defaultSocialShare,
|
||||
twitter: { ...defaultSocialShare.twitter, isEnabled: false },
|
||||
facebook: { ...defaultSocialShare.facebook, isEnabled: false },
|
||||
describe('render', () => {
|
||||
it('renders null if showDropdown is false', () => {
|
||||
mockHooks();
|
||||
render();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
const testHandleToggle = () => {
|
||||
it('displays Dropdown with onToggle=handleToggleDropdown', () => {
|
||||
expect(el.instance.findByType(Dropdown)[0].props.onToggle).toEqual(handleToggleDropdown);
|
||||
});
|
||||
};
|
||||
const testUnenrollConfirmModal = () => {
|
||||
it('displays UnenrollConfirmModal with cardId and unenrollModal data', () => {
|
||||
const modal = el.instance.findByType(UnenrollConfirmModal)[0];
|
||||
expect(modal.props.show).toEqual(unenrollData.isVisible);
|
||||
expect(modal.props.closeModal).toEqual(unenrollData.hide);
|
||||
expect(modal.props.cardId).toEqual(props.cardId);
|
||||
});
|
||||
};
|
||||
const testSocialShareMenu = () => {
|
||||
it('displays SocialShareMenu with cardID and emailSettings', () => {
|
||||
const menu = el.instance.findByType(SocialShareMenu)[0];
|
||||
expect(menu.props.cardId).toEqual(props.cardId);
|
||||
expect(menu.props.emailSettings).toEqual(emailSettings);
|
||||
});
|
||||
};
|
||||
describe('show dropdown', () => {
|
||||
describe('hide unenroll item and disable email', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ shouldShowDropdown: true });
|
||||
render();
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
testHandleToggle();
|
||||
testSocialShareMenu();
|
||||
it('does not render unenroll modal toggle', () => {
|
||||
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(0);
|
||||
});
|
||||
it('does not render EmailSettingsModal', () => {
|
||||
expect(el.instance.findByType(EmailSettingsModal).length).toEqual(0);
|
||||
});
|
||||
testUnenrollConfirmModal();
|
||||
});
|
||||
describe('show unenroll and enable email', () => {
|
||||
const hookProps = {
|
||||
shouldShowDropdown: true,
|
||||
isEmailEnabled: true,
|
||||
shouldShowUnenrollItem: true,
|
||||
};
|
||||
beforeEach(() => {
|
||||
mockHooks(hookProps);
|
||||
render();
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
testHandleToggle();
|
||||
testSocialShareMenu();
|
||||
describe('unenroll modal toggle', () => {
|
||||
let toggle;
|
||||
describe('not masquerading', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks(hookProps);
|
||||
render();
|
||||
[toggle] = el.instance.findByTestId(testIds.unenrollModalToggle);
|
||||
});
|
||||
it('renders unenroll modal toggle', () => {
|
||||
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(1);
|
||||
});
|
||||
test('onClick from unenroll modal hook', () => {
|
||||
expect(toggle.props.onClick).toEqual(unenrollData.show);
|
||||
});
|
||||
test('disabled', () => {
|
||||
expect(toggle.props.disabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('masquerading', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ ...hookProps, isMasquerading: true });
|
||||
render();
|
||||
[toggle] = el.instance.findByTestId(testIds.unenrollModalToggle);
|
||||
});
|
||||
it('renders', () => {
|
||||
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(1);
|
||||
});
|
||||
test('onClick from unenroll modal hook', () => {
|
||||
expect(toggle.props.onClick).toEqual(unenrollData.show);
|
||||
});
|
||||
test('disabled', () => {
|
||||
expect(toggle.props.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
testUnenrollConfirmModal();
|
||||
it('displays EmaiSettingsModal with cardId and emailSettingsModal data', () => {
|
||||
const modal = el.instance.findByType(EmailSettingsModal)[0];
|
||||
expect(modal.props.show).toEqual(emailSettings.isVisible);
|
||||
expect(modal.props.closeModal).toEqual(emailSettings.hide);
|
||||
expect(modal.props.cardId).toEqual(props.cardId);
|
||||
});
|
||||
});
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false, isEmailEnabled: false });
|
||||
wrapper = shallow(<CourseCardMenu {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('does not renders share buttons', () => {
|
||||
expect(wrapper.find('FacebookShareButton').length).toEqual(0);
|
||||
expect(wrapper.find('TwitterShareButton').length).toEqual(0);
|
||||
});
|
||||
it('does not render unenroll modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
|
||||
expect(el.length).toEqual(0);
|
||||
});
|
||||
it('does not render email settings modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
|
||||
expect(el.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe('masquerading', () => {
|
||||
beforeEach(() => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
||||
wrapper = shallow(<CourseCardMenu {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('renders share buttons', () => {
|
||||
expect(wrapper.find('FacebookShareButton').length).toEqual(1);
|
||||
el = wrapper.find('TwitterShareButton');
|
||||
expect(el.length).toEqual(1);
|
||||
expect(el.prop('url')).toEqual('twitter-share-url');
|
||||
});
|
||||
it('renders disabled unenroll modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
|
||||
expect(el.props().disabled).toEqual(true);
|
||||
});
|
||||
it('renders disabled email settings modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
|
||||
expect(el.props().disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,25 +3,33 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from './hooks';
|
||||
|
||||
const { courseTitleClicked } = track.course;
|
||||
|
||||
export const CourseCardTitle = ({ cardId }) => {
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const handleTitleClicked = reduxHooks.useTrackCourseEvent(courseTitleClicked, cardId, homeUrl);
|
||||
const handleTitleClicked = reduxHooks.useTrackCourseEvent(
|
||||
courseTitleClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
);
|
||||
const { disableCourseTitle } = useActionDisabledState(cardId);
|
||||
return (
|
||||
<h3>
|
||||
<a
|
||||
href={homeUrl}
|
||||
className="course-card-title"
|
||||
data-testid="CourseCardTitle"
|
||||
onClick={handleTitleClicked}
|
||||
disabled={isEntitlement && !isFulfilled}
|
||||
>
|
||||
{courseName}
|
||||
</a>
|
||||
{disableCourseTitle ? (
|
||||
<span className="course-card-title" data-testid="CourseCardTitle">{courseName}</span>
|
||||
) : (
|
||||
<a
|
||||
href={homeUrl}
|
||||
className="course-card-title"
|
||||
data-testid="CourseCardTitle"
|
||||
onClick={handleTitleClicked}
|
||||
>
|
||||
{courseName}
|
||||
</a>
|
||||
)}
|
||||
</h3>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from './hooks';
|
||||
import CourseCardTitle from './CourseCardTitle';
|
||||
|
||||
const homeUrl = 'home-url';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
courseTitleClicked: jest.fn().mockName('segment.courseTitleClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(() => ({ courseName: 'course-name' })),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
|
||||
trackCourseEvent: { eventName, cardId, upgradeUrl },
|
||||
})),
|
||||
},
|
||||
}));
|
||||
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
|
||||
|
||||
describe('CourseCardTitle', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('renders clickable link course title', () => {
|
||||
const wrapper = shallow(<CourseCardTitle {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const title = wrapper.find('.course-card-title');
|
||||
expect(title.type()).toBe('a');
|
||||
expect(title.prop('onClick')).toEqual(
|
||||
reduxHooks.useTrackCourseEvent(
|
||||
track.course.courseTitleClicked,
|
||||
props.cardId,
|
||||
homeUrl,
|
||||
),
|
||||
);
|
||||
});
|
||||
test('renders disabled link', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
|
||||
const wrapper = shallow(<CourseCardTitle {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const title = wrapper.find('.course-card-title');
|
||||
expect(title.type()).toBe('span');
|
||||
expect(title.prop('onClick')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes', () => {
|
||||
shallow(<CourseCardTitle {...props} />);
|
||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
|
||||
props.cardId,
|
||||
);
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,9 +17,8 @@ const cardId = 'test-card-id';
|
||||
const state = new MockUseState(hooks);
|
||||
const numPrograms = 27;
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
describe('RelatedProgramsBadge hooks', () => {
|
||||
const { formatMessage } = useIntl();
|
||||
let out;
|
||||
describe('state values', () => {
|
||||
state.testGetter(state.keys.isOpen);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
||||
<a
|
||||
className="pgn__card-wrapper-image-cap overflow-visible orientation"
|
||||
href="home-url"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.courseImageClicked],
|
||||
"upgradeUrl": "home-url",
|
||||
},
|
||||
}
|
||||
}
|
||||
tabIndex="-1"
|
||||
>
|
||||
<img
|
||||
alt="Course thumbnail"
|
||||
className="pgn__card-image-cap show"
|
||||
src="banner-img-src"
|
||||
/>
|
||||
<span
|
||||
className="course-card-verify-ribbon-container"
|
||||
title="You're enrolled as a verified student"
|
||||
>
|
||||
<Badge
|
||||
as="div"
|
||||
className="w-100"
|
||||
variant="success"
|
||||
>
|
||||
Verified
|
||||
</Badge>
|
||||
<img
|
||||
alt="ID Verified Ribbon/Badge"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
`;
|
||||
|
||||
exports[`CourseCardImage snapshot renders disabled link 1`] = `
|
||||
<div
|
||||
className="pgn__card-wrapper-image-cap overflow-visible orientation"
|
||||
>
|
||||
<img
|
||||
alt="Course thumbnail"
|
||||
className="pgn__card-image-cap show"
|
||||
src="banner-img-src"
|
||||
/>
|
||||
<span
|
||||
className="course-card-verify-ribbon-container"
|
||||
title="You're enrolled as a verified student"
|
||||
>
|
||||
<Badge
|
||||
as="div"
|
||||
className="w-100"
|
||||
variant="success"
|
||||
>
|
||||
Verified
|
||||
</Badge>
|
||||
<img
|
||||
alt="ID Verified Ribbon/Badge"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
|
||||
<h3>
|
||||
<a
|
||||
className="course-card-title"
|
||||
data-testid="CourseCardTitle"
|
||||
href="home-url"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.courseTitleClicked],
|
||||
"upgradeUrl": "home-url",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
course-name
|
||||
</a>
|
||||
</h3>
|
||||
`;
|
||||
|
||||
exports[`CourseCardTitle snapshot renders disabled link 1`] = `
|
||||
<h3>
|
||||
<span
|
||||
className="course-card-title"
|
||||
data-testid="CourseCardTitle"
|
||||
>
|
||||
course-name
|
||||
</span>
|
||||
</h3>
|
||||
`;
|
||||
32
src/containers/CourseCard/components/hooks.js
Normal file
32
src/containers/CourseCard/components/hooks.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
export const useActionDisabledState = (cardId) => {
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const {
|
||||
canUpgrade, hasAccess, isAudit, isAuditAccessExpired,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const {
|
||||
isEntitlement, isFulfilled, canChange, hasSessions,
|
||||
} = reduxHooks.useCardEntitlementData(cardId);
|
||||
|
||||
const { resumeUrl, homeUrl, upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
|
||||
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
|
||||
const disableSelectSession = !isEntitlement || isMasquerading || !hasAccess || (!canChange || !hasSessions);
|
||||
const disableUpgradeCourse = !upgradeUrl || (isMasquerading && !canUpgrade);
|
||||
|
||||
const disableCourseTitle = (isEntitlement && !isFulfilled) || disableViewCourse;
|
||||
|
||||
return {
|
||||
disableBeginCourse,
|
||||
disableResumeCourse,
|
||||
disableViewCourse,
|
||||
disableUpgradeCourse,
|
||||
disableSelectSession,
|
||||
disableCourseTitle,
|
||||
};
|
||||
};
|
||||
|
||||
export default useActionDisabledState;
|
||||
186
src/containers/CourseCard/components/hooks.test.js
Normal file
186
src/containers/CourseCard/components/hooks.test.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const cardId = 'my-test-course-number';
|
||||
|
||||
describe('useActionDisabledState', () => {
|
||||
const defaultData = {
|
||||
isMasquerading: false,
|
||||
canUpgrade: false,
|
||||
isEntitlement: false,
|
||||
isFulfilled: false,
|
||||
canChange: false,
|
||||
hasSessions: false,
|
||||
hasAccess: false,
|
||||
isAudit: false,
|
||||
isAuditAccessExpired: false,
|
||||
resumeUrl: 'resume.url',
|
||||
homeUrl: 'home.url',
|
||||
upgradeUrl: 'upgrade.url',
|
||||
};
|
||||
const mockHooksData = (args) => {
|
||||
const {
|
||||
isMasquerading,
|
||||
canUpgrade,
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
hasSessions,
|
||||
hasAccess,
|
||||
isAudit,
|
||||
isAuditAccessExpired,
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
upgradeUrl,
|
||||
} = { ...defaultData, ...args };
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
canUpgrade,
|
||||
hasAccess,
|
||||
isAudit,
|
||||
isAuditAccessExpired,
|
||||
});
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
hasSessions,
|
||||
});
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
upgradeUrl,
|
||||
});
|
||||
};
|
||||
|
||||
const runHook = () => hooks.useActionDisabledState(cardId);
|
||||
describe('disableBeginCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableBeginCourse).toBe(expected);
|
||||
};
|
||||
it('disable when homeUrl is invalid', () => {
|
||||
testDisabled({ homeUrl: null }, true);
|
||||
});
|
||||
it('disable when isMasquerading is true', () => {
|
||||
testDisabled({ isMasquerading: true }, true);
|
||||
});
|
||||
it('disable when hasAccess is false', () => {
|
||||
testDisabled({ hasAccess: false }, true);
|
||||
});
|
||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||
testDisabled({ isAudit: true, isAuditAccessExpired: true }, true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ hasAccess: true }, false);
|
||||
});
|
||||
});
|
||||
describe('disableResumeCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableResumeCourse).toBe(expected);
|
||||
};
|
||||
it('disable when resumeUrl is invalid', () => {
|
||||
testDisabled({ resumeUrl: null }, true);
|
||||
});
|
||||
it('disable when isMasquerading is true', () => {
|
||||
testDisabled({ isMasquerading: true }, true);
|
||||
});
|
||||
it('disable when hasAccess is false', () => {
|
||||
testDisabled({ hasAccess: false }, true);
|
||||
});
|
||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||
testDisabled({ isAudit: true, isAuditAccessExpired: true }, true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ hasAccess: true }, false);
|
||||
});
|
||||
});
|
||||
describe('disableViewCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableViewCourse).toBe(expected);
|
||||
};
|
||||
it('disable when hasAccess is false', () => {
|
||||
testDisabled({ hasAccess: false }, true);
|
||||
});
|
||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||
testDisabled({ isAudit: true, isAuditAccessExpired: true }, true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ hasAccess: true }, false);
|
||||
});
|
||||
});
|
||||
describe('disableUpgradeCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableUpgradeCourse).toBe(expected);
|
||||
};
|
||||
it('disable when upgradeUrl is invalid', () => {
|
||||
testDisabled({ upgradeUrl: null }, true);
|
||||
});
|
||||
it('disable when isMasquerading is true and canUpgrade is false', () => {
|
||||
testDisabled({ isMasquerading: true, canUpgrade: false }, true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ canUpgrade: true }, false);
|
||||
});
|
||||
});
|
||||
describe('disableSelectSession', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableSelectSession).toBe(expected);
|
||||
};
|
||||
it('disable when isEntitlement is false', () => {
|
||||
testDisabled({ isEntitlement: false }, true);
|
||||
});
|
||||
it('disable when isMasquerading is true', () => {
|
||||
testDisabled({ isMasquerading: true }, true);
|
||||
});
|
||||
it('disable when hasAccess is false', () => {
|
||||
testDisabled({ hasAccess: false }, true);
|
||||
});
|
||||
it('disable when canChange is false', () => {
|
||||
testDisabled({ canChange: false }, true);
|
||||
});
|
||||
it('disable when hasSessions is false', () => {
|
||||
testDisabled({ hasSessions: false }, true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled(
|
||||
{
|
||||
isEntitlement: true,
|
||||
hasAccess: true,
|
||||
canChange: true,
|
||||
hasSessions: true,
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('disableCourseTitle', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableCourseTitle).toBe(expected);
|
||||
};
|
||||
it('disable when isEntitlement is true and isFulfilled is false', () => {
|
||||
testDisabled({ isEntitlement: true, isFulfilled: false }, true);
|
||||
});
|
||||
it('disable when disableViewCourse is true', () => {
|
||||
testDisabled({ hasAccess: false }, true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ isEntitlement: true, isFulfilled: true, hasAccess: true }, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -68,7 +68,7 @@ export const CourseFilterControls = ({
|
||||
onClose={close}
|
||||
>
|
||||
<div className="p-1 mr-3">
|
||||
<b>Refine</b>
|
||||
<b>{formatMessage(messages.refine)}</b>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="filter-form-row">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export const messages = StrictDict({
|
||||
const messages = defineMessages({
|
||||
inProgress: {
|
||||
id: 'learner-dash.courseListFilters.inProgress',
|
||||
description: 'in-progress filter checkbox label for course list filters',
|
||||
@@ -52,4 +52,5 @@ export const messages = StrictDict({
|
||||
defaultMessage: 'Refine',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -17,7 +17,7 @@ exports[`NoCoursesView snapshot 1`] = `
|
||||
</p>
|
||||
<Button
|
||||
as="a"
|
||||
href="course-search-url"
|
||||
href="http://localhost:18000/course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
variant="brand"
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Image } from '@edx/paragon';
|
||||
import { Search } from '@edx/paragon/icons';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import emptyCourseSVG from 'assets/empty-course.svg';
|
||||
import { reduxHooks } from 'hooks';
|
||||
@@ -27,7 +28,7 @@ export const NoCoursesView = () => {
|
||||
<Button
|
||||
variant="brand"
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
href={baseAppUrl(courseSearchUrl)}
|
||||
iconBefore={Search}
|
||||
>
|
||||
{formatMessage(messages.exploreCoursesButton)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import EmptyCourse from '.';
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: 'course-search-url',
|
||||
courseSearchUrl: '/course-search-url',
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -3,12 +3,19 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { Container, Col, Row } from '@edx/paragon';
|
||||
|
||||
import WidgetFooter from 'containers/WidgetContainers/WidgetFooter';
|
||||
import hooks from './hooks';
|
||||
|
||||
export const columnConfig = {
|
||||
courseList: {
|
||||
lg: { span: 12, offset: 0 },
|
||||
xl: { span: 8, offset: 0 },
|
||||
withSidebar: {
|
||||
lg: { span: 12, offset: 0 },
|
||||
xl: { span: 8, offset: 0 },
|
||||
},
|
||||
noSidebar: {
|
||||
lg: { span: 12, offset: 0 },
|
||||
xl: { span: 12, offset: 0 },
|
||||
},
|
||||
},
|
||||
sidebar: {
|
||||
lg: { span: 12, offset: 0 },
|
||||
@@ -16,18 +23,31 @@ export const columnConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export const DashboardLayout = ({ children, sidebar }) => {
|
||||
const isCollapsed = hooks.useIsDashboardCollapsed();
|
||||
export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
|
||||
const {
|
||||
isCollapsed,
|
||||
sidebarShowing,
|
||||
setSidebarShowing,
|
||||
} = hooks.useDashboardLayoutData();
|
||||
|
||||
const courseListColumnProps = sidebarShowing
|
||||
? columnConfig.courseList.withSidebar
|
||||
: columnConfig.courseList.noSidebar;
|
||||
|
||||
return (
|
||||
<Container fluid size="xl">
|
||||
<Row>
|
||||
<Col {...columnConfig.courseList} className="course-list-column">
|
||||
<Col {...courseListColumnProps} className="course-list-column">
|
||||
{children}
|
||||
</Col>
|
||||
<Col {...columnConfig.sidebar} className="sidebar-column">
|
||||
{!isCollapsed && (<h2 className="course-list-title"> </h2>)}
|
||||
{sidebar}
|
||||
<Sidebar setSidebarShowing={setSidebarShowing} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -35,7 +55,7 @@ export const DashboardLayout = ({ children, sidebar }) => {
|
||||
};
|
||||
DashboardLayout.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
sidebar: PropTypes.node.isRequired,
|
||||
sidebar: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DashboardLayout;
|
||||
|
||||
@@ -1,60 +1,125 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Col, Row } from '@edx/paragon';
|
||||
|
||||
import WidgetFooter from 'containers/WidgetContainers/WidgetFooter';
|
||||
import hooks from './hooks';
|
||||
import DashboardLayout, { columnConfig } from './DashboardLayout';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useIsDashboardCollapsed: jest.fn(() => true),
|
||||
useDashboardLayoutData: jest.fn(),
|
||||
}));
|
||||
|
||||
const hookProps = {
|
||||
isCollapsed: true,
|
||||
sidebarShowing: false,
|
||||
setSidebarShowing: jest.fn().mockName('hooks.setSidebarShowing'),
|
||||
};
|
||||
hooks.useDashboardLayoutData.mockReturnValue(hookProps);
|
||||
|
||||
const props = {
|
||||
sidebar: jest.fn(() => 'test-sidebar-content'),
|
||||
};
|
||||
|
||||
const children = 'test-children';
|
||||
|
||||
let el;
|
||||
describe('DashboardLayout', () => {
|
||||
const children = 'test-children';
|
||||
const props = {
|
||||
sidebar: 'test-sidebar-content',
|
||||
};
|
||||
const render = () => shallow(<DashboardLayout sidebar={props.sidebar}>{children}</DashboardLayout>);
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<DashboardLayout {...props}>{children}</DashboardLayout>);
|
||||
});
|
||||
|
||||
const testColumns = () => {
|
||||
it('loads courseList and sidebar column layout', () => {
|
||||
const columns = render().find(Row).find(Col);
|
||||
Object.keys(columnConfig.courseList).forEach(size => {
|
||||
expect(columns.at(0).props()[size]).toEqual(columnConfig.courseList[size]);
|
||||
});
|
||||
const columns = el.find(Row).find(Col);
|
||||
Object.keys(columnConfig.sidebar).forEach(size => {
|
||||
expect(columns.at(1).props()[size]).toEqual(columnConfig.sidebar[size]);
|
||||
});
|
||||
});
|
||||
it('displays children in first column', () => {
|
||||
const columns = render().find(Row).find(Col);
|
||||
const columns = el.find(Row).find(Col);
|
||||
expect(columns.at(0).contains(children)).toEqual(true);
|
||||
});
|
||||
it('displays sidebar prop in second column', () => {
|
||||
const columns = render().find(Row).find(Col);
|
||||
expect(columns.at(1).contains(props.sidebar)).toEqual(true);
|
||||
const columns = el.find(Row).find(Col);
|
||||
expect(columns.at(1).find(props.sidebar)).toHaveLength(1);
|
||||
});
|
||||
it('displays a footer in the second row', () => {
|
||||
const columns = el.find(Row).at(1).find(Col);
|
||||
expect(columns.at(0).containsMatchingElement(<WidgetFooter />)).toBeTruthy();
|
||||
});
|
||||
};
|
||||
const testSidebarLayout = () => {
|
||||
it('displays widthSidebar width for course list column', () => {
|
||||
const columns = el.find(Row).find(Col);
|
||||
Object.keys(columnConfig.courseList.withSidebar).forEach(size => {
|
||||
expect(columns.at(0).props()[size]).toEqual(columnConfig.courseList.withSidebar[size]);
|
||||
});
|
||||
});
|
||||
};
|
||||
const testNoSidebarLayout = () => {
|
||||
it('displays noSidebar width for course list column', () => {
|
||||
const columns = el.find(Row).find(Col);
|
||||
Object.keys(columnConfig.courseList.noSidebar).forEach(size => {
|
||||
expect(columns.at(0).props()[size]).toEqual(columnConfig.courseList.noSidebar[size]);
|
||||
});
|
||||
});
|
||||
};
|
||||
const testSnapshot = () => {
|
||||
test('snapshot', () => {
|
||||
expect(render()).toMatchSnapshot();
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
};
|
||||
describe('collapsed', () => {
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
describe('sidebar showing', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useDashboardLayoutData.mockReturnValueOnce({ ...hookProps, sidebarShowing: true });
|
||||
});
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
testSidebarLayout();
|
||||
});
|
||||
describe('sidebar not showing', () => {
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
testNoSidebarLayout();
|
||||
});
|
||||
it('does not show spacer component above widget sidebar', () => {
|
||||
const columns = render().find(Col);
|
||||
const columns = el.find(Col);
|
||||
expect(columns.at(1).find('h2').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('not collapsed', () => {
|
||||
beforeEach(() => { hooks.useIsDashboardCollapsed.mockReturnValueOnce(false); });
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
it('shows a blank (nbsp) h2 spacer component above widget sidebar', () => {
|
||||
const columns = render().find(Col);
|
||||
// nonbreaking space equivalent
|
||||
expect(columns.at(1).find('h2').text()).toEqual('\xA0');
|
||||
const testWidgetSpacing = () => {
|
||||
it('shows a blank (nbsp) h2 spacer component above widget sidebar', () => {
|
||||
const columns = el.find(Col);
|
||||
// nonbreaking space equivalent
|
||||
expect(columns.at(1).find('h2').text()).toEqual('\xA0');
|
||||
});
|
||||
};
|
||||
describe('sidebar showing', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useDashboardLayoutData.mockReturnValueOnce({
|
||||
...hookProps,
|
||||
isCollapsed: false,
|
||||
sidebarShowing: true,
|
||||
});
|
||||
});
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
testSidebarLayout();
|
||||
testWidgetSpacing();
|
||||
});
|
||||
describe('sidebar not showing', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useDashboardLayoutData.mockReturnValueOnce({ ...hookProps, isCollapsed: false });
|
||||
});
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
testNoSidebarLayout();
|
||||
testWidgetSpacing();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,57 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DashboardLayout collapsed snapshot 1`] = `
|
||||
exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
|
||||
<Container
|
||||
fluid={true}
|
||||
size="xl"
|
||||
>
|
||||
<Row>
|
||||
<Col
|
||||
className="course-list-column"
|
||||
lg={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
test-children
|
||||
</Col>
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
lg={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
|
||||
<Container
|
||||
fluid={true}
|
||||
size="xl"
|
||||
@@ -38,13 +89,76 @@ exports[`DashboardLayout collapsed snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
test-sidebar-content
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`DashboardLayout not collapsed snapshot 1`] = `
|
||||
exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
|
||||
<Container
|
||||
fluid={true}
|
||||
size="xl"
|
||||
>
|
||||
<Row>
|
||||
<Col
|
||||
className="course-list-column"
|
||||
lg={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
test-children
|
||||
</Col>
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
lg={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<h2
|
||||
className="course-list-title"
|
||||
>
|
||||
|
||||
</h2>
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
||||
<Container
|
||||
fluid={true}
|
||||
size="xl"
|
||||
@@ -87,7 +201,14 @@ exports[`DashboardLayout not collapsed snapshot 1`] = `
|
||||
>
|
||||
|
||||
</h2>
|
||||
test-sidebar-content
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -15,7 +15,7 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
|
||||
id="dashboard-content"
|
||||
>
|
||||
<DashboardLayout
|
||||
sidebar={<LoadedWidgetSidebar />}
|
||||
sidebar="LoadedWidgetSidebar"
|
||||
>
|
||||
<CourseList />
|
||||
</DashboardLayout>
|
||||
@@ -56,7 +56,7 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
|
||||
id="dashboard-content"
|
||||
>
|
||||
<DashboardLayout
|
||||
sidebar={<NoCoursesWidgetSidebar />}
|
||||
sidebar="NoCoursesWidgetSidebar"
|
||||
>
|
||||
<CourseList />
|
||||
</DashboardLayout>
|
||||
|
||||
@@ -2,13 +2,14 @@ import React from 'react';
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { apiHooks } from 'hooks';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import appMessages from 'messages';
|
||||
import * as module from './hooks';
|
||||
|
||||
export const useIsDashboardCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
return width < breakpoints.large.maxWidth;
|
||||
};
|
||||
export const state = StrictDict({
|
||||
sidebarShowing: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useInitializeDashboard = () => {
|
||||
const initialize = apiHooks.useInitializeApp();
|
||||
@@ -23,8 +24,18 @@ export const useDashboardMessages = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useDashboardLayoutData = () => {
|
||||
const { width } = useWindowSize();
|
||||
const [sidebarShowing, setSidebarShowing] = module.state.sidebarShowing(false);
|
||||
return {
|
||||
isDashboardCollapsed: width < breakpoints.large.maxWidth,
|
||||
sidebarShowing,
|
||||
setSidebarShowing,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
useIsDashboardCollapsed,
|
||||
useDashboardLayoutData,
|
||||
useInitializeDashboard,
|
||||
useDashboardMessages,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
|
||||
import { apiHooks } from 'hooks';
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import appMessages from 'messages';
|
||||
import * as hooks from './hooks';
|
||||
@@ -19,8 +20,12 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const initializeApp = jest.fn();
|
||||
apiHooks.useInitializeApp.mockReturnValue(initializeApp);
|
||||
useWindowSize.mockReturnValue({ width: 20 });
|
||||
breakpoints.large = { maxWidth: 30 };
|
||||
describe('CourseCard hooks', () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
@@ -28,15 +33,32 @@ describe('CourseCard hooks', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useIsDashboardCollapsed', () => {
|
||||
it('returns true iff windowSize width is below the xl breakpoint', () => {
|
||||
useWindowSize.mockReturnValueOnce({ width: 20 });
|
||||
breakpoints.large = { maxWidth: 30 };
|
||||
expect(hooks.useIsDashboardCollapsed()).toEqual(true);
|
||||
useWindowSize.mockReturnValueOnce({ width: 40 });
|
||||
expect(hooks.useIsDashboardCollapsed()).toEqual(false);
|
||||
useWindowSize.mockReturnValueOnce({ width: 40 });
|
||||
expect(hooks.useIsDashboardCollapsed()).toEqual(false);
|
||||
describe('state fields', () => {
|
||||
state.testGetter(state.keys.sidebarShowing);
|
||||
});
|
||||
|
||||
describe('useDashboardLayoutData', () => {
|
||||
beforeEach(() => { state.mock(); });
|
||||
describe('behavior', () => {
|
||||
it('initializes sidebarShowing to default false value', () => {
|
||||
hooks.useDashboardLayoutData();
|
||||
state.expectInitializedWith(state.keys.sidebarShowing, false);
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('isDashboardCollapsed', () => {
|
||||
it('returns true iff windowSize width is below the xl breakpoint', () => {
|
||||
expect(hooks.useDashboardLayoutData().isDashboardCollapsed).toEqual(true);
|
||||
useWindowSize.mockReturnValueOnce({ width: 40 });
|
||||
expect(hooks.useDashboardLayoutData().isDashboardCollapsed).toEqual(false);
|
||||
});
|
||||
});
|
||||
it('forwards sidebarShowing and setSidebarShowing from state hook', () => {
|
||||
const hook = hooks.useDashboardLayoutData();
|
||||
const { sidebarShowing, setSidebarShowing } = hook;
|
||||
expect(sidebarShowing).toEqual(state.stateVals.sidebarShowing);
|
||||
expect(setSidebarShowing).toEqual(state.setState.sidebarShowing);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('useInitializeDashboard', () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ export const Dashboard = () => {
|
||||
const hasAvailableDashboards = reduxHooks.useHasAvailableDashboards();
|
||||
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
|
||||
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
|
||||
|
||||
return (
|
||||
<div id="dashboard-container" className="d-flex flex-column p-2 pt-0">
|
||||
<h1 className="sr-only">{pageTitle}</h1>
|
||||
@@ -34,7 +35,7 @@ export const Dashboard = () => {
|
||||
{initIsPending
|
||||
? (<LoadingView />)
|
||||
: (
|
||||
<DashboardLayout sidebar={hasCourses ? <LoadedSidebar /> : <NoCoursesSidebar />}>
|
||||
<DashboardLayout sidebar={hasCourses ? LoadedSidebar : NoCoursesSidebar}>
|
||||
<CourseList />
|
||||
</DashboardLayout>
|
||||
)}
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('Dashboard', () => {
|
||||
showSelectSessionModal: true,
|
||||
},
|
||||
content: ['LoadedView', (
|
||||
<DashboardLayout sidebar={<LoadedWidgetSidebar />}><CourseList /></DashboardLayout>
|
||||
<DashboardLayout sidebar={LoadedWidgetSidebar}><CourseList /></DashboardLayout>
|
||||
)],
|
||||
showEnterpriseModal: false,
|
||||
showSelectSessionModal: true,
|
||||
@@ -132,7 +132,7 @@ describe('Dashboard', () => {
|
||||
showSelectSessionModal: false,
|
||||
},
|
||||
content: ['Dashboard layout with no courses sidebar and content', (
|
||||
<DashboardLayout sidebar={<NoCoursesWidgetSidebar />}><CourseList /></DashboardLayout>
|
||||
<DashboardLayout sidebar={NoCoursesWidgetSidebar}><CourseList /></DashboardLayout>
|
||||
)],
|
||||
showEnterpriseModal: true,
|
||||
showSelectSessionModal: false,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user