Compare commits
180 Commits
open-relea
...
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 | ||
|
|
3c7be4c65c | ||
|
|
7ccf049edb | ||
|
|
49d70dda93 | ||
|
|
6b225cbf86 | ||
|
|
07874fecf7 | ||
|
|
b79c0d1434 | ||
|
|
63ab012bcb | ||
|
|
f25a15d917 | ||
|
|
ff631a21c2 | ||
|
|
3f801caf38 | ||
|
|
38b0d5832f | ||
|
|
33c50082ef | ||
|
|
cba982a7a0 | ||
|
|
55de93ef59 | ||
|
|
0933d185af | ||
|
|
a13085a6a1 | ||
|
|
dc3141cc65 | ||
|
|
b2e8621e5c | ||
|
|
82268b4f37 | ||
|
|
044bf0f45a | ||
|
|
166c64a391 | ||
|
|
a21698e96a | ||
|
|
41f563dd9a | ||
|
|
81f1282cb0 | ||
|
|
875ecdbdb0 | ||
|
|
a3106928e1 | ||
|
|
3b32a5cd16 | ||
|
|
b23e741a5e | ||
|
|
06bd224a20 | ||
|
|
6003099a73 | ||
|
|
03e42f45bb | ||
|
|
d878358984 | ||
|
|
61bf5f8685 | ||
|
|
f435e8d2c2 | ||
|
|
496abc5bfb | ||
|
|
b6c8f7e3b2 | ||
|
|
0c0aed00de | ||
|
|
bbe9b7bb80 | ||
|
|
821e6bce50 | ||
|
|
82219e9b08 | ||
|
|
9a57f9de13 | ||
|
|
15afb3645f | ||
|
|
2932693cda | ||
|
|
552614466a | ||
|
|
25383e3913 | ||
|
|
fbf812f75c | ||
|
|
1ed4bac475 | ||
|
|
48b157fdd0 | ||
|
|
e92a571cac | ||
|
|
2a72a85efd | ||
|
|
dde8d45df3 | ||
|
|
b8245d6631 | ||
|
|
1129ff2847 | ||
|
|
c6432864ab | ||
|
|
c8b729a65d | ||
|
|
0badf690a6 | ||
|
|
41df13b059 | ||
|
|
254ccfccb6 | ||
|
|
3973d173cf | ||
|
|
61e484af1f | ||
|
|
f8b181e8c9 | ||
|
|
115ef77e37 | ||
|
|
57eac99b42 | ||
|
|
7174f8de1e | ||
|
|
c16390e157 | ||
|
|
a0c0384a3d | ||
|
|
e4900351f8 | ||
|
|
0f4e2e28dd | ||
|
|
4b38aaa199 | ||
|
|
9abcf35100 | ||
|
|
66b7a97450 |
14
.env
14
.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=''
|
||||
@@ -30,4 +31,15 @@ ENTERPRISE_MARKETING_URL=''
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
LEARNING_MICROFRONTEND_URL=''
|
||||
LEARNING_BASE_URL=''
|
||||
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
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
NODE_ENV='development'
|
||||
PORT=1993
|
||||
BASE_URL='localhost:1993'
|
||||
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
|
||||
@@ -36,4 +37,15 @@ ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
LEARNING_MICROFRONTEND_URL='http://localhost:2000'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
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
|
||||
|
||||
18
.env.test
18
.env.test
@@ -1,7 +1,8 @@
|
||||
NODE_ENV='test'
|
||||
PORT=1993
|
||||
BASE_URL='localhost:1993'
|
||||
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
|
||||
@@ -36,4 +37,15 @@ ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
LEARNING_MICROFRONTEND_URL='http://localhost:2000'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
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': ['*'] } }],
|
||||
},
|
||||
});
|
||||
|
||||
8
.github/CODEOWNERS
vendored
Normal file
8
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Root app is developed and owned by Aurora
|
||||
* @edx/content-aurora
|
||||
|
||||
# WIDGETS and experiments are developed and owned by separate teams below
|
||||
|
||||
# Recommendations panel
|
||||
/src/widgets/RecommendationsPanel @edx/vanguards
|
||||
/src/widgets/LookingForChallengeWidget @edx/vanguards
|
||||
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Run the workflow that adds new tickets that are either:
|
||||
# - labelled "DEPR"
|
||||
# - title starts with "[DEPR]"
|
||||
# - body starts with "Proposal Date" (this is the first template field)
|
||||
# to the org-wide DEPR project board
|
||||
|
||||
name: Add newly created DEPR issues to the DEPR project board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
routeissue:
|
||||
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "label: " it tries to apply
|
||||
# the label indicated in rest of comment.
|
||||
# If the comment starts with "remove label: ", it tries
|
||||
# to remove the indicated label.
|
||||
# Note: Labels are allowed to have spaces and this script does
|
||||
# not parse spaces (as often a space is legitimate), so the command
|
||||
# "label: really long lots of words label" will apply the
|
||||
# label "really long lots of words label"
|
||||
|
||||
name: Allows for the adding and removing of labels via comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
add_remove_labels:
|
||||
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||
|
||||
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@@ -11,17 +11,17 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14, 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
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
@@ -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/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -7,4 +7,4 @@ on:
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
||||
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||
|
||||
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
|
||||
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "assign me" it assigns the author to the
|
||||
# ticket (case insensitive)
|
||||
|
||||
name: Assign comment author to ticket if they say "assign me"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
self_assign_by_comment:
|
||||
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||
34
Makefile
34
Makefile
@@ -1,20 +1,18 @@
|
||||
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 is-es5
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
.PHONY: test
|
||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||
@@ -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 -f --mode reviewed --languages=$(transifex_langs)
|
||||
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:
|
||||
|
||||
65
README.md
Normal file
65
README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# frontend-app-learner-dashboard
|
||||
|
||||
The Learner Home app is a microfrontend (MFE) course listing experience for the Open edX Learning Management System (LMS). This experience was designed to provide a clean and functional interface to allow learners to view all of their open enrollments, as well as take relevant actions on those enrollments. It also serves as host to a number of exposed "widget" containers to provide upsell and discovery widgets as sidebar/footer components.
|
||||
|
||||
## Quickstart
|
||||
|
||||
To start the MFE and enable the feature in LMS:
|
||||
|
||||
1. Start the MFE with `npm run start`. Take a note of the path/port (defaults to `http://localhost:1996`).
|
||||
|
||||
From there, simply load the configured address/port. You should be prompted to log into your LMS if you are not already, and then redirected to your home page.
|
||||
|
||||
## Contributing
|
||||
|
||||
A core goal of this app is to provide a clean experimentation interface. To promote this end, we have provided a silo'ed code directory at `src/widgets` in which contributors should add their custom widget components. In order to ensure our ability to maintain the code stability of the app, the code for these widgets should be strictly contained within the bounds of that directory.
|
||||
|
||||
Once written, the widgets can be configured into one of our widget containers at `src/containers/WidgetContainers`. This can include conditional logic, as well as Optimizely triggers. It is important to note that our integration tests will isolate and ignore these containers, and thus testing your widget is the response of the creator/maintainer of the widget itself.
|
||||
|
||||
Some guidelines for writing widgets:
|
||||
* Code for the widget should be strictly confined to the `src/widgets` directory.
|
||||
* 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.
|
||||
42351
package-lock.json
generated
42351
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@@ -1,16 +1,14 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-learner-dash",
|
||||
"name": "@edx/frontend-app-learner-dashboard",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-learner-dash.git"
|
||||
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"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",
|
||||
@@ -18,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",
|
||||
@@ -27,15 +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.3.1",
|
||||
"@edx/frontend-platform": "3.0.0",
|
||||
"@edx/paragon": "20.12.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",
|
||||
@@ -52,18 +54,20 @@
|
||||
"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": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-share": "^4.4.0",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.1.1",
|
||||
"redux-beacon": "^2.1.0",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
@@ -71,25 +75,25 @@
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"reselect": "^4.0.0",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"util": "^0.12.4",
|
||||
"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",
|
||||
"codecov": "^3.8.3",
|
||||
"enzyme-adapter-react-16": "^1.15.6",
|
||||
"es-check": "^6.0.0",
|
||||
"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: /
|
||||
111
src/App.jsx
111
src/App.jsx
@@ -1,47 +1,118 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
||||
|
||||
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import { thunkActions } from 'data/redux';
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import store from 'data/store';
|
||||
import {
|
||||
selectors,
|
||||
actions,
|
||||
} from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import ZendeskFab from 'components/ZendeskFab';
|
||||
import { ExperimentProvider } from 'ExperimentContext';
|
||||
|
||||
import track from 'tracking';
|
||||
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
|
||||
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 = () => {
|
||||
const dispatch = useDispatch();
|
||||
// TODO: made development-only
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
const { formatMessage } = useIntl();
|
||||
const isFailed = {
|
||||
initialize: reduxHooks.useRequestIsFailed(RequestKeys.initialize),
|
||||
refreshList: reduxHooks.useRequestIsFailed(RequestKeys.refreshList),
|
||||
};
|
||||
const hasNetworkFailure = isFailed.initialize || isFailed.refreshList;
|
||||
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 = () => {
|
||||
dispatch(thunkActions.app.loadData({ ...fakeData.globalData, courses: [] }));
|
||||
loadData({ ...fakeData.globalData, courses: [] });
|
||||
};
|
||||
window.loadMockData = () => {
|
||||
dispatch(thunkActions.app.loadData({
|
||||
loadData({
|
||||
...fakeData.globalData,
|
||||
courses: [
|
||||
...fakeData.courseRunData,
|
||||
...fakeData.entitlementData,
|
||||
],
|
||||
}));
|
||||
});
|
||||
};
|
||||
window.store = store;
|
||||
window.selectors = selectors;
|
||||
window.actions = actions;
|
||||
window.track = track;
|
||||
}
|
||||
});
|
||||
if (getConfig().HOTJAR_APP_ID) {
|
||||
try {
|
||||
initializeHotjar({
|
||||
hotjarId: getConfig().HOTJAR_APP_ID,
|
||||
hotjarVersion: getConfig().HOTJAR_VERSION,
|
||||
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}
|
||||
}, [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>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,13 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
|
||||
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
|
||||
.alert .alert-icon {
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.alert.alert-info .alert-icon {
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
166
src/App.test.jsx
166
src/App.test.jsx
@@ -1,41 +1,169 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
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/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',
|
||||
thunkActions: 'redux.thunkActions',
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useRequestIsFailed: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
useLoadData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
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);
|
||||
|
||||
let el;
|
||||
let router;
|
||||
|
||||
const supportEmail = 'test-support-url';
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
||||
|
||||
describe('App router component', () => {
|
||||
test('snapshot: enabled', () => {
|
||||
expect(shallow(<App />)).toMatchSnapshot();
|
||||
});
|
||||
const { formatMessage } = useIntl();
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.find(BrowserRouter);
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('Routing - ListView is only route', () => {
|
||||
expect(router.find('main')).toEqual(shallow(
|
||||
<main><Dashboard /></main>,
|
||||
));
|
||||
const runBasicTests = () => {
|
||||
test('snapshot', () => { expect(el.snapshot).toMatchSnapshot(); });
|
||||
it('displays title in helmet component', () => {
|
||||
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.instance.findByType(LearnerDashboardHeader).length).toEqual(1);
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
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', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(router.find(Footer).props().logo).toEqual(logo);
|
||||
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', () => {
|
||||
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', () => {
|
||||
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,15 +1,170 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot: enabled 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Dashboard />
|
||||
</main>
|
||||
<Footer
|
||||
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
|
||||
exports[`App router component component initialize failure snapshot 1`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<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`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</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 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`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<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>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -7,17 +7,34 @@ exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPag
|
||||
`;
|
||||
|
||||
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",
|
||||
}
|
||||
>
|
||||
<App />
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
}
|
||||
wrapWithRouter={true}
|
||||
>
|
||||
<NoticesWrapper>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<App />
|
||||
</PageWrap>
|
||||
}
|
||||
path="/"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<Navigate
|
||||
replace={true}
|
||||
to="/"
|
||||
/>
|
||||
}
|
||||
path="*"
|
||||
/>
|
||||
</Routes>
|
||||
</NoticesWrapper>
|
||||
</AppProvider>
|
||||
`;
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 45 KiB |
BIN
src/assets/verified-ribbon.png
Normal file
BIN
src/assets/verified-ribbon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 498 B |
@@ -4,19 +4,23 @@ import PropTypes from 'prop-types';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
|
||||
export const Banner = ({ children, variant, icon }) => (
|
||||
<Alert variant={variant} className="mb-0" icon={icon}>
|
||||
export const Banner = ({
|
||||
children, variant, icon, className,
|
||||
}) => (
|
||||
<Alert variant={variant} className={className} icon={icon}>
|
||||
{children}
|
||||
</Alert>
|
||||
);
|
||||
Banner.defaultProps = {
|
||||
icon: Info,
|
||||
variant: 'info',
|
||||
className: 'mb-0',
|
||||
};
|
||||
Banner.propTypes = {
|
||||
variant: PropTypes.string,
|
||||
icon: PropTypes.func,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Banner;
|
||||
|
||||
@@ -19,5 +19,9 @@ describe('Banner', () => {
|
||||
|
||||
expect(wrapper.find(Alert).prop('variant')).toEqual('success');
|
||||
});
|
||||
test('renders with custom class', () => {
|
||||
const wrapper = shallow(<Banner {...props} className="custom-class" />);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
65
src/components/ZendeskFab/__snapshots__/index.test.jsx.snap
Normal file
65
src/components/ZendeskFab/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,65 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ZendeskFab snapshot 1`] = `
|
||||
<Zendesk
|
||||
cookies={true}
|
||||
defer={true}
|
||||
webWidget={
|
||||
Object {
|
||||
"answerBot": Object {
|
||||
"avatar": Object {
|
||||
"name": Object {
|
||||
"*": "edX Support",
|
||||
},
|
||||
"url": "https://edx-cdn.org/v3/prod/favicon.ico",
|
||||
},
|
||||
"contactOnlyAfterQuery": true,
|
||||
"suppress": false,
|
||||
"title": Object {
|
||||
"*": "edX Support",
|
||||
},
|
||||
},
|
||||
"chat": Object {
|
||||
"departments": Object {
|
||||
"enabled": Array [
|
||||
"account settings",
|
||||
"billing and payments",
|
||||
"certificates",
|
||||
"deadlines",
|
||||
"errors and technical issues",
|
||||
"other",
|
||||
"proctoring",
|
||||
],
|
||||
},
|
||||
"suppress": false,
|
||||
},
|
||||
"contactForm": Object {
|
||||
"attachments": true,
|
||||
"selectTicketForm": Object {
|
||||
"*": "Please choose your request type:",
|
||||
},
|
||||
"ticketForms": Array [
|
||||
Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"id": "description",
|
||||
"prefill": Object {
|
||||
"*": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": 360003368814,
|
||||
"subject": false,
|
||||
},
|
||||
],
|
||||
},
|
||||
"contactOptions": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"helpCenter": Object {
|
||||
"originalArticleButton": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
56
src/components/ZendeskFab/index.jsx
Normal file
56
src/components/ZendeskFab/index.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Zendesk from 'react-zendesk';
|
||||
import messages from './messages';
|
||||
|
||||
const ZendeskFab = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const setting = {
|
||||
cookies: true,
|
||||
webWidget: {
|
||||
contactOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
chat: {
|
||||
suppress: false,
|
||||
departments: {
|
||||
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
|
||||
},
|
||||
},
|
||||
contactForm: {
|
||||
ticketForms: [
|
||||
{
|
||||
id: 360003368814,
|
||||
subject: false,
|
||||
fields: [{ id: 'description', prefill: { '*': '' } }],
|
||||
},
|
||||
],
|
||||
selectTicketForm: {
|
||||
'*': formatMessage(messages.selectTicketForm),
|
||||
},
|
||||
attachments: true,
|
||||
},
|
||||
helpCenter: {
|
||||
originalArticleButton: true,
|
||||
},
|
||||
answerBot: {
|
||||
suppress: false,
|
||||
contactOnlyAfterQuery: true,
|
||||
title: { '*': formatMessage(messages.supportTitle) },
|
||||
avatar: {
|
||||
url: 'https://edx-cdn.org/v3/prod/favicon.ico',
|
||||
name: { '*': formatMessage(messages.supportTitle) },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ZendeskFab;
|
||||
12
src/components/ZendeskFab/index.test.jsx
Normal file
12
src/components/ZendeskFab/index.test.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import ZendeskFab from '.';
|
||||
|
||||
jest.mock('react-zendesk', () => 'Zendesk');
|
||||
|
||||
describe('ZendeskFab', () => {
|
||||
test('snapshot', () => {
|
||||
const wrapper = shallow(<ZendeskFab />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
16
src/components/ZendeskFab/messages.js
Normal file
16
src/components/ZendeskFab/messages.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export const messages = StrictDict({
|
||||
supportTitle: {
|
||||
id: 'zendesk.supportTitle',
|
||||
description: 'Title for the support button',
|
||||
defaultMessage: 'edX Support',
|
||||
},
|
||||
selectTicketForm: {
|
||||
id: 'zendesk.selectTicketForm',
|
||||
description: 'Select ticket form',
|
||||
defaultMessage: 'Please choose your request type:',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -10,6 +10,16 @@ exports[`Banner snapshot renders default banner 1`] = `
|
||||
</Alert>
|
||||
`;
|
||||
|
||||
exports[`Banner snapshot renders with custom class 1`] = `
|
||||
<Alert
|
||||
className="custom-class"
|
||||
icon={[MockFunction icons.Info]}
|
||||
variant="info"
|
||||
>
|
||||
Hello, world!
|
||||
</Alert>
|
||||
`;
|
||||
|
||||
exports[`Banner snapshot renders with variants 1`] = `
|
||||
<Alert
|
||||
className="mb-0"
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
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,
|
||||
// REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
|
||||
// DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
|
||||
// SECURE_COOKIES: process.env.NODE_ENV !== 'development',
|
||||
// SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
LEARNING_MICROFRONTEND_URL: process.env.LEARNING_MICROFRONTEND_URL,
|
||||
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
||||
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 = {};
|
||||
|
||||
@@ -2,20 +2,74 @@
|
||||
|
||||
.course-card {
|
||||
.card {
|
||||
.pgn__card-wrapper-image-cap.vertical {
|
||||
display: flex;
|
||||
min-height: $card-image-vertical-max-height;
|
||||
}
|
||||
.pgn__card-image-cap {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.overflow-visible {
|
||||
overflow: visible;
|
||||
}
|
||||
.pgn__card-header-content {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.pgn__card-footer {
|
||||
flex-wrap: nowrap;
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pgn__action-row {
|
||||
align-self: flex-end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.course-card-verify-ribbon-container {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
|
||||
.badge {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
> img {
|
||||
width: 40px;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-card-banners {
|
||||
> .alert {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: map-get($spacers, 3) map-get($spacers, 4);
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-left-radius: $alert-border-radius;
|
||||
border-bottom-right-radius: $alert-border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
.related-programs-banner {
|
||||
.related-programs-list-container {
|
||||
list-style: none;
|
||||
display: inline;
|
||||
|
||||
> li {
|
||||
line-height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,42 @@ exports[`CourseCard component snapshot: collapsed 1`] = `
|
||||
className="d-flex flex-column w-100"
|
||||
>
|
||||
<div>
|
||||
<CourseCardContent
|
||||
cardId="test-card-id"
|
||||
orientation="vertical"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="course-card-banners"
|
||||
data-testid="CourseCardBanners"
|
||||
>
|
||||
<CourseCardBanners
|
||||
<CourseCardImage
|
||||
cardId="test-card-id"
|
||||
orientation="horizontal"
|
||||
/>
|
||||
<Card.Body>
|
||||
<Card.Header
|
||||
actions={
|
||||
<CourseCardMenu
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<CourseCardTitle
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card.Section
|
||||
className="pt-0"
|
||||
>
|
||||
<CourseCardDetails
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Card.Footer
|
||||
orientation="vertical"
|
||||
>
|
||||
<CourseCardActions
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
</div>
|
||||
<CourseCardBanners
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -46,19 +69,42 @@ exports[`CourseCard component snapshot: not collapsed 1`] = `
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<CourseCardContent
|
||||
<CourseCardImage
|
||||
cardId="test-card-id"
|
||||
orientation="horizontal"
|
||||
/>
|
||||
<Card.Body>
|
||||
<Card.Header
|
||||
actions={
|
||||
<CourseCardMenu
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<CourseCardTitle
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card.Section
|
||||
className="pt-0"
|
||||
>
|
||||
<CourseCardDetails
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Card.Footer
|
||||
orientation="horizontal"
|
||||
>
|
||||
<CourseCardActions
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
</div>
|
||||
<div
|
||||
className="course-card-banners"
|
||||
data-testid="CourseCardBanners"
|
||||
>
|
||||
<CourseCardBanners
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</div>
|
||||
<CourseCardBanners
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ActionButton snapshot is collapsed 1`] = `
|
||||
<Button
|
||||
arbitary="props"
|
||||
size="sm"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ActionButton snapshot is not collapsed 1`] = `
|
||||
<Button
|
||||
arbitary="props"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
|
||||
export const useIsCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
return width < breakpoints.medium.maxWidth && width > breakpoints.small.maxWidth;
|
||||
};
|
||||
|
||||
export default useIsCollapsed;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import useIsCollapsed from './hooks';
|
||||
|
||||
describe('useIsCollapsed', () => {
|
||||
it('returns true only when it is between medium and small', () => {
|
||||
// make sure all three breakpoints gap is large enough for test
|
||||
expect(
|
||||
(breakpoints.large.maxWidth - 1)
|
||||
> (breakpoints.medium.maxWidth - 1)
|
||||
&& (breakpoints.medium.maxWidth - 1)
|
||||
> (breakpoints.small.maxWidth - 1),
|
||||
).toBe(true);
|
||||
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.large.maxWidth - 1 });
|
||||
expect(useIsCollapsed()).toEqual(false);
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.medium.maxWidth - 1 });
|
||||
expect(useIsCollapsed()).toEqual(true);
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.small.maxWidth - 1 });
|
||||
expect(useIsCollapsed()).toEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import useIsCollapsed from './hooks';
|
||||
|
||||
export const ActionButton = (props) => {
|
||||
const isSmall = useIsCollapsed();
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
{...isSmall && { size: 'sm' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import ActionButton from '.';
|
||||
|
||||
import useIsCollapsed from './hooks';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
describe('ActionButton', () => {
|
||||
const props = {
|
||||
arbitary: 'props',
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('is collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(true);
|
||||
const wrapper = shallow(<ActionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is not collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
const wrapper = shallow(<ActionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
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 { homeUrl } = hooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = hooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||
const { disableBeginCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl + execEdTrackingParam,
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
disabled={isMasquerading || !hasAccess}
|
||||
<ActionButton
|
||||
disabled={disableBeginCourse}
|
||||
as="a"
|
||||
href={homeUrl}
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{formatMessage(messages.beginCourse)}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
BeginCourseButton.propTypes = {
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { hooks } from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'home-url' })),
|
||||
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
let wrapper;
|
||||
const { homeUrl } = hooks.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 = {
|
||||
@@ -22,33 +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.href)).toEqual(homeUrl);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data with cardId', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(hooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('initializes enrollment data with cardId', () => {
|
||||
it('loads exec education path param', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(hooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('learner does not have access', () => {
|
||||
hooks.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', () => {
|
||||
hooks.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),
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
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 } = hooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess, isAudit, isAuditAccessExpired } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = hooks.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 + execEdTrackingParam,
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
disabled={isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired)}
|
||||
<ActionButton
|
||||
disabled={disableResumeCourse}
|
||||
as="a"
|
||||
href={resumeUrl}
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{formatMessage(messages.resume)}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
ResumeButton.propTypes = {
|
||||
|
||||
@@ -1,67 +1,84 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { hooks } from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ResumeButton from './ResumeButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ resumeUrl: 'resumeUrl' })),
|
||||
useCardEnrollmentData: jest.fn(() => ({
|
||||
hasAccess: true,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: false,
|
||||
})),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
const { resumeUrl } = hooks.useCardCourseRunData();
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
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.href)).toEqual(resumeUrl);
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data with cardId', () => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads exec education path param', () => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads disabled states for resume action from action hooks', () => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data based on cardId', () => {
|
||||
shallow(<ResumeButton {...props} />);
|
||||
expect(hooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('initializes course enrollment data based on cardId', () => {
|
||||
shallow(<ResumeButton {...props} />);
|
||||
expect(hooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('masquerading', () => {
|
||||
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
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', () => {
|
||||
hooks.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', () => {
|
||||
hooks.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),
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const SelectSessionButton = ({ cardId }) => {
|
||||
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
|
||||
const { canChange, hasSessions } = hooks.useCardEntitlementData(cardId);
|
||||
const { isMasquerading } = hooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const openSessionModal = hooks.useUpdateSelectSessionModalCallback(dispatch, cardId);
|
||||
const { disableSelectSession } = useActionDisabledState(cardId);
|
||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
return (
|
||||
<Button
|
||||
disabled={isMasquerading || !hasAccess || (!canChange || !hasSessions)}
|
||||
<ActionButton
|
||||
disabled={disableSelectSession}
|
||||
onClick={openSessionModal}
|
||||
>
|
||||
{formatMessage(messages.resume)}
|
||||
</Button>
|
||||
{formatMessage(messages.selectSession)}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
SelectSessionButton.propTypes = {
|
||||
|
||||
@@ -1,66 +1,35 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
|
||||
useCardEntitlementData: jest.fn(() => ({ canChange: true, hasSessions: true })),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
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', () => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('renders disabled button if masquerading', () => {
|
||||
hooks.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(hooks.useUpdateSelectSessionModalCallback().getMockName());
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('learner does not have access', () => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner cannot change sessions', () => {
|
||||
hooks.useCardEntitlementData.mockReturnValueOnce({ canChange: false, hasSessions: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('entitlement does not have available sessions', () => {
|
||||
hooks.useCardEntitlementData.mockReturnValueOnce({ canChange: true, hasSessions: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('user is masquerading', () => {
|
||||
hooks.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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Locked } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const UpgradeButton = ({ cardId }) => {
|
||||
const { upgradeUrl } = hooks.useCardCourseRunData(cardId);
|
||||
const { canUpgrade } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = hooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const isEnabled = (!isMasquerading && canUpgrade);
|
||||
|
||||
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { disableUpgradeCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.upgradeClicked,
|
||||
cardId,
|
||||
upgradeUrl,
|
||||
);
|
||||
|
||||
const enabledProps = {
|
||||
as: 'a',
|
||||
href: upgradeUrl,
|
||||
onClick: trackUpgradeClick,
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
<ActionButton
|
||||
iconBefore={Locked}
|
||||
variant="outline-primary"
|
||||
disabled={!isEnabled}
|
||||
{...isEnabled && { as: 'a', href: upgradeUrl }}
|
||||
disabled={disableUpgradeCourse}
|
||||
{...!disableUpgradeCourse && enabledProps}
|
||||
>
|
||||
{formatMessage(messages.upgrade)}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
UpgradeButton.propTypes = {
|
||||
|
||||
@@ -1,37 +1,47 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { hooks } from 'data/redux';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })),
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
upgradeClicked: jest.fn().mockName('segment.trackUpgradeClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableUpgradeCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
describe('UpgradeButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
const upgradeUrl = 'upgradeUrl';
|
||||
hooks.useCardCourseRunData.mockReturnValue({ upgradeUrl });
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ upgradeUrl });
|
||||
describe('snapshot', () => {
|
||||
test('can upgrade', () => {
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.upgradeClicked,
|
||||
props.cardId,
|
||||
upgradeUrl,
|
||||
));
|
||||
});
|
||||
test('cannot upgrade', () => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('masquerading', () => {
|
||||
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
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 { homeUrl } = hooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isEntitlement, isExpired } = hooks.useCardEntitlementData(cardId);
|
||||
const { formatMessage } = useIntl();
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { disableViewCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
disabled={!hasAccess || (isEntitlement && isExpired)}
|
||||
<ActionButton
|
||||
disabled={disableViewCourse}
|
||||
as="a"
|
||||
href={homeUrl}
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{formatMessage(messages.viewCourse)}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
ViewCourseButton.propTypes = {
|
||||
|
||||
@@ -1,58 +1,46 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import track from 'tracking';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { hooks } from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
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');
|
||||
|
||||
const defaultProps = { cardId: 'cardId' };
|
||||
const homeUrl = 'homeUrl';
|
||||
|
||||
describe('ViewCourseButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
const homeUrl = 'homeUrl';
|
||||
hooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
const createWrapper = ({
|
||||
hasAccess = false,
|
||||
isEntitlement = false,
|
||||
isExpired = false,
|
||||
}) => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess });
|
||||
hooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
|
||||
return shallow(<ViewCourseButton {...props} />);
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('default button', () => {
|
||||
const wrapper = createWrapper({ hasAccess: true });
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.href)).toEqual(homeUrl);
|
||||
});
|
||||
test('disabled button', () => {
|
||||
const wrapper = createWrapper({});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
expect(wrapper.prop(htmlProps.href)).toEqual(homeUrl);
|
||||
});
|
||||
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('behavior', () => {
|
||||
it('disabled button without access', () => {
|
||||
const wrapper = createWrapper({ hasAccess: false, isEntitlement: false, isExpired: false });
|
||||
expect(wrapper.prop('disabled')).toEqual(true);
|
||||
});
|
||||
it('disabled button with access', () => {
|
||||
const wrapper = createWrapper({ hasAccess: true, isEntitlement: true, isExpired: true });
|
||||
expect(wrapper.prop('disabled')).toEqual(true);
|
||||
});
|
||||
it('enabled button', () => {
|
||||
const wrapper = createWrapper({ hasAccess: true, isEntitlement: false, isExpired: false });
|
||||
expect(wrapper.prop('disabled')).toEqual(false);
|
||||
});
|
||||
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,11 +1,39 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BeginCourseButton snapshot renders default button when learner has access to the course 1`] = `
|
||||
<Button
|
||||
exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="home-url"
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Begin Course
|
||||
</Button>
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Begin Course
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
@@ -1,11 +1,39 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResumeButton snapshot renders default button when learner has access to the course 1`] = `
|
||||
<Button
|
||||
exports[`ResumeButton snapshot disabled snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="resumeUrl"
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
</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,28 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectSessionButton snapshot renders default button 1`] = `
|
||||
<Button
|
||||
exports[`SelectSessionButton default render 1`] = `undefined`;
|
||||
|
||||
exports[`SelectSessionButton disabled states 1`] = `
|
||||
<ActionButton
|
||||
disabled={false}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionButton snapshot renders disabled button if masquerading 1`] = `
|
||||
<Button
|
||||
disabled={true}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionButton snapshot renders disabled button when user does not have access to the course 1`] = `
|
||||
<Button
|
||||
disabled={true}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
Select Session
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UpgradeButton snapshot can upgrade 1`] = `
|
||||
<Button
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="upgradeUrl"
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.trackUpgradeClicked],
|
||||
"upgradeUrl": "upgradeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`UpgradeButton snapshot cannot upgrade 1`] = `
|
||||
<Button
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`UpgradeButton snapshot masquerading 1`] = `
|
||||
<Button
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ViewCourseButton snapshot default button 1`] = `
|
||||
<Button
|
||||
exports[`ViewCourseButton learner can view course 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="homeUrl"
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
View Course
|
||||
</Button>
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`ViewCourseButton snapshot disabled button 1`] = `
|
||||
<Button
|
||||
exports[`ViewCourseButton learner cannot view course 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={true}
|
||||
href="homeUrl"
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
View Course
|
||||
</Button>
|
||||
</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>
|
||||
`;
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { ActionRow } from '@edx/paragon';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
@@ -12,22 +12,28 @@ import ResumeButton from './ResumeButton';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
export const CourseCardActions = ({ cardId }) => {
|
||||
const { isEntitlement, isFulfilled } = hooks.useCardEntitlementData(cardId);
|
||||
const { isVerified, hasStarted } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isArchived } = hooks.useCardCourseRunData(cardId);
|
||||
let PrimaryButton;
|
||||
if (isEntitlement) {
|
||||
PrimaryButton = isFulfilled ? ViewCourseButton : SelectSessionButton;
|
||||
} else if (isArchived) {
|
||||
PrimaryButton = ViewCourseButton;
|
||||
} else {
|
||||
PrimaryButton = hasStarted ? ResumeButton : BeginCourseButton;
|
||||
}
|
||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const {
|
||||
isVerified,
|
||||
hasStarted,
|
||||
isExecEd2UCourse,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
||||
|
||||
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,13 +1,21 @@
|
||||
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 { hooks } from 'data/redux';
|
||||
import CourseCardActions from '.';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -17,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 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({ isExecEd2UCourse, isVerified, hasStarted });
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
};
|
||||
const createWrapper = ({
|
||||
isEntitlement, isFulfilled, isArchived, isVerified, hasStarted,
|
||||
}) => {
|
||||
hooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
|
||||
hooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ isVerified, hasStarted });
|
||||
return shallow(<CourseCardActions {...props} />);
|
||||
const render = () => {
|
||||
el = shallow(<CourseCardActions {...props} />);
|
||||
};
|
||||
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();
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,33 +6,48 @@ import { MailtoLink, Hyperlink } from '@edx/paragon';
|
||||
import { CheckCircle } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const { useFormatDate } = utilHooks;
|
||||
|
||||
export const CertificateBanner = ({ cardId }) => {
|
||||
const certificate = appHooks.useCardCertificateData(cardId);
|
||||
const certificate = reduxHooks.useCardCertificateData(cardId);
|
||||
const {
|
||||
isAudit,
|
||||
isVerified,
|
||||
hasFinished,
|
||||
} = appHooks.useCardEnrollmentData(cardId);
|
||||
const { isPassing } = appHooks.useCardGradeData(cardId);
|
||||
const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(cardId);
|
||||
const { supportEmail, billingEmail } = appHooks.usePlatformSettingsData();
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isPassing } = reduxHooks.useCardGradeData(cardId);
|
||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { minPassingGrade, progressUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { supportEmail, billingEmail } = reduxHooks.usePlatformSettingsData();
|
||||
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>
|
||||
);
|
||||
@@ -45,12 +60,12 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (hasFinished) {
|
||||
if (isArchived) {
|
||||
return (
|
||||
<Banner variant="warning">
|
||||
{formatMessage(messages.notEligibleForCert)}.
|
||||
{formatMessage(messages.notEligibleForCert)}
|
||||
{' '}
|
||||
<Hyperlink destination={progressUrl}>{formatMessage(messages.viewGrades)}</Hyperlink>
|
||||
<Hyperlink isInline destination={progressUrl}>{formatMessage(messages.viewGrades)}</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
@@ -60,17 +75,6 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isDownloadable) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
{formatMessage(messages.certReady)}
|
||||
{' '}
|
||||
<Hyperlink destination={certificate.certPreviewUrl}>
|
||||
{formatMessage(messages.viewCertificate)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isEarnedButUnavailable) {
|
||||
return (
|
||||
<Banner>
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import CertificateBanner from './CertificateBanner';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
jest.mock('hooks', () => ({
|
||||
utilHooks: {
|
||||
useFormatDate: jest.fn(() => date => date),
|
||||
},
|
||||
reduxHooks: {
|
||||
useCardCertificateData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardGradeData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('Components/Banner', () => 'Banner');
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
|
||||
describe('CertificateBanner', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
hooks.usePlatformSettingsData.mockReturnValue({
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
});
|
||||
hooks.useCardCourseRunData.mockReturnValue({
|
||||
const props = { cardId: 'cardId' };
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({
|
||||
minPassingGrade: 0.8,
|
||||
progressUrl: 'progressUrl',
|
||||
});
|
||||
|
||||
const defaultCertificate = {
|
||||
availableDate: '10/20/3030',
|
||||
isRestricted: false,
|
||||
isDownloadable: false,
|
||||
isEarnedButUnavailable: false,
|
||||
@@ -37,21 +35,25 @@ describe('CertificateBanner', () => {
|
||||
const defaultEnrollment = {
|
||||
isAudit: false,
|
||||
isVerified: false,
|
||||
hasFinished: false,
|
||||
};
|
||||
const defaultGrade = {
|
||||
isPassing: false,
|
||||
};
|
||||
const defaultCourseRun = { isArchived: false };
|
||||
const defaultGrade = { isPassing: false };
|
||||
const defaultPlatformSettings = {};
|
||||
const createWrapper = ({
|
||||
certificate = {},
|
||||
enrollment = {},
|
||||
grade = {},
|
||||
courseRun = {},
|
||||
platformSettings = {},
|
||||
}) => {
|
||||
hooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
|
||||
hooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
|
||||
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({
|
||||
@@ -61,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: {
|
||||
@@ -72,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: {
|
||||
@@ -82,9 +163,7 @@ describe('CertificateBanner', () => {
|
||||
});
|
||||
test('not passing and has finished', () => {
|
||||
const wrapper = createWrapper({
|
||||
enrollment: {
|
||||
hasFinished: true,
|
||||
},
|
||||
courseRun: { isArchived: true },
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -14,10 +14,10 @@ export const CourseBanner = ({ cardId }) => {
|
||||
isAuditAccessExpired,
|
||||
canUpgrade,
|
||||
coursewareAccess = {},
|
||||
} = appHooks.useCardEnrollmentData(cardId);
|
||||
const courseRun = appHooks.useCardCourseRunData(cardId);
|
||||
const course = appHooks.useCardCourseData(cardId);
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = utilHooks.useFormatDate();
|
||||
|
||||
const { hasUnmetPrerequisites, isStaff, isTooEarly } = coursewareAccess;
|
||||
|
||||
@@ -36,11 +36,9 @@ export const CourseBanner = ({ cardId }) => {
|
||||
<Banner>
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
{
|
||||
<Hyperlink destination="">
|
||||
{formatMessage(messages.findAnotherCourse)}
|
||||
</Hyperlink>
|
||||
}
|
||||
<Hyperlink isInline destination="">
|
||||
{formatMessage(messages.findAnotherCourse)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
))}
|
||||
|
||||
@@ -48,7 +46,7 @@ export const CourseBanner = ({ cardId }) => {
|
||||
<Banner>
|
||||
{formatMessage(messages.upgradeDeadlinePassed)}
|
||||
{' '}
|
||||
<Hyperlink destination={course.website || ''}>
|
||||
<Hyperlink isInline destination={courseRun.marketingUrl || ''}>
|
||||
{formatMessage(messages.exploreCourseDetails)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
|
||||
@@ -2,16 +2,18 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { CourseBanner } from './CourseBanner';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseData: jest.fn(),
|
||||
jest.mock('hooks', () => ({
|
||||
utilHooks: {
|
||||
useFormatDate: () => date => date,
|
||||
},
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
},
|
||||
@@ -34,26 +36,19 @@ const enrollmentData = {
|
||||
const courseRunData = {
|
||||
isActive: false,
|
||||
startDate: '11/11/3030',
|
||||
};
|
||||
const courseData = {
|
||||
website: 'test-course-website',
|
||||
marketingUrl: 'marketing-url',
|
||||
};
|
||||
|
||||
const render = (overrides = {}) => {
|
||||
const {
|
||||
course = {},
|
||||
courseRun = {},
|
||||
enrollment = {},
|
||||
} = overrides;
|
||||
appHooks.useCardCourseData.mockReturnValueOnce({
|
||||
...courseData,
|
||||
...course,
|
||||
});
|
||||
appHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
...courseRunData,
|
||||
...courseRun,
|
||||
});
|
||||
appHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
...enrollmentData,
|
||||
...enrollment,
|
||||
});
|
||||
@@ -63,9 +58,8 @@ const render = (overrides = {}) => {
|
||||
describe('CourseBanner', () => {
|
||||
test('initializes data with course number from enrollment, course and course run data', () => {
|
||||
render();
|
||||
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
|
||||
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
test('no display if learner is verified', () => {
|
||||
render({ enrollment: { isVerified: true } });
|
||||
@@ -106,7 +100,7 @@ describe('CourseBanner', () => {
|
||||
expect(el.text()).toContain(messages.upgradeDeadlinePassed.defaultMessage);
|
||||
const link = el.find(Hyperlink);
|
||||
expect(link.text()).toEqual(messages.exploreCourseDetails.defaultMessage);
|
||||
expect(link.props().destination).toEqual(courseData.website);
|
||||
expect(link.props().destination).toEqual(courseRunData.marketingUrl);
|
||||
});
|
||||
});
|
||||
test('no display if audit access not expired and (course is not active or can upgrade)', () => {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreditBanner component render with error state snapshot 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
<p
|
||||
className="credit-error-msg"
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
|
||||
"description": "",
|
||||
"id": "learner-dash.courseCard.banners.credit.error",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmailLink": <MailtoLink
|
||||
to="test-support-email"
|
||||
>
|
||||
test-support-email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<ContentComponent
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</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
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import ApprovedContent from './views/ApprovedContent';
|
||||
import EligibleContent from './views/EligibleContent';
|
||||
import MustRequestContent from './views/MustRequestContent';
|
||||
import PendingContent from './views/PendingContent';
|
||||
import RejectedContent from './views/RejectedContent';
|
||||
|
||||
export const statusComponents = StrictDict({
|
||||
pending: PendingContent,
|
||||
approved: ApprovedContent,
|
||||
rejected: RejectedContent,
|
||||
});
|
||||
|
||||
export const useCreditBannerData = (cardId) => {
|
||||
const credit = reduxHooks.useCardCreditData(cardId);
|
||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
||||
if (!credit.isEligible) { return null; }
|
||||
|
||||
const { error, purchased, requestStatus } = credit;
|
||||
let ContentComponent = EligibleContent;
|
||||
if (purchased) {
|
||||
if (requestStatus == null) {
|
||||
ContentComponent = MustRequestContent;
|
||||
} else if (Object.keys(statusComponents).includes(requestStatus)) {
|
||||
ContentComponent = statusComponents[requestStatus];
|
||||
}
|
||||
// Current behavior is to show Elligible State if unknown request status is returned
|
||||
}
|
||||
return {
|
||||
ContentComponent,
|
||||
error,
|
||||
supportEmail,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
useCreditBannerData,
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { keyStore } from 'utils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import ApprovedContent from './views/ApprovedContent';
|
||||
import EligibleContent from './views/EligibleContent';
|
||||
import MustRequestContent from './views/MustRequestContent';
|
||||
import PendingContent from './views/PendingContent';
|
||||
import RejectedContent from './views/RejectedContent';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./views/ApprovedContent', () => 'ApprovedContent');
|
||||
jest.mock('./views/EligibleContent', () => 'EligibleContent');
|
||||
jest.mock('./views/MustRequestContent', () => 'MustRequestContent');
|
||||
jest.mock('./views/PendingContent', () => 'PendingContent');
|
||||
jest.mock('./views/RejectedContent', () => 'RejectedContent');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const statuses = keyStore(hooks.statusComponents);
|
||||
const supportEmail = 'test-support-email';
|
||||
let out;
|
||||
|
||||
const defaultProps = {
|
||||
isEligible: true,
|
||||
error: false,
|
||||
isPurchased: false,
|
||||
requestStatus: null,
|
||||
};
|
||||
|
||||
const loadHook = (creditData = {}) => {
|
||||
reduxHooks.useCardCreditData.mockReturnValue({ ...defaultProps, ...creditData });
|
||||
out = hooks.useCreditBannerData(cardId);
|
||||
};
|
||||
|
||||
describe('useCreditBannerData hook', () => {
|
||||
beforeEach(() => {
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
||||
});
|
||||
it('loads card credit data with cardID and loads platform settings data', () => {
|
||||
loadHook({ isEligible: false });
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.usePlatformSettingsData).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('non-credit-eligible learner', () => {
|
||||
it('returns null if the learner is not credit eligible', () => {
|
||||
loadHook({ isEligible: false });
|
||||
expect(out).toEqual(null);
|
||||
});
|
||||
});
|
||||
describe('credit-eligible learner', () => {
|
||||
it('returns error object from credit', () => {
|
||||
loadHook();
|
||||
expect(out.error).toEqual(defaultProps.error);
|
||||
loadHook({ error: true });
|
||||
expect(out.error).toEqual(true);
|
||||
});
|
||||
describe('ContentComponent', () => {
|
||||
it('returns EligibleContent if not purchased', () => {
|
||||
loadHook();
|
||||
expect(out.ContentComponent).toEqual(EligibleContent);
|
||||
});
|
||||
it('returns MustRequestContent if purchased but not requested', () => {
|
||||
loadHook({ purchased: true });
|
||||
expect(out.ContentComponent).toEqual(MustRequestContent);
|
||||
});
|
||||
it('returns PendingContent if purchased and request is pending', () => {
|
||||
loadHook({ purchased: true, requestStatus: statuses.pending });
|
||||
expect(out.ContentComponent).toEqual(PendingContent);
|
||||
});
|
||||
it('returns ApprovedContent if purchased and request is approved', () => {
|
||||
loadHook({ purchased: true, requestStatus: statuses.approved });
|
||||
expect(out.ContentComponent).toEqual(ApprovedContent);
|
||||
});
|
||||
it('returns RejectedContent if purchased and request is rejected', () => {
|
||||
loadHook({ purchased: true, requestStatus: statuses.rejected });
|
||||
expect(out.ContentComponent).toEqual(RejectedContent);
|
||||
});
|
||||
it('returns EligibleContent if purchased and request status is invalid', () => {
|
||||
loadHook({ purchased: true, requestStatus: 'fake-status' });
|
||||
expect(out.ContentComponent).toEqual(EligibleContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
import hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const CreditBanner = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const hookData = hooks.useCreditBannerData(cardId);
|
||||
if (hookData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ContentComponent, error, supportEmail } = hookData;
|
||||
const supportEmailLink = (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>);
|
||||
return (
|
||||
<Banner {...(error && { variant: 'danger' })}>
|
||||
{error && (
|
||||
<p className="credit-error-msg">
|
||||
{supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)}
|
||||
</p>
|
||||
)}
|
||||
<ContentComponent cardId={cardId} />
|
||||
</Banner>
|
||||
);
|
||||
};
|
||||
CreditBanner.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CreditBanner;
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
|
||||
import hooks from './hooks';
|
||||
import messages from './messages';
|
||||
import CreditBanner from '.';
|
||||
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useCreditBannerData: jest.fn(),
|
||||
}));
|
||||
|
||||
let el;
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
const ContentComponent = () => 'ContentComponent';
|
||||
const supportEmail = 'test-support-email';
|
||||
|
||||
describe('CreditBanner component', () => {
|
||||
describe('behavior', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useCreditBannerData.mockReturnValue(null);
|
||||
el = shallow(<CreditBanner cardId={cardId} />);
|
||||
});
|
||||
it('initializes hooks with cardId', () => {
|
||||
expect(hooks.useCreditBannerData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
it('returns null if hookData is null', () => {
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('with error state', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useCreditBannerData.mockReturnValue({
|
||||
error: true,
|
||||
ContentComponent,
|
||||
supportEmail,
|
||||
});
|
||||
el = shallow(<CreditBanner cardId={cardId} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('passes danger variant to Banner parent', () => {
|
||||
expect(el.find('Banner').props().variant).toEqual('danger');
|
||||
});
|
||||
it('includes credit-error-msg with support email link', () => {
|
||||
expect(el.find('.credit-error-msg').containsMatchingElement(
|
||||
formatMessage(messages.error, {
|
||||
supportEmailLink: (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>),
|
||||
}),
|
||||
)).toEqual(true);
|
||||
});
|
||||
it('loads ContentComponent with cardId', () => {
|
||||
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({
|
||||
error: false,
|
||||
ContentComponent,
|
||||
supportEmail,
|
||||
});
|
||||
el = shallow(<CreditBanner cardId={cardId} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('loads ContentComponent with cardId', () => {
|
||||
expect(el.find('ContentComponent').props().cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export const messages = StrictDict({
|
||||
error: {
|
||||
id: 'learner-dash.courseCard.banners.credit.error',
|
||||
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;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
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 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), disabled: isMasquerading }}
|
||||
message={formatMessage(
|
||||
messages.approved,
|
||||
{
|
||||
congratulations: <b>{formatMessage(messages.congratulations)}</b>,
|
||||
linkToProviderSite: <ProviderLink cardId={cardId} />,
|
||||
providerName,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
ApprovedContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ApprovedContent;
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import messages from './messages';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import ApprovedContent from './ApprovedContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
let el;
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
providerStatusUrl: 'test-credit-provider-status-url',
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
describe('ApprovedContent component', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<ApprovedContent cardId={cardId} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes credit data with cardId', () => {
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('rendered CreditContent component', () => {
|
||||
let component;
|
||||
beforeAll(() => {
|
||||
component = el.find('CreditContent');
|
||||
});
|
||||
test('action.href from credit.providerStatusUrl', () => {
|
||||
expect(component.props().action.href).toEqual(credit.providerStatusUrl);
|
||||
});
|
||||
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,
|
||||
{
|
||||
congratulations: (<b>{formatMessage(messages.congratulations)}</b>),
|
||||
linkToProviderSite: <ProviderLink cardId={cardId} />,
|
||||
providerName: credit.providerName,
|
||||
},
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
|
||||
import CreditContent from './components/CreditContent';
|
||||
import messages from './messages';
|
||||
|
||||
export const EligibleContent = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { courseId } = reduxHooks.useCardCourseRunData(cardId);
|
||||
|
||||
const onClick = track.credit.purchase(courseId);
|
||||
const getCredit = formatMessage(messages.getCredit);
|
||||
const message = providerName
|
||||
? formatMessage(messages.eligibleFromProvider, { providerName })
|
||||
: formatMessage(messages.eligible, { getCredit: (<b>{getCredit}</b>) });
|
||||
|
||||
return (
|
||||
<CreditContent
|
||||
action={{ onClick, message: getCredit }}
|
||||
message={message}
|
||||
/>
|
||||
);
|
||||
};
|
||||
EligibleContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default EligibleContent;
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import track from 'tracking';
|
||||
|
||||
import messages from './messages';
|
||||
import EligibleContent from './EligibleContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('tracking', () => ({
|
||||
credit: {
|
||||
purchase: (...args) => ({ trackCredit: args }),
|
||||
},
|
||||
}));
|
||||
|
||||
let el;
|
||||
let component;
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const courseId = 'test-course-id';
|
||||
const credit = {
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<EligibleContent cardId={cardId} />);
|
||||
};
|
||||
const loadComponent = () => {
|
||||
component = el.find('CreditContent');
|
||||
};
|
||||
describe('EligibleContent component', () => {
|
||||
beforeEach(() => {
|
||||
render();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes credit data with cardId', () => {
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
it('initializes course run data with cardId', () => {
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('rendered CreditContent component', () => {
|
||||
beforeEach(() => {
|
||||
loadComponent();
|
||||
});
|
||||
test('action.onClick sends credit purchase track event', () => {
|
||||
expect(component.props().action.onClick).toEqual(
|
||||
track.credit.purchase(courseId),
|
||||
);
|
||||
});
|
||||
test('action.message is formatted getCredit message', () => {
|
||||
expect(component.props().action.message).toEqual(formatMessage(messages.getCredit));
|
||||
});
|
||||
test('message is formatted eligible message if no provider', () => {
|
||||
reduxHooks.useCardCreditData.mockReturnValueOnce({});
|
||||
render();
|
||||
loadComponent();
|
||||
expect(component.props().message).toEqual(formatMessage(
|
||||
messages.eligible,
|
||||
{ getCredit: (<b>{formatMessage(messages.getCredit)}</b>) },
|
||||
));
|
||||
});
|
||||
test('message is formatted eligible message if provider', () => {
|
||||
expect(component.props().message).toEqual(
|
||||
formatMessage(messages.eligibleFromProvider, { providerName: credit.providerName }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
|
||||
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} />),
|
||||
requestCredit: (<b>{formatMessage(messages.requestCredit)}</b>),
|
||||
})}
|
||||
requestData={requestData}
|
||||
/>
|
||||
);
|
||||
};
|
||||
MustRequestContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default MustRequestContent;
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
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');
|
||||
|
||||
let el;
|
||||
let component;
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const requestData = { test: 'requestData' };
|
||||
const createCreditRequest = jest.fn().mockName('createCreditRequest');
|
||||
hooks.useCreditRequestData.mockReturnValue({
|
||||
requestData,
|
||||
createCreditRequest,
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<MustRequestContent cardId={cardId} />);
|
||||
};
|
||||
describe('MustRequestContent component', () => {
|
||||
beforeEach(() => {
|
||||
render();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes credit request data with cardId', () => {
|
||||
expect(hooks.useCreditRequestData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('rendered CreditContent component', () => {
|
||||
beforeEach(() => {
|
||||
component = el.find('CreditContent');
|
||||
});
|
||||
test('action.onClick calls createCreditRequest from useCreditRequestData hook', () => {
|
||||
expect(component.props().action.onClick).toEqual(createCreditRequest);
|
||||
});
|
||||
test('action.message is formatted requestCredit message', () => {
|
||||
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>,
|
||||
}),
|
||||
);
|
||||
});
|
||||
test('requestData drawn from useCreditRequestData hook', () => {
|
||||
expect(component.props().requestData).toEqual(requestData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
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 })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
PendingContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PendingContent;
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import PendingContent from './PendingContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
let el;
|
||||
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.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<PendingContent cardId={cardId} />);
|
||||
};
|
||||
describe('PendingContent component', () => {
|
||||
beforeEach(() => {
|
||||
render();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes card credit data with cardId', () => {
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('rendered CreditContent component', () => {
|
||||
beforeEach(() => {
|
||||
component = el.find('CreditContent');
|
||||
});
|
||||
test('action.href will go to provider status site', () => {
|
||||
expect(component.props().action.href).toEqual(providerStatusUrl);
|
||||
});
|
||||
test('action.message is formatted requestCredit message', () => {
|
||||
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(
|
||||
formatMessage(messages.received, { providerName }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
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 messages from './messages';
|
||||
|
||||
export const RejectedContent = ({ cardId }) => {
|
||||
const credit = reduxHooks.useCardCreditData(cardId);
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
message={formatMessage(messages.rejected, {
|
||||
providerName: credit.providerName,
|
||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
RejectedContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RejectedContent;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import messages from './messages';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import RejectedContent from './RejectedContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
providerStatusUrl: 'test-credit-provider-status-url',
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
|
||||
let el;
|
||||
let component;
|
||||
const render = () => { el = shallow(<RejectedContent cardId={cardId} />); };
|
||||
const loadComponent = () => { component = el.find('CreditContent'); };
|
||||
|
||||
describe('RejectedContent component', () => {
|
||||
beforeEach(render);
|
||||
describe('behavior', () => {
|
||||
it('initializes credit data with cardId', () => {
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('rendered CreditContent component', () => {
|
||||
beforeAll(loadComponent);
|
||||
test('no action is passed', () => {
|
||||
expect(component.props().action).toEqual(undefined);
|
||||
});
|
||||
test('message is formatted rejected message', () => {
|
||||
expect(component.props().message).toEqual(formatMessage(
|
||||
messages.rejected,
|
||||
{
|
||||
linkToProviderSite: <ProviderLink cardId={cardId} />,
|
||||
providerName: credit.providerName,
|
||||
},
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ActionRow, Button } from '@edx/paragon';
|
||||
import CreditRequestForm from './CreditRequestForm';
|
||||
|
||||
export const CreditContent = ({ action, message, requestData }) => (
|
||||
<>
|
||||
<div className="message-copy credit-msg">
|
||||
{message}
|
||||
</div>
|
||||
{action && (
|
||||
<ActionRow className="mt-4">
|
||||
<Button
|
||||
as="a"
|
||||
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"
|
||||
className="border-gray-400"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action.message}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
<CreditRequestForm requestData={requestData} />
|
||||
</>
|
||||
);
|
||||
CreditContent.defaultProps = {
|
||||
action: null,
|
||||
requestData: null,
|
||||
};
|
||||
CreditContent.propTypes = {
|
||||
action: PropTypes.shape({
|
||||
href: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
message: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
}),
|
||||
message: PropTypes.node.isRequired,
|
||||
requestData: PropTypes.shape({
|
||||
url: PropTypes.string,
|
||||
parameters: PropTypes.objectOf(PropTypes.string),
|
||||
}),
|
||||
};
|
||||
|
||||
export default CreditContent;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import CreditContent from './CreditContent';
|
||||
|
||||
let el;
|
||||
const action = {
|
||||
href: 'test-action-href',
|
||||
onClick: jest.fn().mockName('test-action-onClick'),
|
||||
message: 'test-action-message',
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
const message = 'test-message';
|
||||
const requestData = { url: 'test-request-data-url', parameters: { key1: 'val1' } };
|
||||
const props = { action, message, requestData };
|
||||
|
||||
describe('CreditContent component', () => {
|
||||
describe('render', () => {
|
||||
describe('with action', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<CreditContent {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('loads href, onClick, and message into action row button', () => {
|
||||
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', () => {
|
||||
expect(el.find('div.credit-msg').text()).toEqual(message);
|
||||
});
|
||||
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', () => {
|
||||
el = shallow(<CreditContent {...{ message, requestData }} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('loads message into credit-msg div', () => {
|
||||
expect(el.find('div.credit-msg').text()).toEqual(message);
|
||||
});
|
||||
it('loads CreditRequestForm with passed requestData', () => {
|
||||
expect(el.find('CreditRequestForm').props().requestData).toEqual(requestData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreditRequestForm component render output valid requestData snapshot 1`] = `
|
||||
<Form
|
||||
accept-method="UTF-8"
|
||||
action="test-request-data-url"
|
||||
className="hidden"
|
||||
method="POST"
|
||||
>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
key="key1"
|
||||
name="key1"
|
||||
value="val1"
|
||||
/>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
key="key2"
|
||||
name="key2"
|
||||
value="val2"
|
||||
/>
|
||||
<FormControl
|
||||
as="textarea"
|
||||
key="key3"
|
||||
name="key3"
|
||||
value="val3"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
/>
|
||||
</Form>
|
||||
`;
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
export const useCreditRequestFormData = (requestData) => {
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (requestData !== null) {
|
||||
ref.current.click();
|
||||
}
|
||||
}, [requestData]);
|
||||
return { ref };
|
||||
};
|
||||
|
||||
export default useCreditRequestFormData;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import useCreditRequestFormData from './hooks';
|
||||
|
||||
const requestData = 'test-request-data';
|
||||
|
||||
let out;
|
||||
const ref = {
|
||||
current: { click: jest.fn() },
|
||||
};
|
||||
React.useRef.mockReturnValue(ref);
|
||||
|
||||
describe('useCreditRequestFormData hook', () => {
|
||||
describe('behavior', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('initializes ref with null', () => {
|
||||
useCreditRequestFormData(requestData);
|
||||
expect(React.useRef).toHaveBeenCalledWith(null);
|
||||
});
|
||||
let cb;
|
||||
let prereqs;
|
||||
it('does not click current ref when request data changes and is null', () => {
|
||||
useCreditRequestFormData(null);
|
||||
([[cb, prereqs]] = React.useEffect.mock.calls);
|
||||
expect(prereqs).toEqual([null]);
|
||||
cb();
|
||||
expect(ref.current.click).not.toHaveBeenCalled();
|
||||
});
|
||||
it('clicks current ref when request data changes and is not null', () => {
|
||||
useCreditRequestFormData(requestData);
|
||||
([[cb, prereqs]] = React.useEffect.mock.calls);
|
||||
expect(prereqs).toEqual([requestData]);
|
||||
cb();
|
||||
expect(ref.current.click).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('returns ref for submit button', () => {
|
||||
out = useCreditRequestFormData(requestData);
|
||||
expect(out.ref).toEqual(ref);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Form, FormControl } from '@edx/paragon';
|
||||
|
||||
import useCreditRequestFormData from './hooks';
|
||||
|
||||
export const CreditRequestForm = ({ requestData }) => {
|
||||
const { ref } = useCreditRequestFormData(requestData);
|
||||
if (requestData === null) {
|
||||
return null;
|
||||
}
|
||||
const { parameters, url } = requestData;
|
||||
return (
|
||||
<Form
|
||||
accept-method="UTF-8"
|
||||
action={url}
|
||||
className="hidden"
|
||||
method="POST"
|
||||
>
|
||||
{Object.keys(parameters).map((key) => (
|
||||
<FormControl
|
||||
as="textarea"
|
||||
key={key}
|
||||
name={key}
|
||||
value={parameters[key]}
|
||||
/>
|
||||
))}
|
||||
<Button type="submit" ref={ref} />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
CreditRequestForm.defaultProps = {
|
||||
requestData: null,
|
||||
};
|
||||
CreditRequestForm.propTypes = {
|
||||
requestData: PropTypes.shape({
|
||||
parameters: PropTypes.objectOf(PropTypes.string),
|
||||
url: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
export default CreditRequestForm;
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
|
||||
import useCreditRequestFormData from './hooks';
|
||||
import CreditRequestForm from '.';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const ref = 'test-ref';
|
||||
const requestData = {
|
||||
url: 'test-request-data-url',
|
||||
parameters: {
|
||||
key1: 'val1',
|
||||
key2: 'val2',
|
||||
key3: 'val3',
|
||||
},
|
||||
};
|
||||
|
||||
const paramKeys = keyStore(requestData.parameters);
|
||||
|
||||
useCreditRequestFormData.mockReturnValue({ ref });
|
||||
|
||||
let el;
|
||||
const shallowRender = (data) => { el = shallow(<CreditRequestForm requestData={data} />); };
|
||||
describe('CreditRequestForm component', () => {
|
||||
describe('behavior', () => {
|
||||
it('initializes ref from hook with requestData', () => {
|
||||
shallowRender(requestData);
|
||||
expect(useCreditRequestFormData).toHaveBeenCalledWith(requestData);
|
||||
});
|
||||
});
|
||||
describe('render output', () => {
|
||||
describe('null requestData', () => {
|
||||
it('returns null', () => {
|
||||
shallowRender(null);
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('valid requestData', () => {
|
||||
beforeEach(() => {
|
||||
shallowRender(requestData);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('loads Form with requestData url', () => {
|
||||
expect(el.find('Form').props().action).toEqual(requestData.url);
|
||||
});
|
||||
it('loads a textarea form control for each requestData parameter', () => {
|
||||
const controls = el.find('FormControl');
|
||||
expect(controls.at(0).props().name).toEqual(paramKeys.key1);
|
||||
expect(controls.at(0).props().value).toEqual(requestData.parameters.key1);
|
||||
expect(controls.at(1).props().name).toEqual(paramKeys.key2);
|
||||
expect(controls.at(1).props().value).toEqual(requestData.parameters.key2);
|
||||
expect(controls.at(2).props().name).toEqual(paramKeys.key3);
|
||||
expect(controls.at(2).props().value).toEqual(requestData.parameters.key3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import useCreditRequestFormData from './hooks';
|
||||
import CreditRequestForm from '.';
|
||||
|
||||
jest.unmock('@edx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const ref = { current: { click: jest.fn() }, useRef: jest.fn() };
|
||||
const requestData = {
|
||||
url: 'test-request-data-url',
|
||||
parameters: {
|
||||
key1: 'val1',
|
||||
key2: 'val2',
|
||||
key3: 'val3',
|
||||
},
|
||||
};
|
||||
|
||||
useCreditRequestFormData.mockReturnValue({ ref });
|
||||
|
||||
let el;
|
||||
describe('CreditRequestForm component ref behavior', () => {
|
||||
it('loads submit button with ref from hook', () => {
|
||||
el = render(<CreditRequestForm requestData={requestData} />);
|
||||
const button = el.getByRole('button');
|
||||
expect(ref.current).toEqual(button);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
export const ProviderLink = ({ cardId }) => {
|
||||
const credit = reduxHooks.useCardCreditData(cardId);
|
||||
return (
|
||||
<Hyperlink
|
||||
href={credit.providerStatusUrl}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
{credit.providerName}
|
||||
</Hyperlink>
|
||||
);
|
||||
};
|
||||
ProviderLink.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ProviderLink;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import ProviderLink from './ProviderLink';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
providerStatusUrl: 'test-credit-provider-status-url',
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
let el;
|
||||
|
||||
describe('ProviderLink component', () => {
|
||||
beforeEach(() => {
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
el = shallow(<ProviderLink cardId={cardId} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes credit hook with cardId', () => {
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('passes credit.providerStatusUrl to the hyperlink href', () => {
|
||||
expect(el.find('Hyperlink').props().href).toEqual(credit.providerStatusUrl);
|
||||
});
|
||||
it('passes providerName for the link message', () => {
|
||||
expect(el.find('Hyperlink').text()).toEqual(credit.providerName);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreditContent component render with action snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="message-copy credit-msg"
|
||||
>
|
||||
test-message
|
||||
</div>
|
||||
<ActionRow
|
||||
className="mt-4"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
className="border-gray-400"
|
||||
disabled={false}
|
||||
href="test-action-href"
|
||||
onClick={[MockFunction test-action-onClick]}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
variant="outline-primary"
|
||||
>
|
||||
test-action-message
|
||||
</Button>
|
||||
</ActionRow>
|
||||
<CreditRequestForm
|
||||
requestData={
|
||||
Object {
|
||||
"parameters": Object {
|
||||
"key1": "val1",
|
||||
},
|
||||
"url": "test-request-data-url",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CreditContent component render without action snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="message-copy credit-msg"
|
||||
>
|
||||
test-message
|
||||
</div>
|
||||
<CreditRequestForm
|
||||
requestData={
|
||||
Object {
|
||||
"parameters": Object {
|
||||
"key1": "val1",
|
||||
},
|
||||
"url": "test-request-data-url",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ProviderLink component render snapshot 1`] = `
|
||||
<Hyperlink
|
||||
href="test-credit-provider-status-url"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
test-credit-provider-name
|
||||
</Hyperlink>
|
||||
`;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { apiHooks } from 'hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
creditRequestData: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useCreditRequestData = (cardId) => {
|
||||
const [requestData, setRequestData] = module.state.creditRequestData(null);
|
||||
const createCreditApiRequest = apiHooks.useCreateCreditRequest(cardId);
|
||||
const createCreditRequest = (e) => {
|
||||
e.preventDefault();
|
||||
createCreditApiRequest()
|
||||
.then((request) => {
|
||||
setRequestData(request.data);
|
||||
});
|
||||
};
|
||||
return { requestData, createCreditRequest };
|
||||
};
|
||||
|
||||
export default {
|
||||
useCreditRequestData,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user