Compare commits

...

63 Commits

Author SHA1 Message Date
Deimer Morales
2b1c56e850 fix: include frontend component header translation (#793) [Ulmo backport] (#800) 2026-02-19 13:46:43 -05:00
dependabot[bot]
0db621b134 chore(deps): bump actions/setup-node from 5 to 6 (#737) 2025-10-23 13:58:00 -04:00
renovate[bot]
aaf2e36fe9 chore(deps): update dependency @reduxjs/toolkit to v2.9.1 (#736)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 15:56:50 +00:00
renovate[bot]
b527fbcfba chore(deps): update dependency @openedx/paragon to v23.14.9 (#735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 05:13:03 +00:00
Feanil Patel
fa3f7b27cf fix: Run npm audit fix to update dependencies. (#734) 2025-10-17 13:23:06 -04:00
Feanil Patel
3b0c58f376 fix: Run npm audit fix to update dependencies. 2025-10-15 10:10:56 -04:00
renovate[bot]
f903392ca1 fix(deps): update dependency core-js to v3.46.0 (#733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 08:39:42 +00:00
renovate[bot]
89032f09f4 chore(deps): update dependency @openedx/paragon to v23.14.8 (#732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 04:46:26 +00:00
renovate[bot]
03f146cce9 chore(deps): update dependency @testing-library/jest-dom to v6.9.1 (#728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 06:04:32 +00:00
renovate[bot]
7dd2bda86e chore(deps): update dependency @openedx/paragon to v23.14.4 (#724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 02:33:18 -04:00
Feanil Patel
410f2f730f build: remove unused reactifex packages
Remove reactifex and/or @edx/reactifex packages from devDependencies
as they are no longer needed. Translation extraction functionality has
been verified to work correctly without these dependencies.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 14:07:40 -03:00
PKulkoRaccoonGang
de13749443 test: Remove support for Node 20 2025-09-25 09:59:23 -03:00
dependabot[bot]
0e66b2031d chore(deps): bump actions/setup-node from 4 to 5 (#715)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 16:23:41 -04:00
Peter Kulko
697c9ff2c8 build: Upgrade to Node 24 (#702) 2025-09-24 16:15:55 -04:00
renovate[bot]
fdb5d2f68c fix(deps): update dependency @reduxjs/toolkit to v2.9.0 (#722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:47:27 +00:00
renovate[bot]
7cf79f8931 chore(deps): update dependency @testing-library/jest-dom to v6.8.0 (#721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 04:57:59 +00:00
Peter Kulko
73b7b7f5d0 test: Add Node 24 to CI matrix (#701) 2025-09-16 09:36:20 -04:00
renovate[bot]
516544c7cc fix(deps): update dependency @edx/frontend-platform to v8.5.1 (#719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 21:06:02 +00:00
renovate[bot]
2134df5478 fix(deps): update dependency @edx/frontend-component-footer to v14.9.2 (#718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 05:49:50 +00:00
renovate[bot]
5345d2eef2 fix(deps): update dependency @edx/frontend-component-header to v6.6.1 (#714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 10:42:20 +00:00
renovate[bot]
d94ac8dd62 fix(deps): update dependency @edx/frontend-component-footer to v14.9.1 (#713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 04:33:27 +00:00
Mubbshar Anwar
ff925b06f1 mubbsharanwar/unenrollment process improvement (#704)
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
2025-09-03 14:39:49 -04:00
Samuel Allan
2696486e5b fix: update frontend-build to fix install issues (#712) 2025-09-02 13:23:36 -04:00
renovate[bot]
5854b00d08 fix(deps): update dependency core-js to v3.45.1 (#711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 09:46:49 +00:00
renovate[bot]
13e9b1a85f fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.6 (#710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 04:58:44 +00:00
Maxwell Frank
8116956a4b feat: remove widgets in favor of plugins (#708) 2025-08-20 11:35:58 -04:00
Maxwell Frank
8d23e7585b chore: upgrade frontend-component-header 6.6.0 (#709) 2025-08-19 19:43:37 -04:00
Maxwell Frank
b63a40006e feat: add tsconfig.json (#707) 2025-08-19 17:24:18 -04:00
renovate[bot]
92243063b9 fix(deps): update dependency @openedx/paragon to v23.14.2 (#706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 08:58:22 +00:00
renovate[bot]
a2673399aa chore(deps): update dependency copy-webpack-plugin to v13.0.1 (#705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 05:46:31 +00:00
dependabot[bot]
cedcb4172d chore(deps): bump actions/checkout from 4 to 5 (#700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-14 13:44:21 -04:00
renovate[bot]
9e4faf1569 fix(deps): update dependency @edx/frontend-platform to v8.5.0 (#698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 08:59:39 +00:00
renovate[bot]
3a9acc981b fix(deps): update dependency core-js to v3.45.0 (#699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 05:22:27 +00:00
renovate[bot]
e3c18698d8 fix(deps): update dependency @openedx/paragon to v23.14.1 (#695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 05:45:04 +00:00
Kyle McCormick
eb38beedc3 chore: Delete CODEOWNERS (#691) 2025-07-31 16:08:12 -04:00
renovate[bot]
01faf5a30a fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.3 (#690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 12:15:42 +00:00
renovate[bot]
f4807614e2 chore(deps): update dependency @testing-library/jest-dom to v6.6.4 (#689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 06:51:58 +00:00
renovate[bot]
ab448e52f2 chore(deps): update dependency copy-webpack-plugin to v13 (#688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 12:09:13 -04:00
renovate[bot]
3383016176 fix(deps): update dependency @edx/frontend-component-header to v6.4.2 (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 04:43:49 +00:00
renovate[bot]
054cd57d4b fix(deps): update dependency react-router-dom to v6.30.1 (#686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 10:34:49 +00:00
renovate[bot]
0fc3cc4d53 fix(deps): update dependency core-js to v3.44.0 (#685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 06:17:55 +00:00
renovate[bot]
e5c1244e59 fix(deps): update dependency @openedx/paragon to v23.14.0 (#683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 14:10:26 +00:00
renovate[bot]
3a389e14e1 fix(deps): update dependency @edx/frontend-component-header to v6.4.1 (#682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 08:00:29 +00:00
Diana Villalvazo
6de409d7cc test: Deprecate react-unit-test-utils 15/15 (#680) 2025-07-02 12:23:55 -04:00
Diana Villalvazo
517b8424b3 test: Deprecate react-unit-test-utils 13/15 (#678) 2025-07-02 12:18:50 -04:00
Diana Villalvazo
49e527d810 test: Deprecate react-unit-test-utils 14/15 (#672) 2025-07-01 13:16:20 -04:00
Diana Villalvazo
fdc58a671a test: Transform snapshots into rtl tests (#674) 2025-06-30 15:35:13 -04:00
Muhammad Noyan Aziz
90aa652ca6 feat: added a generic creditPurchase Url logic (#675)
Co-authored-by: Muhammad Noyan  Aziz <noyan.aziz@A006-01474.local>
2025-06-30 10:35:32 -04:00
renovate[bot]
05a08101a2 fix(deps): update dependency @openedx/paragon to v23.13.0 (#677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 10:17:18 +00:00
renovate[bot]
cf94ea99b4 chore(deps): update dependency @openedx/frontend-build to v14.6.1 (#676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 04:54:58 +00:00
Diana Villalvazo
7f1e509ecf test: Deprecate react-unit-test-utils 12/15 (#671) 2025-06-27 12:13:33 -04:00
Diana Villalvazo
31c5445722 test: Deprecate react-unit-test-utils 11/15 (#670) 2025-06-26 15:53:29 -04:00
Diana Villalvazo
2acf7fbd73 test: Deprecate react-unit-test-utils 10/15 (#658) 2025-06-25 08:22:38 -04:00
Diana Villalvazo
cae7b1bba0 test: Deprecate react-unit-test-utils 9/15 (#668) 2025-06-24 16:50:11 +00:00
Diana Villalvazo
6ae8180f99 test: Deprecate react-unit-test-utils 8/15 (#664) 2025-06-24 12:38:13 -04:00
Diana Villalvazo
ab0f139d75 test: Deprecate react-unit-test-utils 7/15 (#662) 2025-06-24 09:44:25 -04:00
Diana Villalvazo
8a0b9dca5d test: Deprecate react-unit-test-utils 6/15 (#660) 2025-06-23 14:22:21 -04:00
Brian Smith
2e59e24876 feat!: add design tokens support (#665)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 15:05:17 -04:00
Diana Villalvazo
75865290bf test: Deprecate react-unit-test-utils 5/15 (#659) 2025-06-17 20:32:59 -04:00
Diana Villalvazo
57fe161b16 test: Deprecate react-unit-test-utils 4/15 (#656) 2025-06-17 20:09:36 -04:00
Diana Villalvazo
5c4dfd5de3 test: Deprecate react-unit-test-utils 3/15 (#655) 2025-06-17 20:00:44 -04:00
Diana Villalvazo
b0daefa2bf test: Deprecate react-unit-test-utils 2/15 (#643) 2025-06-17 19:47:36 -04:00
Diana Villalvazo
42a21180c7 test: Deprecate react-unit-test-utils 1/15 (#640) 2025-06-17 12:52:56 -04:00
165 changed files with 5884 additions and 6977 deletions

2
.env
View File

@@ -42,3 +42,5 @@ CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false NON_BROWSABLE_COURSES=false
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -48,3 +48,5 @@ CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false NON_BROWSABLE_COURSES=false
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -47,3 +47,4 @@ CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=true ENABLE_EDX_PERSONAL_DASHBOARD=true
ENABLE_PROGRAMS=false ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false NON_BROWSABLE_COURSES=false
PARAGON_THEME_URLS={}

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @openedx/2U-aperture

View File

@@ -14,10 +14,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
@@ -29,6 +29,9 @@ jobs:
- name: Lint - name: Lint
run: npm run lint run: npm run lint
- name: Type check
run: npm run types
- name: Test - name: Test
run: npm run test run: npm run test

2
.nvmrc
View File

@@ -1 +1 @@
20 24

View File

@@ -47,10 +47,11 @@ pull_translations:
&& atlas pull $(ATLAS_OPTIONS) \ && atlas pull $(ATLAS_OPTIONS) \
translations/frontend-platform/src/i18n/messages:frontend-platform \ translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/paragon/src/i18n/messages:paragon \ translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \ translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
$(intl_imports) frontend-platform paragon frontend-component-footer frontend-app-learner-dashboard $(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-learner-dashboard
# This target is used by CI. # This target is used by CI.
validate-no-uncommitted-package-lock-changes: validate-no-uncommitted-package-lock-changes:

View File

@@ -39,22 +39,12 @@ The parts of this MFE that can be customized in that manner are documented `here
Contributing Contributing
------------ ------------
A core goal of this app is to provide a clean experimentation interface. To promote this end, we have provided a Contributions are very welcome. Please read `So you want to contribute to Open edX <https://docs.openedx.org/en/latest/developers/quickstarts/so_you_want_to_contribute.html>`_ for details on how to get started as an Open edX contributor.
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 project is currently accepting all types of contributions — bug fixes, security fixes, maintenance work, or new features.
This can include conditional logic, as well as Optimizely triggers. It is important to note that our integration tests However, if you intend to add a new feature, make sure it has gone through the `Product Review process <https://openedx.atlassian.net/wiki/spaces/COMM/pages/3875962884/How+to+submit+an+open+source+contribution+for+Product+Review>`_.
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: When proposing a change, create an issue in this repo to get the discussion started.
* 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 License
------- -------

View File

@@ -24,6 +24,7 @@ module.exports = {
BASE_URL: 'localhost:1996', BASE_URL: 'localhost:1996',
LMS_BASE_URL: 'http://localhost:18000', LMS_BASE_URL: 'http://localhost:18000',
ECOMMERCE_BASE_URL: 'http://localhost:18130', ECOMMERCE_BASE_URL: 'http://localhost:18130',
CREDIT_PURCHASE_URL: 'http://localhost:8140',
LOGIN_URL: 'http://localhost:18000/login', LOGIN_URL: 'http://localhost:18000/login',
LOGOUT_URL: 'http://localhost:18000/logout', LOGOUT_URL: 'http://localhost:18000/logout',
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg', LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',

5312
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests", "test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
"quality": "npm run lint-fix && npm run test", "quality": "npm run lint-fix && npm run test",
"watch-tests": "jest --watch", "watch-tests": "jest --watch",
"snapshot": "fedx-scripts jest --updateSnapshot" "types": "tsc --noEmit"
}, },
"author": "edX", "author": "edX",
"license": "AGPL-3.0", "license": "AGPL-3.0",
@@ -31,21 +31,20 @@
"dependencies": { "dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "^14.6.0", "@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.2.0", "@edx/frontend-component-header": "^6.6.0",
"@edx/frontend-enterprise-hotjar": "7.2.0", "@edx/frontend-enterprise-hotjar": "7.2.0",
"@edx/frontend-platform": "^8.3.1", "@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.7.0", "@edx/openedx-atlas": "^0.7.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4", "@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.7.0", "@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^22.16.0", "@openedx/paragon": "^23.4.5",
"@redux-devtools/extension": "3.3.0", "@redux-devtools/extension": "3.3.0",
"@reduxjs/toolkit": "^2.0.0", "@reduxjs/toolkit": "^2.0.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"core-js": "3.42.0", "core-js": "3.46.0",
"filesize": "^10.0.0", "filesize": "^10.0.0",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"history": "5.3.0", "history": "5.3.0",
@@ -57,7 +56,7 @@
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-intl": "6.8.9", "react-intl": "6.8.9",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",
"react-router-dom": "6.29.0", "react-router-dom": "6.30.1",
"react-share": "^4.4.0", "react-share": "^4.4.0",
"redux": "4.2.1", "redux": "4.2.1",
"redux-logger": "3.0.6", "redux-logger": "3.0.6",
@@ -69,11 +68,12 @@
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "^1.3.0", "@edx/browserslist-config": "^1.3.0",
"@edx/reactifex": "^2.1.1", "@edx/typescript-config": "^1.1.0",
"@openedx/frontend-build": "^14.3.3", "@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
"copy-webpack-plugin": "^12.0.0", "@testing-library/user-event": "^14.6.1",
"copy-webpack-plugin": "^13.0.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",

View File

@@ -22,7 +22,7 @@ import track from 'tracking';
import fakeData from 'data/services/lms/fakeData/courses'; import fakeData from 'data/services/lms/fakeData/courses';
import AppWrapper from 'containers/WidgetContainers/AppWrapper'; import AppWrapper from 'containers/AppWrapper';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader'; import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';

View File

@@ -1,13 +1,10 @@
// frontend-app-*/src/index.scss // frontend-app-*/src/index.scss
@import "~@edx/brand/paragon/fonts"; @use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
$fa-font-path: "~font-awesome/fonts"; $fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome"; @import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work $input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
@import "~@edx/frontend-component-header/dist/index"; @import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/_footer"; @import "~@edx/frontend-component-footer/dist/_footer";

View File

@@ -1,23 +1,19 @@
import React from 'react'; import { render, screen, waitFor } from '@testing-library/react';
import { Helmet } from 'react-helmet';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { RequestKeys } from 'data/constants/requests'; import { RequestKeys } from 'data/constants/requests';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import { App } from './App'; import { App } from './App';
import messages from './messages'; import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' })); jest.mock('@edx/frontend-component-footer', () => ({
FooterSlot: jest.fn(() => <div>FooterSlot</div>),
jest.mock('containers/Dashboard', () => 'Dashboard'); }));
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader'); jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper'); jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => <div>LearnerDashboardHeader</div>));
jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
jest.mock('data/redux', () => ({ jest.mock('data/redux', () => ({
selectors: 'redux.selectors', selectors: 'redux.selectors',
actions: 'redux.actions', actions: 'redux.actions',
@@ -36,113 +32,103 @@ jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({})), getConfig: jest.fn(() => ({})),
})); }));
jest.mock('@edx/frontend-platform/react', () => ({
...jest.requireActual('@edx/frontend-platform/react'),
ErrorPage: () => 'ErrorPage',
}));
const loadData = jest.fn(); const loadData = jest.fn();
reduxHooks.useLoadData.mockReturnValue(loadData); reduxHooks.useLoadData.mockReturnValue(loadData);
let el; const supportEmail = 'test@support.com';
const supportEmail = 'test-support-url';
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail }); reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
describe('App router component', () => { describe('App router component', () => {
const { formatMessage } = useIntl();
describe('component', () => { describe('component', () => {
const runBasicTests = () => { const runBasicTests = () => {
test('snapshot', () => { expect(el.snapshot).toMatchSnapshot(); }); it('displays title in helmet component', async () => {
it('displays title in helmet component', () => { await waitFor(() => expect(document.title).toEqual(messages.pageTitle.defaultMessage));
const control = el.instance
.findByType(Helmet)[0]
.findByType('title')[0];
expect(control.children[0].el).toEqual(formatMessage(messages.pageTitle));
}); });
it('displays learner dashboard header', () => { it('displays learner dashboard header', () => {
expect(el.instance.findByType(LearnerDashboardHeader).length).toEqual(1); const learnerDashboardHeader = screen.getByText('LearnerDashboardHeader');
expect(learnerDashboardHeader).toBeInTheDocument();
}); });
it('wraps the header and main components in an AppWrapper widget container', () => { it('wraps the header and main components in an AppWrapper widget container', () => {
const container = el.instance.findByType(AppWrapper)[0]; const appWrapper = screen.getByText('LearnerDashboardHeader').parentElement;
expect(container.children[0].type).toEqual('LearnerDashboardHeader'); expect(appWrapper).toHaveClass('AppWrapper');
expect(container.children[1].type).toEqual('main'); expect(appWrapper.children[1].id).toEqual('main');
});
it('displays footer slot', () => {
const footerSlot = screen.getByText('FooterSlot');
expect(footerSlot).toBeInTheDocument();
}); });
}; };
describe('no network failure', () => { describe('no network failure', () => {
beforeAll(() => { beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false); reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({}); getConfig.mockReturnValue({});
el = shallow(<App />); render(<IntlProvider locale="en"><App /></IntlProvider>);
}); });
runBasicTests(); runBasicTests();
it('loads dashboard', () => { it('loads dashboard', () => {
const main = el.instance.findByType('main')[0]; const dashboard = screen.getByText('Dashboard');
expect(main.children.length).toEqual(1); expect(dashboard).toBeInTheDocument();
const dashboard = main.children[0].el;
expect(dashboard.type).toEqual('Dashboard');
expect(dashboard).toEqual(shallow(<Dashboard />));
}); });
}); });
describe('no network failure with optimizely url', () => { describe('no network failure with optimizely url', () => {
beforeAll(() => { beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false); reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' }); getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
el = shallow(<App />); render(<IntlProvider locale="en"><App /></IntlProvider>);
}); });
runBasicTests(); runBasicTests();
it('loads dashboard', () => { it('loads dashboard', () => {
const main = el.instance.findByType('main')[0]; const dashboard = screen.getByText('Dashboard');
expect(main.children.length).toEqual(1); expect(dashboard).toBeInTheDocument();
const dashboard = main.children[0].el;
expect(dashboard.type).toEqual('Dashboard');
expect(dashboard).toEqual(shallow(<Dashboard />));
}); });
}); });
describe('no network failure with optimizely project id', () => { describe('no network failure with optimizely project id', () => {
beforeAll(() => { beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false); reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' }); getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
el = shallow(<App />); render(<IntlProvider locale="en"><App /></IntlProvider>);
}); });
runBasicTests(); runBasicTests();
it('loads dashboard', () => { it('loads dashboard', () => {
const main = el.instance.findByType('main')[0]; const dashboard = screen.getByText('Dashboard');
expect(main.children.length).toEqual(1); expect(dashboard).toBeInTheDocument();
const dashboard = main.children[0].el;
expect(dashboard.type).toEqual('Dashboard');
expect(dashboard).toEqual(shallow(<Dashboard />));
}); });
}); });
describe('initialize failure', () => { describe('initialize failure', () => {
beforeAll(() => { beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize); reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
getConfig.mockReturnValue({}); getConfig.mockReturnValue({});
el = shallow(<App />); render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
}); });
runBasicTests(); runBasicTests();
it('loads error page', () => { it('loads error page', () => {
const main = el.instance.findByType('main')[0]; const alert = screen.getByRole('alert');
expect(main.children.length).toEqual(1); expect(alert).toBeInTheDocument();
const alert = main.children[0]; const errorPage = screen.getByText('ErrorPage');
expect(alert.type).toEqual('Alert'); expect(errorPage).toBeInTheDocument();
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', () => { describe('refresh failure', () => {
beforeAll(() => { beforeEach(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList); reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
getConfig.mockReturnValue({}); getConfig.mockReturnValue({});
el = shallow(<App />); render(<IntlProvider locale="en"><App /></IntlProvider>);
}); });
runBasicTests(); runBasicTests();
it('loads error page', () => { it('loads error page', () => {
const main = el.instance.findByType('main')[0]; const alert = screen.getByRole('alert');
expect(main.children.length).toEqual(1); expect(alert).toBeInTheDocument();
const alert = main.children[0]; const errorPage = screen.getByText('ErrorPage');
expect(alert.type).toEqual('Alert'); expect(errorPage).toBeInTheDocument();
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 }));
}); });
}); });
}); });

View File

@@ -1,153 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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
id="main"
>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
</AppWrapper>
<FooterSlot />
</div>
</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
id="main"
>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
</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"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main
id="main"
>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
</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"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main
id="main"
>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
</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
id="main"
>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
</AppWrapper>
<FooterSlot />
</div>
</Fragment>
`;

View File

@@ -1,43 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
<UNDEFINED>
<ErrorPage
message="test-error-message"
/>
</UNDEFINED>
`;
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<UNDEFINED>
<AppProvider
store={
{
"redux": "store",
}
}
>
<NoticesWrapper>
<Routes>
<Route
element={
<PageWrap>
<App />
</PageWrap>
}
path="/"
/>
<Route
element={
<Navigate
replace={true}
to="/"
/>
}
path="*"
/>
</Routes>
</NoticesWrapper>
</AppProvider>
</UNDEFINED>
`;

View File

@@ -1,27 +1,27 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { Alert } from '@openedx/paragon';
import Banner from './Banner'; import Banner from './Banner';
describe('Banner', () => { describe('Banner component', () => {
const props = { it('renders children content', () => {
children: 'Hello, world!', render(<Banner>Test content</Banner>);
}; expect(screen.getByText('Test content')).toBeInTheDocument();
describe('snapshot', () => { });
test('renders default banner', () => {
const wrapper = shallow(<Banner {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('renders with variants', () => {
const wrapper = shallow(<Banner {...props} variant="success" />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.findByType(Alert)[0].props.variant).toEqual('success'); it('uses default props correctly', () => {
}); render(<Banner>Test content</Banner>);
test('renders with custom class', () => { const banner = screen.getByRole('alert');
const wrapper = shallow(<Banner {...props} className="custom-class" />); expect(banner).toHaveClass('mb-0');
expect(wrapper.snapshot).toMatchSnapshot(); });
});
it('accepts custom variant prop', () => {
render(<Banner variant="success">Test content</Banner>);
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-success');
});
it('accepts custom className prop', () => {
render(<Banner className="custom-class">Test content</Banner>);
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('custom-class');
}); });
}); });

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { useIntl } from 'react-intl'; import { useIntl } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils'; import { StrictDict } from 'utils';
import { getNotices } from './api'; import { getNotices } from './api';

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { MockUseState } from 'testUtils'; import { MockUseState, formatMessage } from 'testUtils';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { getNotices } from './api'; import { getNotices } from './api';
@@ -8,13 +8,23 @@ import * as hooks from './hooks';
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() })); jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
jest.mock('./api', () => ({ getNotices: jest.fn() })); jest.mock('./api', () => ({ getNotices: jest.fn() }));
const mockFormatMessage = jest.fn(message => message.defaultMessage || 'translated-string');
jest.mock('react-intl', () => ({ jest.mock('react', () => ({
useIntl: () => ({ ...jest.requireActual('react'),
formatMessage: mockFormatMessage, useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
}), useContext: jest.fn(context => context),
})); }));
jest.mock('@edx/frontend-platform/i18n', () => {
const { formatMessage: fn } = jest.requireActual('testUtils');
return {
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({
formatMessage: fn,
}),
};
});
getConfig.mockReturnValue({ ENABLE_NOTICES: true }); getConfig.mockReturnValue({ ENABLE_NOTICES: true });
const state = new MockUseState(hooks); const state = new MockUseState(hooks);
@@ -40,7 +50,7 @@ describe('NoticesWrapper hooks', () => {
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false }); getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
hooks.useNoticesWrapperData(); hooks.useNoticesWrapperData();
const [cb, prereqs] = React.useEffect.mock.calls[0]; const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected, mockFormatMessage]); expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb(); cb();
expect(getNotices).not.toHaveBeenCalled(); expect(getNotices).not.toHaveBeenCalled();
}); });
@@ -49,7 +59,7 @@ describe('NoticesWrapper hooks', () => {
hooks.useNoticesWrapperData(); hooks.useNoticesWrapperData();
expect(React.useEffect).toHaveBeenCalled(); expect(React.useEffect).toHaveBeenCalled();
const [cb, prereqs] = React.useEffect.mock.calls[0]; const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected, mockFormatMessage]); expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb(); cb();
expect(getNotices).toHaveBeenCalled(); expect(getNotices).toHaveBeenCalled();
const { onLoad } = getNotices.mock.calls[0][0]; const { onLoad } = getNotices.mock.calls[0][0];
@@ -65,7 +75,7 @@ describe('NoticesWrapper hooks', () => {
window.location = { replace: jest.fn(), href: 'test-old-href' }; window.location = { replace: jest.fn(), href: 'test-old-href' };
hooks.useNoticesWrapperData(); hooks.useNoticesWrapperData();
const [cb, prereqs] = React.useEffect.mock.calls[0]; const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected, mockFormatMessage]); expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb(); cb();
expect(getNotices).toHaveBeenCalled(); expect(getNotices).toHaveBeenCalled();
const { onLoad } = getNotices.mock.calls[0][0]; const { onLoad } = getNotices.mock.calls[0][0];

View File

@@ -1,5 +1,4 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import useNoticesWrapperData from './hooks'; import useNoticesWrapperData from './hooks';
import NoticesWrapper from '.'; import NoticesWrapper from '.';
@@ -7,30 +6,31 @@ import NoticesWrapper from '.';
jest.mock('./hooks', () => jest.fn()); jest.mock('./hooks', () => jest.fn());
const hookProps = { isRedirected: false }; const hookProps = { isRedirected: false };
useNoticesWrapperData.mockReturnValue(hookProps);
let el;
const children = [<b key={1}>some</b>, <i key={2}>children</i>]; const children = [<b key={1}>some</b>, <i key={2}>children</i>];
describe('NoticesWrapper component', () => { describe('NoticesWrapper component', () => {
beforeEach(() => {
useNoticesWrapperData.mockClear();
});
describe('behavior', () => { describe('behavior', () => {
it('initializes hooks', () => { it('initializes hooks', () => {
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>); useNoticesWrapperData.mockReturnValue(hookProps);
render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(useNoticesWrapperData).toHaveBeenCalledWith(); expect(useNoticesWrapperData).toHaveBeenCalledWith();
}); });
}); });
describe('output', () => { describe('output', () => {
it('does not show children if redirected', () => { it('does not show children if redirected', () => {
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true }); useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>); render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(el.instance.children.length).toEqual(0); expect(screen.queryByText('some')).not.toBeInTheDocument();
expect(screen.queryByText('children')).not.toBeInTheDocument();
}); });
it('shows children if not redirected', () => { it('shows children if not redirected', () => {
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>); useNoticesWrapperData.mockReturnValue(hookProps);
expect(el.instance.children.length).toEqual(2); render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(el.instance.children[0].type).toEqual(shallow(children[0]).type); expect(screen.getByText('some')).toBeInTheDocument();
expect(el.instance.props).toEqual(shallow(children[0]).props); expect(screen.getByText('children')).toBeInTheDocument();
expect(el.instance.children[1].type).toEqual(shallow(children[1]).type);
expect(el.instance.props).toEqual(shallow(children[1]).props);
}); });
}); });
}); });

View File

@@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Banner snapshot renders default banner 1`] = `
<Alert
className="mb-0"
icon={[MockFunction icons.Info]}
variant="info"
>
Hello, world!
</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"
icon={[MockFunction icons.Info]}
variant="success"
>
Hello, world!
</Alert>
`;

View File

@@ -2,6 +2,7 @@ const configuration = {
// BASE_URL: process.env.BASE_URL, // BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL, LMS_BASE_URL: process.env.LMS_BASE_URL,
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL, ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
CREDIT_PURCHASE_URL: process.env.CREDIT_PURCHASE_URL,
// LOGIN_URL: process.env.LOGIN_URL, // LOGIN_URL: process.env.LOGIN_URL,
// LOGOUT_URL: process.env.LOGOUT_URL, // LOGOUT_URL: process.env.LOGOUT_URL,
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH, // CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,

View File

@@ -1,10 +1,8 @@
@import "@openedx/paragon/scss/core/core";
.course-card { .course-card {
.card { .card {
.pgn__card-wrapper-image-cap.vertical { .pgn__card-wrapper-image-cap.vertical {
display: flex; display: flex;
min-height: $card-image-vertical-max-height; min-height: var(--pgn-size-card-image-vertical-max-height);
} }
.pgn__card-image-cap { .pgn__card-image-cap {
border-bottom-left-radius: 0 !important; border-bottom-left-radius: 0 !important;
@@ -53,11 +51,11 @@
> .alert { > .alert {
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
padding: map-get($spacers, 3) map-get($spacers, 4); padding: var(--pgn-spacing-spacer-3) var(--pgn-spacing-spacer-4);
&:last-of-type { &:last-of-type {
border-bottom-left-radius: $alert-border-radius; border-bottom-left-radius: var(--pgn-size-alert-border-radius);
border-bottom-right-radius: $alert-border-radius; border-bottom-right-radius: var(--pgn-size-alert-border-radius);
} }
} }

View File

@@ -1,111 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCard component snapshot: collapsed 1`] = `
<div
className="mb-4.5 course-card"
data-testid="CourseCard"
id="test-card-id"
>
<Card
orientation="vertical"
>
<div
className="d-flex flex-column w-100"
>
<div>
<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>
`;
exports[`CourseCard component snapshot: not collapsed 1`] = `
<div
className="mb-4.5 course-card"
data-testid="CourseCard"
id="test-card-id"
>
<Card
orientation="horizontal"
>
<div
className="d-flex flex-column w-100"
>
<div
className="d-flex"
>
<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>
<CourseCardBanners
cardId="test-card-id"
/>
</div>
</Card>
</div>
`;

View File

@@ -1,14 +0,0 @@
// 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"
/>
`;

View File

@@ -1,6 +1,33 @@
import { useWindowSize, breakpoints } from '@openedx/paragon'; import { useWindowSize, breakpoints } from '@openedx/paragon';
import useIsCollapsed from './hooks'; import useIsCollapsed from './hooks';
jest.mock('@openedx/paragon', () => ({
...jest.requireActual('@openedx/paragon'),
useWindowSize: jest.fn(),
breakpoints: {
extraSmall: {
minWidth: 0,
maxWidth: 575,
},
small: {
minWidth: 576,
maxWidth: 767,
},
medium: {
minWidth: 768,
maxWidth: 991,
},
large: {
minWidth: 992,
maxWidth: 1199,
},
extraLarge: {
minWidth: 1200,
maxWidth: 100000,
},
},
}));
describe('useIsCollapsed', () => { describe('useIsCollapsed', () => {
it('returns true only when it is between medium and small', () => { it('returns true only when it is between medium and small', () => {
// make sure all three breakpoints gap is large enough for test // make sure all three breakpoints gap is large enough for test

View File

@@ -1,5 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import ActionButton from '.'; import ActionButton from '.';
import useIsCollapsed from './hooks'; import useIsCollapsed from './hooks';
@@ -8,18 +7,22 @@ jest.mock('./hooks', () => jest.fn());
describe('ActionButton', () => { describe('ActionButton', () => {
const props = { const props = {
arbitary: 'props', className: 'custom-class',
children: 'Test',
}; };
describe('snapshot', () => {
test('is collapsed', () => { it('is collapsed', async () => {
useIsCollapsed.mockReturnValueOnce(true); useIsCollapsed.mockReturnValue(true);
const wrapper = shallow(<ActionButton {...props} />); render(<ActionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot(); const button = screen.getByRole('button', { name: 'Test' });
}); expect(button).toHaveClass('btn-sm', 'custom-class');
test('is not collapsed', () => { });
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<ActionButton {...props} />); it('is not collapsed', () => {
expect(wrapper.snapshot).toMatchSnapshot(); useIsCollapsed.mockReturnValue(false);
}); render(<ActionButton {...props} />);
const button = screen.getByRole('button', { name: 'Test' });
expect(button).toBeInTheDocument();
expect(button).not.toHaveClass('size', 'sm');
}); });
}); });

View File

@@ -1,5 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import track from 'tracking'; import track from 'tracking';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
@@ -18,10 +19,11 @@ jest.mock('hooks', () => ({
useTrackCourseEvent: jest.fn(), useTrackCourseEvent: jest.fn(),
}, },
})); }));
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper; jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const homeUrl = 'home-url'; const homeUrl = 'home-url';
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl }); reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`; const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
@@ -30,56 +32,57 @@ reduxHooks.useTrackCourseEvent.mockImplementation(
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }), (eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
); );
const props = {
cardId: 'cardId',
};
const renderComponent = () => render(<IntlProvider locale="en"><BeginCourseButton {...props} /></IntlProvider>);
describe('BeginCourseButton', () => { describe('BeginCourseButton', () => {
const props = {
cardId: 'cardId',
};
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
describe('behavior', () => { describe('initiliaze hooks', () => {
it('initializes course run data with cardId', () => { it('initializes course run data with cardId', () => {
wrapper = shallow(<BeginCourseButton {...props} />); renderComponent();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId); expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
}); });
it('loads exec education path param', () => { it('loads exec education path param', () => {
wrapper = shallow(<BeginCourseButton {...props} />); renderComponent();
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId); expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
}); });
it('loads disabled states for begin action from action hooks', () => { it('loads disabled states for begin action from action hooks', () => {
wrapper = shallow(<BeginCourseButton {...props} />); renderComponent();
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId); expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
}); });
}); });
describe('snapshot', () => { describe('behavior', () => {
describe('disabled', () => { describe('disabled', () => {
beforeEach(() => {
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be disabled', () => { it('should be disabled', () => {
expect(wrapper.instance.props.disabled).toEqual(true); useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
renderComponent();
const button = screen.getByRole('button', { name: 'Begin Course' });
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });
describe('enabled', () => { describe('enabled', () => {
beforeEach(() => {
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be enabled', () => { it('should be enabled', () => {
expect(wrapper.instance.props.disabled).toEqual(false); renderComponent();
const button = screen.getByRole('button', { name: 'Begin Course' });
expect(button).not.toHaveClass('disabled');
expect(button).not.toHaveAttribute('aria-disabled', 'true');
}); });
it('should track enter course clicked event on click, with exec ed param', () => { it('should track enter course clicked event on click, with exec ed param', async () => {
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent( renderComponent();
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Begin Course' });
user.click(button);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked, track.course.enterCourseClicked,
props.cardId, props.cardId,
homeUrl + execEdPath(props.cardId), homeUrl + execEdPath(props.cardId),
)); );
}); });
}); });
}); });

View File

@@ -1,4 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import track from 'tracking'; import track from 'tracking';
@@ -19,7 +21,8 @@ jest.mock('hooks', () => ({
}, },
})); }));
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false }))); jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const resumeUrl = 'resume-url'; const resumeUrl = 'resume-url';
reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl }); reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
@@ -29,55 +32,52 @@ reduxHooks.useTrackCourseEvent.mockImplementation(
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }), (eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
); );
let wrapper;
describe('ResumeButton', () => { describe('ResumeButton', () => {
const props = { const props = {
cardId: 'cardId', cardId: 'cardId',
}; };
describe('behavior', () => { describe('initialize hooks', () => {
beforeEach(() => render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>));
it('initializes course run data with cardId', () => { it('initializes course run data with cardId', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId); expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
}); });
it('loads exec education path param', () => { it('loads exec education path param', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId); expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
}); });
it('loads disabled states for resume action from action hooks', () => { it('loads disabled states for resume action from action hooks', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId); expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
}); });
}); });
describe('snapshot', () => { describe('behavior', () => {
describe('disabled', () => { describe('disabled', () => {
beforeEach(() => { beforeEach(() => {
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true }); useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
}); });
it('should be disabled', () => { it('should be disabled', () => {
expect(wrapper.instance.props.disabled).toEqual(true); render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'Resume' });
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });
describe('enabled', () => { describe('enabled', () => {
beforeEach(() => {
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be enabled', () => { it('should be enabled', () => {
expect(wrapper.instance.props.disabled).toEqual(false); render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'Resume' });
expect(button).toBeInTheDocument();
expect(button).not.toHaveClass('disabled');
expect(button).not.toHaveAttribute('aria-disabled', 'true');
}); });
it('should track enter course clicked event on click, with exec ed param', () => { it('should track enter course clicked event on click, with exec ed param', async () => {
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent( render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Resume' });
user.click(button);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked, track.course.enterCourseClicked,
props.cardId, props.cardId,
resumeUrl + execEdPath(props.cardId), resumeUrl + execEdPath(props.cardId),
)); );
}); });
}); });
}); });

View File

@@ -1,4 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
@@ -7,28 +9,35 @@ import SelectSessionButton from './SelectSessionButton';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useUpdateSelectSessionModalCallback: () => jest.fn().mockName('mockOpenSessionModal'), useUpdateSelectSessionModalCallback: jest.fn(),
}, },
})); }));
jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false }))); jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false })));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper; jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
describe('SelectSessionButton', () => { describe('SelectSessionButton', () => {
const props = { cardId: 'cardId' }; const props = { cardId: 'cardId' };
it('default render', () => { it('default render', () => {
wrapper = shallow(<SelectSessionButton {...props} />); render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
expect(wrapper.snapshot).toMatchSnapshot(); const button = screen.getByRole('button', { name: 'Select Session' });
expect(wrapper.instance.props.disabled).toEqual(false); expect(button).toBeInTheDocument();
expect(wrapper.instance.props.onClick.getMockName()).toEqual(
reduxHooks.useUpdateSelectSessionModalCallback().getMockName(),
);
}); });
test('disabled states', () => { describe('if useActionDisabledState is false', () => {
useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true }); it('should disabled Select Session', () => {
wrapper = shallow(<SelectSessionButton {...props} />); useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true });
expect(wrapper.snapshot).toMatchSnapshot(); render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
expect(wrapper.instance.props.disabled).toEqual(true); const button = screen.getByRole('button', { name: 'Select Session' });
expect(button).toBeDisabled();
});
});
describe('on click', () => {
it('should call openSessionModal', async () => {
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Select Session' });
await user.click(button);
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(props.cardId);
});
}); });
}); });

View File

@@ -1,4 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import track from 'tracking'; import track from 'tracking';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
@@ -14,32 +16,40 @@ jest.mock('tracking', () => ({
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })), useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
useTrackCourseEvent: jest.fn( useTrackCourseEvent: jest.fn(),
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
),
}, },
})); }));
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false }))); jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const defaultProps = { cardId: 'cardId' }; const defaultProps = { cardId: 'cardId' };
const homeUrl = 'homeUrl'; const homeUrl = 'homeUrl';
describe('ViewCourseButton', () => { describe('ViewCourseButton', () => {
test('learner can view course', () => { it('learner can view course', async () => {
const wrapper = shallow(<ViewCourseButton {...defaultProps} />); render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
expect(wrapper.snapshot).toMatchSnapshot(); const button = screen.getByRole('button', { name: 'View Course' });
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent( expect(button).toBeInTheDocument();
expect(button).not.toHaveClass('disabled');
expect(button).not.toHaveAttribute('aria-disabled', 'true');
});
it('calls trackCourseEvent on click', async () => {
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'View Course' });
await user.click(button);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked, track.course.enterCourseClicked,
defaultProps.cardId, defaultProps.cardId,
homeUrl, homeUrl,
)); );
expect(wrapper.instance.props.disabled).toEqual(false);
}); });
test('learner cannot view course', () => { it('learner cannot view course', () => {
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true }); useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
const wrapper = shallow(<ViewCourseButton {...defaultProps} />); render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
expect(wrapper.snapshot).toMatchSnapshot(); const button = screen.getByRole('button', { name: 'View Course' });
expect(wrapper.instance.props.disabled).toEqual(true); expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "home-urlexec-ed-tracking-path=cardId",
},
}
}
>
Begin Course
</ActionButton>
`;
exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "home-urlexec-ed-tracking-path=cardId",
},
}
}
>
Begin Course
</ActionButton>
`;

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResumeButton snapshot disabled snapshot 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "resume-urlexec-ed-tracking-path=cardId",
},
}
}
>
Resume
</ActionButton>
`;
exports[`ResumeButton snapshot enabled snapshot 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "resume-urlexec-ed-tracking-path=cardId",
},
}
}
>
Resume
</ActionButton>
`;

View File

@@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectSessionButton default render 1`] = `
<ActionButton
disabled={false}
onClick={[MockFunction mockOpenSessionModal]}
>
Select Session
</ActionButton>
`;
exports[`SelectSessionButton disabled states 1`] = `
<ActionButton
disabled={true}
onClick={[MockFunction mockOpenSessionModal]}
>
Select Session
</ActionButton>
`;

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ViewCourseButton learner can view course 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "homeUrl",
},
}
}
>
View Course
</ActionButton>
`;
exports[`ViewCourseButton learner cannot view course 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"url": "homeUrl",
},
}
}
>
View Course
</ActionButton>
`;

View File

@@ -1,13 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton';
import ResumeButton from './ResumeButton';
import ViewCourseButton from './ViewCourseButton';
import CourseCardActions from '.'; import CourseCardActions from '.';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
@@ -19,16 +12,15 @@ jest.mock('hooks', () => ({
}, },
})); }));
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton'); jest.mock('plugin-slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
jest.mock('./SelectSessionButton', () => 'SelectSessionButton'); jest.mock('./SelectSessionButton', () => jest.fn(() => <div>SelectSessionButton</div>));
jest.mock('./ViewCourseButton', () => 'ViewCourseButton'); jest.mock('./ViewCourseButton', () => jest.fn(() => <div>ViewCourseButton</div>));
jest.mock('./BeginCourseButton', () => 'BeginCourseButton'); jest.mock('./BeginCourseButton', () => jest.fn(() => <div>BeginCourseButton</div>));
jest.mock('./ResumeButton', () => 'ResumeButton'); jest.mock('./ResumeButton', () => jest.fn(() => <div>ResumeButton</div>));
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const props = { cardId }; const props = { cardId };
let el;
describe('CourseCardActions', () => { describe('CourseCardActions', () => {
const mockHooks = ({ const mockHooks = ({
isEntitlement = false, isEntitlement = false,
@@ -44,13 +36,11 @@ describe('CourseCardActions', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted }); reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted });
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading }); reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
}; };
const render = () => { const renderComponent = () => render(<CourseCardActions {...props} />);
el = shallow(<CourseCardActions {...props} />); describe('hooks', () => {
};
describe('behavior', () => {
it('initializes redux hooks', () => { it('initializes redux hooks', () => {
mockHooks(); mockHooks();
render(); renderComponent();
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
@@ -60,36 +50,44 @@ describe('CourseCardActions', () => {
describe('entitlement course', () => { describe('entitlement course', () => {
it('renders ViewCourseButton if fulfilled', () => { it('renders ViewCourseButton if fulfilled', () => {
mockHooks({ isEntitlement: true, isFulfilled: true }); mockHooks({ isEntitlement: true, isFulfilled: true });
render(); renderComponent();
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId); const ViewCourseButton = screen.getByText('ViewCourseButton');
expect(ViewCourseButton).toBeInTheDocument();
}); });
it('renders SelectSessionButton if not fulfilled', () => { it('renders SelectSessionButton if not fulfilled', () => {
mockHooks({ isEntitlement: true }); mockHooks({ isEntitlement: true });
render(); renderComponent();
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId); const SelectSessionButton = screen.getByText('SelectSessionButton');
expect(SelectSessionButton).toBeInTheDocument();
}); });
}); });
describe('not entitlement, verified, or exec ed', () => { describe('not entitlement, verified, or exec ed', () => {
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => { it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
mockHooks({ isArchived: true }); mockHooks({ isArchived: true });
render(); renderComponent();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId); const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId); expect(CourseCardActionSlot).toBeInTheDocument();
const ViewCourseButton = screen.getByText('ViewCourseButton');
expect(ViewCourseButton).toBeInTheDocument();
}); });
describe('unstarted courses', () => { describe('unstarted courses', () => {
it('renders CourseCardActionSlot and BeginCourseButton', () => { it('renders CourseCardActionSlot and BeginCourseButton', () => {
mockHooks(); mockHooks();
render(); renderComponent();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId); const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId); expect(CourseCardActionSlot).toBeInTheDocument();
const BeginCourseButton = screen.getByText('BeginCourseButton');
expect(BeginCourseButton).toBeInTheDocument();
}); });
}); });
describe('active courses (started, and not archived)', () => { describe('active courses (started, and not archived)', () => {
it('renders CourseCardActionSlot and ResumeButton', () => { it('renders CourseCardActionSlot and ResumeButton', () => {
mockHooks({ hasStarted: true }); mockHooks({ hasStarted: true });
render(); renderComponent();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId); const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
expect(el.instance.findByType(ResumeButton)[0].props.cardId).toEqual(cardId); expect(CourseCardActionSlot).toBeInTheDocument();
const ResumeButton = screen.getByText('ResumeButton');
expect(ResumeButton).toBeInTheDocument();
}); });
}); });
}); });

View File

@@ -1,8 +1,8 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import CertificateBanner from './CertificateBanner'; import CertificateBanner from './CertificateBanner';
import messages from './messages';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
utilHooks: { utilHooks: {
@@ -17,28 +17,28 @@ jest.mock('hooks', () => ({
}, },
})); }));
jest.mock('components/Banner', () => 'Banner'); const defaultCertificate = {
availableDate: '10/20/3030',
isRestricted: false,
isDownloadable: false,
isEarnedButUnavailable: false,
};
const defaultEnrollment = {
isAudit: false,
isVerified: false,
};
const defaultCourseRun = { isArchived: false };
const defaultGrade = { isPassing: false };
const defaultPlatformSettings = {};
const props = { cardId: 'cardId' };
const supportEmail = 'suport@email.com';
const billingEmail = 'billing@email.com';
describe('CertificateBanner', () => { describe('CertificateBanner', () => {
const props = { cardId: 'cardId' };
reduxHooks.useCardCourseRunData.mockReturnValue({ reduxHooks.useCardCourseRunData.mockReturnValue({
minPassingGrade: 0.8, minPassingGrade: 0.8,
progressUrl: 'progressUrl', progressUrl: 'progressUrl',
}); });
const defaultCertificate = {
availableDate: '10/20/3030',
isRestricted: false,
isDownloadable: false,
isEarnedButUnavailable: false,
};
const defaultEnrollment = {
isAudit: false,
isVerified: false,
};
const defaultCourseRun = { isArchived: false };
const defaultGrade = { isPassing: false };
const defaultPlatformSettings = {};
const createWrapper = ({ const createWrapper = ({
certificate = {}, certificate = {},
enrollment = {}, enrollment = {},
@@ -51,177 +51,192 @@ describe('CertificateBanner', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment }); reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun }); reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings }); reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings });
return shallow(<CertificateBanner {...props} />); return render(<IntlProvider locale="en"><CertificateBanner {...props} /></IntlProvider>);
}; };
/** TODO: Update tests to validate snapshots **/ beforeEach(() => {
describe('snapshot', () => { jest.clearAllMocks();
test('is restricted', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted with support email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail: 'suport@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted with billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with support email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).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.snapshot).toMatchSnapshot();
});
test('is passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: true },
certificate: { isDownloadable: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: false },
certificate: { isDownloadable: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and audit', () => {
const wrapper = createWrapper({
enrollment: {
isAudit: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and has finished', () => {
const wrapper = createWrapper({
courseRun: { isArchived: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and not audit and not finished', () => {
const wrapper = createWrapper({});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and is earned but unavailable', () => {
const wrapper = createWrapper({
grade: {
isPassing: true,
},
certificate: {
isEarnedButUnavailable: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and not downloadable render empty', () => {
const wrapper = createWrapper({
grade: {
isPassing: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
}); });
describe('behavior', () => { it('is restricted', () => {
it('is restricted', () => { createWrapper({
const wrapper = createWrapper({ certificate: {
certificate: { isRestricted: true,
isRestricted: true, },
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
const bannerMessage = wrapper.instance.findByType('format-message-function').map(el => el.props.message.defaultMessage).join('\n');
expect(bannerMessage).toEqual(messages.certRestricted.defaultMessage);
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
}); });
it('is restricted and verified', () => { const banner = screen.getByRole('alert');
const wrapper = createWrapper({ expect(banner).toBeInTheDocument();
certificate: { const msg = screen.getByText((text) => text.includes('please let us know.'));
isRestricted: true, expect(msg).toBeInTheDocument();
}, expect(msg).not.toContain(supportEmail);
enrollment: { });
isVerified: true, it('is restricted with support email', () => {
}, createWrapper({
platformSettings: { certificate: {
supportEmail: 'suport@email', isRestricted: true,
billingEmail: 'billing@email', },
}, platformSettings: {
}); supportEmail,
const bannerMessage = wrapper.instance.findByType('format-message-function').map(el => el.props.message.defaultMessage).join('\n'); },
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
expect(bannerMessage).toContain(messages.certRefundContactBilling.defaultMessage);
}); });
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
const msg = screen.getByText((text) => text.includes(supportEmail));
expect(msg).toBeInTheDocument();
});
it('is restricted with billing email but not verified', () => {
createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
billingEmail,
},
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-danger');
const msg = screen.queryByText((text) => text.includes(billingEmail));
expect(msg).not.toBeInTheDocument();
});
it('is restricted and verified', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
const restrictedMsg = screen.getByText((text) => text.includes('please let us know.'));
expect(restrictedMsg).toBeInTheDocument();
const refundMsg = screen.getByText((text) => text.includes('If you would like a refund'));
expect(refundMsg).toBeInTheDocument();
});
it('is restricted and verified with support email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail,
},
});
const restrictedMsg = screen.getByText((text) => text.includes(supportEmail));
expect(restrictedMsg).toBeInTheDocument();
const refundMsg = screen.getByText((text) => text.includes('If you would like a refund'));
expect(refundMsg).toBeInTheDocument();
expect(refundMsg.innerHTML).not.toContain(billingEmail);
});
it('is restricted and verified with billing email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
billingEmail,
},
});
const restrictedMsg = screen.queryByText((text) => text.includes('please let us know.'));
expect(restrictedMsg).toBeInTheDocument();
expect(restrictedMsg.innerHTML).not.toContain(supportEmail);
const refundMsg = screen.getByText((text) => text.includes(billingEmail));
expect(refundMsg).toBeInTheDocument();
});
it('is restricted and verified with support and billing email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail,
billingEmail,
},
});
const restrictedMsg = screen.getByText((text) => text.includes(supportEmail));
expect(restrictedMsg).toBeInTheDocument();
const refundMsg = screen.getByText((text) => text.includes(billingEmail));
expect(refundMsg).toBeInTheDocument();
});
it('is passing and is downloadable', () => {
createWrapper({
grade: { isPassing: true },
certificate: { isDownloadable: true },
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-success');
const readyMsg = screen.getByText((text) => text.includes('Congratulations.'));
expect(readyMsg).toBeInTheDocument();
});
it('not passing and is downloadable', () => {
createWrapper({
grade: { isPassing: false },
certificate: { isDownloadable: true },
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-success');
const readyMsg = screen.getByText((text) => text.includes('Congratulations.'));
expect(readyMsg).toBeInTheDocument();
});
it('not passing and audit', () => {
createWrapper({
enrollment: {
isAudit: true,
},
});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-info');
const auditMsg = screen.getByText((text) => text.includes('Grade required to pass the course:'));
expect(auditMsg).toBeInTheDocument();
});
it('not passing and has finished', () => {
createWrapper({
courseRun: { isArchived: true },
});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-warning');
const archivedMsg = screen.getByText('You are not eligible for a certificate.');
expect(archivedMsg).toBeInTheDocument();
});
it('not passing and not audit and not finished', () => {
createWrapper({});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-warning');
const msg = screen.getByText((text) => text.includes('Grade required for a certificate'));
expect(msg).toBeInTheDocument();
});
it('is passing and is earned but unavailable', () => {
createWrapper({
grade: {
isPassing: true,
},
certificate: {
isEarnedButUnavailable: true,
},
});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-info');
const earnedMsg = screen.getByText((text) => text.includes('Your grade and certificate will be ready after'));
expect(earnedMsg).toBeInTheDocument();
});
it('is passing and not downloadable render empty', () => {
createWrapper({
grade: {
isPassing: true,
},
});
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
}); });
}); });

View File

@@ -1,6 +1,5 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils'; import { formatMessage } from 'testUtils';
@@ -8,7 +7,6 @@ import { CourseBanner } from './CourseBanner';
import messages from './messages'; import messages from './messages';
jest.mock('components/Banner', () => 'Banner');
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
utilHooks: { utilHooks: {
useFormatDate: () => date => date, useFormatDate: () => date => date,
@@ -19,9 +17,7 @@ jest.mock('hooks', () => ({
}, },
})); }));
const cardId = 'my-test-course-number'; const cardId = 'test-card-id';
let el;
const enrollmentData = { const enrollmentData = {
isVerified: false, isVerified: false,
@@ -38,7 +34,7 @@ const courseRunData = {
marketingUrl: 'marketing-url', marketingUrl: 'marketing-url',
}; };
const render = (overrides = {}) => { const renderCourseBanner = (overrides = {}) => {
const { const {
courseRun = {}, courseRun = {},
enrollment = {}, enrollment = {},
@@ -51,81 +47,58 @@ const render = (overrides = {}) => {
...enrollmentData, ...enrollmentData,
...enrollment, ...enrollment,
}); });
el = shallow(<CourseBanner cardId={cardId} />); return render(<IntlProvider locale="en"><CourseBanner cardId={cardId} /></IntlProvider>);
}; };
describe('CourseBanner', () => { describe('CourseBanner', () => {
test('initializes data with course number from enrollment, course and course run data', () => { it('initializes data with course number from enrollment, course and course run data', () => {
render(); renderCourseBanner();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
}); });
test('no display if learner is verified', () => { it('no display if learner is verified', () => {
render({ enrollment: { isVerified: true } }); renderCourseBanner({ enrollment: { isVerified: true } });
expect(el.isEmptyRender()).toEqual(true); expect(screen.queryByRole('alert')).toBeNull();
}); });
describe('audit access expired', () => { describe('audit access expired', () => {
beforeEach(() => { it('should display correct message and link', () => {
render({ enrollment: { isAuditAccessExpired: true } }); renderCourseBanner({ enrollment: { isAuditAccessExpired: true } });
}); const auditAccessText = screen.getByText(formatMessage(messages.auditAccessExpired));
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => { expect(auditAccessText).toBeInTheDocument();
expect(el.snapshot).toMatchSnapshot(); const auditAccessLink = screen.getByText(formatMessage(messages.findAnotherCourse));
}); expect(auditAccessLink).toBeInTheDocument();
test('messages: auditAccessExpired', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.instance.findByType(Hyperlink)[0].children[0].el).toEqual(messages.findAnotherCourse.defaultMessage);
}); });
}); });
describe('unmet prerequisites', () => { describe('unmet prerequisites', () => {
beforeEach(() => { it('should display correct message', () => {
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } }); renderCourseBanner({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
}); const preReqText = screen.getByText(formatMessage(messages.prerequisitesNotMet));
test('snapshot: unmetPrerequisites', () => { expect(preReqText).toBeInTheDocument();
expect(el.snapshot).toMatchSnapshot();
});
test('messages: prerequisitesNotMet', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.prerequisitesNotMet.defaultMessage);
}); });
}); });
describe('too early', () => { describe('too early', () => {
describe('no start date', () => { describe('no start date', () => {
beforeEach(() => { it('should not display banner', () => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } }, courseRun: { startDate: null } }); renderCourseBanner({ enrollment: { coursewareAccess: { isTooEarly: true } }, courseRun: { startDate: null } });
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
}); });
test('snapshot', () => expect(el.snapshot).toMatchSnapshot());
test('messages', () => expect(el.instance.children).toEqual([]));
}); });
describe('has start date', () => { describe('has start date', () => {
beforeEach(() => { it('should display messages courseHasNotStarted', () => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } } }); renderCourseBanner({ enrollment: { coursewareAccess: { isTooEarly: true } } });
}); const earlyMsg = screen.getByText(
test('snapshot', () => expect(el.snapshot).toMatchSnapshot());
test('messages: courseHasNotStarted', () => {
expect(el.instance.children[0].children[0].el).toContain(
formatMessage(messages.courseHasNotStarted, { startDate: courseRunData.startDate }), formatMessage(messages.courseHasNotStarted, { startDate: courseRunData.startDate }),
); );
expect(earlyMsg).toBeInTheDocument();
}); });
}); });
}); });
describe('staff', () => { describe('staff', () => {
beforeEach(() => { it('should not display banner', () => {
render({ enrollment: { coursewareAccess: { isStaff: true } } }); renderCourseBanner({ enrollment: { coursewareAccess: { isStaff: true } } });
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
}); });
test('snapshot: isStaff', () => {
expect(el.snapshot).toMatchSnapshot();
});
});
test('snapshot: stacking banners', () => {
render({
enrollment: {
coursewareAccess: {
isStaff: true,
isTooEarly: true,
hasUnmetPrerequisites: true,
},
},
});
expect(el.snapshot).toMatchSnapshot();
}); });
}); });

View File

@@ -1,58 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditBanner component render with error state snapshot 1`] = `
<Banner
variant="danger"
>
<p
className="credit-error-msg"
data-testid="credit-error-msg"
>
<format-message-function
message={
{
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
"description": "",
"id": "learner-dash.courseCard.banners.credit.error",
}
}
values={
{
"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"
data-testid="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>
`;

View File

@@ -1,95 +1,65 @@
import React from 'react'; import { screen, render } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { MailtoLink } from '@openedx/paragon';
import hooks from './hooks'; import hooks from './hooks';
import messages from './messages'; import { CreditBanner } from '.';
import CreditBanner from '.';
jest.mock('components/Banner', () => 'Banner');
jest.mock('./hooks', () => ({ jest.mock('./hooks', () => ({
useCreditBannerData: jest.fn(), useCreditBannerData: jest.fn(),
})); }));
let el; describe('CreditBanner', () => {
const cardId = 'test-card-id'; const mockCardId = 'test-card-id';
const mockContentComponent = () => <div data-testid="mock-content">Test Content</div>;
const mockSupportEmail = 'support@test.com';
const ContentComponent = () => 'ContentComponent'; const renderCreditBanner = () => render(
const supportEmail = 'test-support-email'; <IntlProvider locale="en">
<CreditBanner cardId={mockCardId} />
</IntlProvider>,
);
describe('CreditBanner component', () => { beforeEach(() => {
describe('behavior', () => { jest.clearAllMocks();
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.snapshot).toMatchSnapshot();
});
it('passes danger variant to Banner parent', () => {
expect(el.instance.findByType('Banner')[0].props.variant).toEqual('danger');
});
it('includes credit-error-msg with support email link', () => {
expect(el.instance.findByTestId('credit-error-msg')[0].children[0].el).toEqual(shallow(formatMessage(messages.error, {
supportEmailLink: (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>),
})));
});
it('loads ContentComponent with cardId', () => {
expect(el.instance.findByType('ContentComponent')[0].props.cardId).toEqual(cardId);
});
});
describe('with error state with no email', () => { it('should return null if hook returns null', () => {
beforeEach(() => { hooks.useCreditBannerData.mockReturnValue(null);
hooks.useCreditBannerData.mockReturnValue({ renderCreditBanner();
error: true, const banner = screen.queryByRole('alert');
ContentComponent, expect(banner).toBeNull();
}); });
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('includes credit-error-msg without support email link', () => {
expect(el.instance.findByTestId('credit-error-msg')[0].children[0].el).toEqual(formatMessage(messages.errorNoEmail));
});
});
describe('with no error state', () => { it('should render content component without error', () => {
beforeEach(() => { hooks.useCreditBannerData.mockReturnValue({
hooks.useCreditBannerData.mockReturnValue({ ContentComponent: mockContentComponent,
error: false, error: false,
ContentComponent,
supportEmail,
});
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads ContentComponent with cardId', () => {
expect(el.instance.findByType('ContentComponent')[0].props.cardId).toEqual(cardId);
});
}); });
renderCreditBanner();
expect(screen.getByTestId('mock-content')).toBeInTheDocument();
expect(screen.queryByTestId('credit-error-msg')).not.toBeInTheDocument();
});
it('should render error message with support email', () => {
hooks.useCreditBannerData.mockReturnValue({
ContentComponent: mockContentComponent,
error: true,
supportEmail: mockSupportEmail,
});
renderCreditBanner();
expect(screen.getByTestId('credit-error-msg')).toBeInTheDocument();
expect(screen.getByText(mockSupportEmail)).toBeInTheDocument();
});
it('should render error message without support email', () => {
hooks.useCreditBannerData.mockReturnValue({
ContentComponent: mockContentComponent,
error: true,
});
renderCreditBanner();
expect(screen.getByTestId('credit-error-msg')).toBeInTheDocument();
expect(screen.queryByText(mockSupportEmail)).not.toBeInTheDocument();
}); });
}); });

View File

@@ -1,10 +1,8 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils'; import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import messages from './messages'; import messages from './messages';
import ProviderLink from './components/ProviderLink';
import ApprovedContent from './ApprovedContent'; import ApprovedContent from './ApprovedContent';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
@@ -13,10 +11,7 @@ jest.mock('hooks', () => ({
useMasqueradeData: jest.fn(), useMasqueradeData: jest.fn(),
}, },
})); }));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
let el;
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const credit = { const credit = {
providerStatusUrl: 'test-credit-provider-status-url', providerStatusUrl: 'test-credit-provider-status-url',
@@ -26,38 +21,54 @@ reduxHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false }); reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
describe('ApprovedContent component', () => { describe('ApprovedContent component', () => {
beforeEach(() => { describe('hooks', () => {
el = shallow(<ApprovedContent cardId={cardId} />);
});
describe('behavior', () => {
it('initializes credit data with cardId', () => { it('initializes credit data with cardId', () => {
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
}); });
}); });
describe('render', () => { describe('render', () => {
describe('rendered CreditContent component', () => { describe('rendered CreditContent component', () => {
let component; beforeEach(() => {
beforeAll(() => { jest.clearAllMocks();
component = el.instance.findByType('CreditContent'); render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
}); });
test('action.href from credit.providerStatusUrl', () => { it('action.message is formatted viewCredit message', () => {
expect(component[0].props.action.href).toEqual(credit.providerStatusUrl); const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).toBeInTheDocument();
expect(actionButton).toHaveTextContent(formatMessage(messages.viewCredit));
}); });
test('action.message is formatted viewCredit message', () => { it('action.href from credit.providerStatusUrl', () => {
expect(component[0].props.action.message).toEqual(formatMessage(messages.viewCredit)); const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).toHaveAttribute('href', credit.providerStatusUrl);
}); });
test('action.disabled is false', () => { it('action.disabled is false', () => {
expect(component[0].props.action.disabled).toEqual(false); const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).not.toHaveAttribute('aria-disabled', 'true');
expect(actionButton).not.toHaveClass('disabled');
expect(actionButton).toBeEnabled();
}); });
test('message is formatted approved message', () => { it('message is formatted approved message', () => {
expect(component[0].props.message).toEqual(formatMessage( const creditMsg = screen.getByTestId('credit-msg');
messages.approved, expect(creditMsg).toBeInTheDocument();
{ expect(creditMsg.textContent).toContain(`${credit.providerName} has approved your request for course credit`);
congratulations: (<b>{formatMessage(messages.congratulations)}</b>), });
linkToProviderSite: <ProviderLink cardId={cardId} />, });
providerName: credit.providerName, describe('when masquerading', () => {
}, beforeEach(() => {
)); reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
});
it('disables the action button', () => {
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).toHaveAttribute('aria-disabled', 'true');
expect(actionButton).toHaveClass('disabled');
});
it('still renders provider name and link correctly', () => {
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg.textContent).toContain(credit.providerName);
}); });
}); });
}); });

View File

@@ -1,8 +1,8 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils';
import track from 'tracking'; import track from 'tracking';
import messages from './messages'; import messages from './messages';
@@ -14,16 +14,13 @@ jest.mock('hooks', () => ({
useCardCourseRunData: jest.fn(), useCardCourseRunData: jest.fn(),
}, },
})); }));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('tracking', () => ({ jest.mock('tracking', () => ({
credit: { credit: {
purchase: (...args) => ({ trackCredit: args }), purchase: jest.fn(),
}, },
})); }));
let el;
let component;
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const courseId = 'test-course-id'; const courseId = 'test-course-id';
const credit = { const credit = {
@@ -32,50 +29,45 @@ const credit = {
reduxHooks.useCardCreditData.mockReturnValue(credit); reduxHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId }); reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
const render = () => { const renderEligibleContent = () => render(<IntlProvider locale="en" messages={{}}><EligibleContent cardId={cardId} /></IntlProvider>);
el = shallow(<EligibleContent cardId={cardId} />);
};
const loadComponent = () => {
component = el.instance.findByType('CreditContent');
};
describe('EligibleContent component', () => { describe('EligibleContent component', () => {
beforeEach(() => { describe('hooks', () => {
render();
});
describe('behavior', () => {
it('initializes credit data with cardId', () => { it('initializes credit data with cardId', () => {
renderEligibleContent();
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
}); });
it('initializes course run data with cardId', () => { it('initializes course run data with cardId', () => {
renderEligibleContent();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
}); });
}); });
describe('render', () => { describe('behavior', () => {
describe('rendered CreditContent component', () => { describe('rendered CreditContent component', () => {
beforeEach(() => { it('action message is formatted getCredit message', () => {
loadComponent(); renderEligibleContent();
const button = screen.getByRole('button', { name: messages.getCredit.defaultMessage });
expect(button).toBeInTheDocument();
}); });
test('action.onClick sends credit purchase track event', () => { it('onClick sends credit purchase track event', async () => {
expect(component[0].props.action.onClick).toEqual( const user = userEvent.setup();
track.credit.purchase(courseId), renderEligibleContent();
); const button = screen.getByRole('button', { name: messages.getCredit.defaultMessage });
await user.click(button);
expect(track.credit.purchase).toHaveBeenCalledWith(courseId);
}); });
test('action.message is formatted getCredit message', () => { it('message is formatted eligible message if provider', () => {
expect(component[0].props.action.message).toEqual(formatMessage(messages.getCredit)); renderEligibleContent();
const eligibleMessage = screen.getByTestId('credit-msg');
expect(eligibleMessage).toBeInTheDocument();
expect(eligibleMessage).toHaveTextContent(credit.providerName);
}); });
test('message is formatted eligible message if no provider', () => { it('message is formatted eligible message if no provider', () => {
reduxHooks.useCardCreditData.mockReturnValueOnce({}); reduxHooks.useCardCreditData.mockReturnValue({});
render(); renderEligibleContent();
loadComponent(); const eligibleMessage = screen.getByTestId('credit-msg');
expect(component[0].props.message).toEqual(formatMessage( expect(eligibleMessage).toBeInTheDocument();
messages.eligible, expect(eligibleMessage).toHaveTextContent(messages.getCredit.defaultMessage);
{ getCredit: (<b>{formatMessage(messages.getCredit)}</b>) },
));
});
test('message is formatted eligible message if provider', () => {
expect(component[0].props.message).toEqual(
formatMessage(messages.eligibleFromProvider, { providerName: credit.providerName }),
);
}); });
}); });
}); });

View File

@@ -1,73 +1,103 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import userEvent from '@testing-library/user-event';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import messages from './messages'; import messages from './messages';
import hooks from './hooks'; import hooks from './hooks';
import ProviderLink from './components/ProviderLink';
import MustRequestContent from './MustRequestContent'; import MustRequestContent from './MustRequestContent';
jest.mock('./hooks', () => ({ jest.mock('./hooks', () => ({
useCreditRequestData: jest.fn(), useCreditRequestData: jest.fn(),
})); }));
jest.mock('hooks', () => ({
reduxHooks: { useMasqueradeData: jest.fn() },
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
let el; jest.mock('hooks', () => ({
let component; reduxHooks: {
useMasqueradeData: jest.fn(),
useCardCreditData: jest.fn(),
},
}));
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const requestData = { test: 'requestData' }; const requestData = {
const createCreditRequest = jest.fn().mockName('createCreditRequest'); url: 'test-request-data-url',
hooks.useCreditRequestData.mockReturnValue({ parameters: {
requestData, key1: 'val1',
createCreditRequest, key2: 'val2',
}); key3: 'val3',
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false }); },
const render = () => {
el = shallow(<MustRequestContent cardId={cardId} />);
}; };
const providerName = 'test-credit-provider-name';
const providerStatusUrl = 'test-credit-provider-status-url';
const createCreditRequest = jest.fn().mockName('createCreditRequest');
const renderMustRequestContent = () => render(
<IntlProvider locale="en" messages={messages}>
<MustRequestContent cardId={cardId} />
</IntlProvider>,
);
describe('MustRequestContent component', () => { describe('MustRequestContent component', () => {
beforeEach(() => { beforeEach(() => {
render(); jest.clearAllMocks();
hooks.useCreditRequestData.mockReturnValue({
requestData,
createCreditRequest,
});
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
reduxHooks.useCardCreditData.mockReturnValue({
providerName,
providerStatusUrl,
});
}); });
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit request data with cardId', () => { it('initializes credit request data with cardId', () => {
renderMustRequestContent();
expect(hooks.useCreditRequestData).toHaveBeenCalledWith(cardId); expect(hooks.useCreditRequestData).toHaveBeenCalledWith(cardId);
}); });
}); });
describe('render', () => {
describe('rendered CreditContent component', () => { describe('behavior', () => {
describe('rendered content', () => {
beforeEach(() => { beforeEach(() => {
component = el.instance.findByType('CreditContent'); renderMustRequestContent();
}); });
test('action.onClick calls createCreditRequest from useCreditRequestData hook', () => {
expect(component[0].props.action.onClick).toEqual(createCreditRequest); it('calls createCreditRequest when request credit button is clicked', async () => {
const user = userEvent.setup();
const button = screen.getByRole('button', { name: /request credit/i });
await user.click(button);
expect(createCreditRequest).toHaveBeenCalled();
}); });
test('action.message is formatted requestCredit message', () => {
expect(component[0].props.action.message).toEqual( it('shows request credit button that is enabled', () => {
formatMessage(messages.requestCredit), const button = screen.getByRole('button', { name: /request credit/i });
); expect(button).toBeEnabled();
}); });
test('action.disabled is false', () => {
expect(component[0].props.action.disabled).toEqual(false); it('displays must request message with provider link', () => {
expect(screen.getByTestId('credit-msg')).toHaveTextContent(/request credit/i);
}); });
test('message is formatted mustRequest message', () => {
expect(component[0].props.message).toEqual( it('renders credit request form with correct data', () => {
formatMessage(messages.mustRequest, { const { container } = renderMustRequestContent();
linkToProviderSite: <ProviderLink cardId={cardId} />, const form = container.querySelector('form');
requestCredit: <b>{formatMessage(messages.requestCredit)}</b>, expect(form).toBeInTheDocument();
}), expect(form).toHaveAttribute('action', requestData.url);
);
}); });
test('requestData drawn from useCreditRequestData hook', () => { });
expect(component[0].props.requestData).toEqual(requestData);
describe('when masquerading', () => {
beforeEach(() => {
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
renderMustRequestContent();
});
it('disables the request credit button', () => {
const button = screen.getByRole('button', { name: /request credit/i });
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });
}); });

View File

@@ -1,7 +1,6 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import messages from './messages'; import messages from './messages';
@@ -10,11 +9,6 @@ import PendingContent from './PendingContent';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() }, 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 cardId = 'test-card-id';
const providerName = 'test-credit-provider-name'; const providerName = 'test-credit-provider-name';
@@ -25,38 +19,48 @@ reduxHooks.useCardCreditData.mockReturnValue({
}); });
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false }); reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
const render = () => { const renderPendingContent = () => render(
el = shallow(<PendingContent cardId={cardId} />); <IntlProvider messages={{}} locale="en">
}; <PendingContent cardId={cardId} />
</IntlProvider>,
);
describe('PendingContent component', () => { describe('PendingContent component', () => {
beforeEach(() => { describe('hooks', () => {
render();
});
describe('behavior', () => {
it('initializes card credit data with cardId', () => { it('initializes card credit data with cardId', () => {
renderPendingContent();
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
}); });
}); });
describe('render', () => { describe('behavior', () => {
describe('rendered CreditContent component', () => { describe('rendered CreditContent component', () => {
beforeEach(() => { it('action message is formatted requestCredit message', () => {
component = el.instance.findByType('CreditContent'); renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toBeInTheDocument();
}); });
test('action.href will go to provider status site', () => { it('action href will go to provider status site', () => {
expect(component[0].props.action.href).toEqual(providerStatusUrl); renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toHaveAttribute('href', providerStatusUrl);
}); });
test('action.message is formatted requestCredit message', () => { it('action.disabled is false', () => {
expect(component[0].props.action.message).toEqual( renderPendingContent();
formatMessage(messages.viewDetails), const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
); expect(button).not.toHaveClass('disabled');
}); });
test('action.disabled is false', () => { it('message is formatted pending message with provider name', () => {
expect(component[0].props.action.disabled).toEqual(false); renderPendingContent();
const component = screen.getByTestId('credit-msg');
expect(component).toBeInTheDocument();
expect(component).toHaveTextContent(`${providerName} has received`);
}); });
test('message is formatted pending message', () => { describe('when masqueradeData is true', () => {
expect(component[0].props.message).toEqual( it('disables the view details button', () => {
formatMessage(messages.received, { providerName }), reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
); renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toHaveClass('disabled');
});
}); });
}); });
}); });

View File

@@ -1,10 +1,7 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import messages from './messages';
import ProviderLink from './components/ProviderLink';
import RejectedContent from './RejectedContent'; import RejectedContent from './RejectedContent';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
@@ -12,8 +9,6 @@ jest.mock('hooks', () => ({
useCardCreditData: jest.fn(), useCardCreditData: jest.fn(),
}, },
})); }));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const credit = { const credit = {
@@ -22,32 +17,27 @@ const credit = {
}; };
reduxHooks.useCardCreditData.mockReturnValue(credit); reduxHooks.useCardCreditData.mockReturnValue(credit);
let el; const renderRejectedContent = () => render(<IntlProvider><RejectedContent cardId={cardId} /></IntlProvider>);
let component;
const render = () => { el = shallow(<RejectedContent cardId={cardId} />); };
const loadComponent = () => { component = el.instance.findByType('CreditContent'); };
describe('RejectedContent component', () => { describe('RejectedContent component', () => {
beforeEach(render); describe('hooks', () => {
describe('behavior', () => {
it('initializes credit data with cardId', () => { it('initializes credit data with cardId', () => {
renderRejectedContent();
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
}); });
}); });
describe('render', () => { describe('render', () => {
describe('rendered CreditContent component', () => { describe('rendered CreditContent component', () => {
beforeAll(loadComponent); it('no action is passed', () => {
test('no action is passed', () => { renderRejectedContent();
expect(component[0].props.action).toEqual(undefined); const action = screen.queryByTestId('action-row-btn');
expect(action).not.toBeInTheDocument();
}); });
test('message is formatted rejected message', () => { it('message is formatted rejected message', () => {
expect(component[0].props.message).toEqual(formatMessage( renderRejectedContent();
messages.rejected, const message = screen.getByTestId('credit-msg');
{ expect(message).toBeInTheDocument();
linkToProviderSite: <ProviderLink cardId={cardId} />, expect(message).toHaveTextContent(`${credit.providerName} did not approve your request for course credit.`);
providerName: credit.providerName,
},
));
}); });
}); });
}); });

View File

@@ -1,9 +1,7 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import CreditContent from './CreditContent'; import CreditContent from './CreditContent';
let el;
const action = { const action = {
href: 'test-action-href', href: 'test-action-href',
onClick: jest.fn().mockName('test-action-onClick'), onClick: jest.fn().mockName('test-action-onClick'),
@@ -15,45 +13,57 @@ const message = 'test-message';
const requestData = { url: 'test-request-data-url', parameters: { key1: 'val1' } }; const requestData = { url: 'test-request-data-url', parameters: { key1: 'val1' } };
const props = { action, message, requestData }; const props = { action, message, requestData };
const renderCreditContent = (data) => render(
<CreditContent {...data} />,
);
describe('CreditContent component', () => { describe('CreditContent component', () => {
describe('render', () => { describe('render', () => {
describe('with action', () => { describe('with action', () => {
beforeEach(() => { it('loads href and message into action row button', () => {
el = shallow(<CreditContent {...props} />); renderCreditContent(props);
}); const button = screen.getByRole('link', { name: action.message });
test('snapshot', () => { expect(button).toBeInTheDocument();
expect(el.snapshot).toMatchSnapshot(); expect(button).toHaveAttribute('href', action.href);
}); expect(button).not.toHaveAttribute('disabled');
it('loads href, onClick, and message into action row button', () => {
const buttonEl = el.instance.findByTestId('action-row-btn')[0];
expect(buttonEl.props.href).toEqual(action.href);
expect(buttonEl.props.onClick).toEqual(action.onClick);
expect(buttonEl.props.disabled).toEqual(action.disabled);
expect(buttonEl.children[0].el).toEqual(action.message);
}); });
it('loads message into credit-msg div', () => { it('loads message into credit-msg div', () => {
expect(el.instance.findByTestId('credit-msg')[0].children[0].el).toEqual(message); renderCreditContent(props);
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg).toBeInTheDocument();
expect(creditMsg.innerHTML).toEqual(message);
}); });
it('loads CreditRequestForm with passed requestData', () => { it('loads CreditRequestForm with passed requestData', () => {
expect(el.instance.findByType('CreditRequestForm')[0].props.requestData).toEqual(requestData); const { container } = renderCreditContent(props);
const creditForm = container.querySelector('form');
expect(creditForm).toBeInTheDocument();
expect(creditForm).toHaveAttribute('action', requestData.url);
}); });
test('disables action button when action.disabled is true', () => { it('disables action button when action.disabled is true', () => {
el = shallow(<CreditContent {...props} action={{ ...action, disabled: true }} />); renderCreditContent({ ...props, action: { ...action, disabled: true } });
expect(el.instance.findByTestId('action-row-btn')[0].props.disabled).toEqual(true); const button = screen.getByRole('link', { name: action.message });
expect(button).toBeInTheDocument();
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });
describe('without action', () => { describe('without action', () => {
test('snapshot', () => {
el = shallow(<CreditContent {...{ message, requestData }} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads message into credit-msg div', () => { it('loads message into credit-msg div', () => {
expect(el.instance.findByTestId('credit-msg')[0].children[0].el).toEqual(message); renderCreditContent({ message, requestData });
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg).toBeInTheDocument();
expect(creditMsg.innerHTML).toEqual(message);
}); });
it('loads CreditRequestForm with passed requestData', () => { it('loads CreditRequestForm with passed requestData', () => {
expect(el.instance.findByType('CreditRequestForm')[0].props.requestData).toEqual(requestData); const { container } = renderCreditContent({ message, requestData });
const creditForm = container.querySelector('form');
expect(creditForm).toBeInTheDocument();
expect(creditForm).toHaveAttribute('action', requestData.url);
});
it('does not render action row button', () => {
renderCreditContent({ message, requestData });
const button = screen.queryByRole('link', { name: action.message });
expect(button).not.toBeInTheDocument();
}); });
}); });
}); });

View File

@@ -1,32 +0,0 @@
// 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>
`;

View File

@@ -4,6 +4,12 @@ import useCreditRequestFormData from './hooks';
const requestData = 'test-request-data'; const requestData = 'test-request-data';
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn((val) => ({ current: val, useRef: true })),
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
}));
let out; let out;
const ref = { const ref = {
current: { click: jest.fn() }, current: { click: jest.fn() },

View File

@@ -1,5 +1,4 @@
import React from 'react'; import { render } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { keyStore } from 'utils'; import { keyStore } from 'utils';
@@ -11,7 +10,8 @@ jest.mock('./hooks', () => ({
default: jest.fn(), default: jest.fn(),
})); }));
const ref = 'test-ref'; const ref = { current: { click: jest.fn() }, useRef: jest.fn() };
const requestData = { const requestData = {
url: 'test-request-data-url', url: 'test-request-data-url',
parameters: { parameters: {
@@ -25,40 +25,41 @@ const paramKeys = keyStore(requestData.parameters);
useCreditRequestFormData.mockReturnValue({ ref }); useCreditRequestFormData.mockReturnValue({ ref });
let el; const renderForm = (data) => render(<CreditRequestForm requestData={data} />);
const shallowRender = (data) => { el = shallow(<CreditRequestForm requestData={data} />); };
describe('CreditRequestForm component', () => { describe('CreditRequestForm component', () => {
describe('behavior', () => { beforeEach(() => {
jest.clearAllMocks();
});
describe('hooks', () => {
it('initializes ref from hook with requestData', () => { it('initializes ref from hook with requestData', () => {
shallowRender(requestData); renderForm(requestData);
expect(useCreditRequestFormData).toHaveBeenCalledWith(requestData); expect(useCreditRequestFormData).toHaveBeenCalledWith(requestData);
}); });
}); });
describe('render output', () => { describe('render output', () => {
describe('null requestData', () => { describe('null requestData', () => {
it('returns null', () => { it('returns null', () => {
shallowRender(null); const { container } = renderForm(null);
expect(el.isEmptyRender()).toEqual(true); expect(container.firstChild).toBeNull();
}); });
}); });
describe('valid requestData', () => { describe('valid requestData', () => {
beforeEach(() => {
shallowRender(requestData);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads Form with requestData url', () => { it('loads Form with requestData url', () => {
expect(el.instance.findByType('Form')[0].props.action).toEqual(requestData.url); const { container } = renderForm(requestData);
const creditForm = container.querySelector('form');
expect(creditForm).toBeInTheDocument();
expect(creditForm).toHaveAttribute('action', requestData.url);
}); });
it('loads a textarea form control for each requestData parameter', () => { it('loads a textarea form control for each requestData parameter', () => {
const controls = el.instance.findByType('FormControl'); const { container } = renderForm(requestData);
expect(controls[0].props.name).toEqual(paramKeys.key1); const controls = container.querySelectorAll('textarea');
expect(controls[0].props.value).toEqual(requestData.parameters.key1); expect(controls.length).toEqual(Object.keys(requestData.parameters).length);
expect(controls[1].props.name).toEqual(paramKeys.key2); expect(controls[0]).toHaveAttribute('name', paramKeys.key1);
expect(controls[1].props.value).toEqual(requestData.parameters.key2); expect(controls[0]).toHaveValue(requestData.parameters.key1);
expect(controls[2].props.name).toEqual(paramKeys.key3); expect(controls[1]).toHaveAttribute('name', paramKeys.key2);
expect(controls[2].props.value).toEqual(requestData.parameters.key3); expect(controls[1]).toHaveValue(requestData.parameters.key2);
expect(controls[2]).toHaveAttribute('name', paramKeys.key3);
expect(controls[2]).toHaveValue(requestData.parameters.key3);
}); });
}); });
}); });

View File

@@ -4,9 +4,6 @@ import { render } from '@testing-library/react';
import useCreditRequestFormData from './hooks'; import useCreditRequestFormData from './hooks';
import CreditRequestForm from '.'; import CreditRequestForm from '.';
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.mock('./hooks', () => ({ jest.mock('./hooks', () => ({
__esModule: true, __esModule: true,
default: jest.fn(), default: jest.fn(),

View File

@@ -1,7 +1,6 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import ProviderLink from './ProviderLink'; import ProviderLink from './ProviderLink';
@@ -16,27 +15,30 @@ const credit = {
providerStatusUrl: 'test-credit-provider-status-url', providerStatusUrl: 'test-credit-provider-status-url',
providerName: 'test-credit-provider-name', providerName: 'test-credit-provider-name',
}; };
let el;
const renderProviderLink = () => render(
<IntlProvider locale="en"><ProviderLink cardId={cardId} /></IntlProvider>,
);
describe('ProviderLink component', () => { describe('ProviderLink component', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useCardCreditData.mockReturnValue(credit); reduxHooks.useCardCreditData.mockReturnValue(credit);
el = shallow(<ProviderLink cardId={cardId} />); renderProviderLink();
}); });
describe('behavior', () => { describe('hooks', () => {
it('initializes credit hook with cardId', () => { it('initializes credit hook with cardId', () => {
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
}); });
}); });
describe('render', () => { describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('passes credit.providerStatusUrl to the hyperlink href', () => { it('passes credit.providerStatusUrl to the hyperlink href', () => {
expect(el.instance.findByType('Hyperlink')[0].props.href).toEqual(credit.providerStatusUrl); const providerLink = screen.getByRole('link', { href: credit.providerStatusUrl });
expect(providerLink).toBeInTheDocument();
}); });
it('passes providerName for the link message', () => { it('passes providerName for the link message', () => {
expect(el.instance.findByType('Hyperlink')[0].children[0].el).toEqual(credit.providerName); const providerLink = screen.getByRole('link', { href: credit.providerStatusUrl });
expect(providerLink).toHaveTextContent(credit.providerName);
}); });
}); });
}); });

View File

@@ -1,60 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditContent component render with action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>
<ActionRow
className="mt-4"
>
<Button
as="a"
className="border-gray-400"
data-testid="action-row-btn"
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={
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",
}
}
/>
</Fragment>
`;
exports[`CreditContent component render without action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>
<CreditRequestForm
requestData={
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",
}
}
/>
</Fragment>
`;

View File

@@ -1,11 +0,0 @@
// 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>
`;

View File

@@ -1,10 +1,11 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import EntitlementBanner from './EntitlementBanner'; import EntitlementBanner from './EntitlementBanner';
import messages from './messages';
jest.mock('components/Banner', () => 'Banner');
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
utilHooks: { utilHooks: {
useFormatDate: () => date => date, useFormatDate: () => date => date,
@@ -18,9 +19,7 @@ jest.mock('hooks', () => ({
}, },
})); }));
const cardId = 'my-test-course-number'; const cardId = 'test-card-id';
let el;
const entitlementData = { const entitlementData = {
isEntitlement: true, isEntitlement: true,
@@ -31,33 +30,43 @@ const entitlementData = {
}; };
const platformData = { supportEmail: 'test-support-email' }; const platformData = { supportEmail: 'test-support-email' };
const render = (overrides = {}) => { const renderComponent = (overrides = {}) => {
const { entitlement = {} } = overrides; const { entitlement = {} } = overrides;
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement }); reduxHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
reduxHooks.usePlatformSettingsData.mockReturnValueOnce(platformData); reduxHooks.usePlatformSettingsData.mockReturnValueOnce(platformData);
el = shallow(<EntitlementBanner cardId={cardId} />); return render(<IntlProvider locale="en"><EntitlementBanner cardId={cardId} /></IntlProvider>);
}; };
describe('EntitlementBanner', () => { describe('EntitlementBanner', () => {
test('initializes data with course number from entitlement', () => { it('initializes data with course number from entitlement', () => {
render(); renderComponent();
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(cardId); expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(cardId);
}); });
test('no display if not an entitlement', () => { it('no display if not an entitlement', () => {
render({ entitlement: { isEntitlement: false } }); renderComponent({ entitlement: { isEntitlement: false } });
expect(el.isEmptyRender()).toEqual(true); const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
}); });
test('snapshot: no sessions available', () => { it('renders when no sessions available', () => {
render({ entitlement: { isFulfilled: false, hasSessions: false } }); renderComponent({ entitlement: { isFulfilled: false, hasSessions: false } });
expect(el.snapshot).toMatchSnapshot(); const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-warning');
expect(banner.innerHTML).toContain(platformData.supportEmail);
}); });
test('snapshot: expiration warning', () => { it('renders when expiration warning', () => {
render({ entitlement: { showExpirationWarning: true } }); renderComponent({ entitlement: { showExpirationWarning: true } });
expect(el.snapshot).toMatchSnapshot(); const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-info');
const button = screen.getByRole('button', { name: formatMessage(messages.selectSession) });
expect(button).toBeInTheDocument();
}); });
test('no display if sessions available and not displaying warning', () => { it('renders expired banner', () => {
render(); renderComponent({ entitlement: { isExpired: true } });
expect(el.isEmptyRender()).toEqual(true); const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner.innerHTML).toContain(formatMessage(messages.entitlementExpired));
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { ProgramsList } from './ProgramsList'; import { ProgramsList } from './ProgramsList';
@@ -9,15 +9,23 @@ describe('ProgramsList', () => {
title: 'Example Program 1', title: 'Example Program 1',
}, },
{ {
programUrl: 'http://example.com', programUrl: 'http://example2.com',
title: 'Example Program 2', title: 'Example Program 2',
}, },
]; ];
it('renders correctly', () => { it('renders correctly', () => {
const wrapper = shallow(<ProgramsList programs={programs} />); render(<ProgramsList programs={programs} />);
expect(wrapper.snapshot).toMatchSnapshot(); const list = screen.getByRole('list');
expect(list).toBeInTheDocument();
expect(list.children.length).toEqual(programs.length);
});
expect(wrapper.instance.findByType('li').length).toEqual(programs.length); it('add the links correctly', () => {
render(<ProgramsList programs={programs} />);
programs.forEach(program => {
const link = screen.getByRole('link', { name: program.title });
expect(link).toHaveAttribute('href', program.url);
});
}); });
}); });

View File

@@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProgramsList renders correctly 1`] = `
<ul
className="related-programs-list-container"
>
<li
className="my-2"
key="http://example.com"
>
<a
href="http://example.com"
>
Example Program 1
</a>
</li>
<li
className="my-2"
key="http://example.com"
>
<a
href="http://example.com"
>
Example Program 2
</a>
</li>
</ul>
`;

View File

@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RelatedProgramsBanner render with programs 1`] = `
<Banner
className="bg-white border-top border-bottom mb-0 related-programs-banner"
icon={[MockFunction icons.Program]}
variant="info"
>
<span
className="font-weight-bolder"
>
Related Programs:
</span>
<ProgramsList
programs={
[
{
"title": "Program 1",
"url": "http://example.com/program1",
},
{
"title": "Program 2",
"url": "http://example.com/program2",
},
]
}
/>
</Banner>
`;

View File

@@ -1,10 +1,9 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import RelatedProgramsBanner from '.'; import RelatedProgramsBanner from '.';
jest.mock('./ProgramsList', () => 'ProgramsList');
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardRelatedProgramsData: jest.fn(), useCardRelatedProgramsData: jest.fn(),
@@ -12,31 +11,39 @@ jest.mock('hooks', () => ({
})); }));
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const programData = {
list: [
{
title: 'Program 1',
url: 'http://example.com/program1',
},
{
title: 'Program 2',
url: 'http://example.com/program2',
},
],
length: 2,
};
describe('RelatedProgramsBanner', () => { describe('RelatedProgramsBanner', () => {
test('render empty', () => { it('render empty', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue({ reduxHooks.useCardRelatedProgramsData.mockReturnValue({});
length: 0, render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
}); const banner = screen.queryByRole('alert');
const el = shallow(<RelatedProgramsBanner cardId={cardId} />); expect(banner).toBeNull();
expect(el.isEmptyRender()).toEqual(true);
}); });
test('render with programs', () => { it('render with programs', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue({ reduxHooks.useCardRelatedProgramsData.mockReturnValue(programData);
list: [ render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
{ const list = screen.getByRole('list');
title: 'Program 1', expect(list.childElementCount).toBe(programData.list.length);
url: 'http://example.com/program1', });
},
{ it('render related programs title', () => {
title: 'Program 2', reduxHooks.useCardRelatedProgramsData.mockReturnValue(programData);
url: 'http://example.com/program2', render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
}, const title = screen.getByText('Related Programs:');
], expect(title).toBeInTheDocument();
length: 2,
});
const el = shallow(<RelatedProgramsBanner cardId={cardId} />);
expect(el.snapshot).toMatchSnapshot();
}); });
}); });

View File

@@ -1,205 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CertificateBanner snapshot is passing and is downloadable 1`] = `
<Banner
icon={[MockFunction icons.CheckCircle]}
variant="success"
>
Congratulations. Your certificate is ready.
</Banner>
`;
exports[`CertificateBanner snapshot is passing and is earned but unavailable 1`] = `
<Banner>
Your grade and certificate will be ready after 10/20/3030.
</Banner>
`;
exports[`CertificateBanner snapshot is passing and not downloadable render empty 1`] = `null`;
exports[`CertificateBanner snapshot is restricted 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
If you would like a refund on your Certificate of Achievement, please contact us.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with billing email 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
<format-message-function
message={
{
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
{
"billingEmail": <MailtoLink
to="billing@email"
>
billing@email
</MailtoLink>,
}
}
/>
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with support and billing email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
<format-message-function
message={
{
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
{
"billingEmail": <MailtoLink
to="billing@email"
>
billing@email
</MailtoLink>,
}
}
/>
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with support email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
If you would like a refund on your Certificate of Achievement, please contact us.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted with billing email 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted with support email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
</Banner>
`;
exports[`CertificateBanner snapshot not passing and audit 1`] = `
<Banner>
Grade required to pass the course: 0.8%
</Banner>
`;
exports[`CertificateBanner snapshot not passing and has finished 1`] = `
<Banner
variant="warning"
>
You are not eligible for a certificate.
<Hyperlink
destination="progressUrl"
isInline={true}
>
View grades.
</Hyperlink>
</Banner>
`;
exports[`CertificateBanner snapshot not passing and is downloadable 1`] = `
<Banner
icon={[MockFunction icons.CheckCircle]}
variant="success"
>
Congratulations. Your certificate is ready.
</Banner>
`;
exports[`CertificateBanner snapshot not passing and not audit and not finished 1`] = `
<Banner
variant="warning"
>
Grade required for a certificate: 0.8%
</Banner>
`;

View File

@@ -1,38 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseBanner audit access expired snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
<Fragment>
<Banner>
Your audit access to this course has expired.
<Hyperlink
destination=""
isInline={true}
>
Find another course
</Hyperlink>
</Banner>
</Fragment>
`;
exports[`CourseBanner snapshot: stacking banners 1`] = `<Fragment />`;
exports[`CourseBanner staff snapshot: isStaff 1`] = `<Fragment />`;
exports[`CourseBanner too early has start date snapshot 1`] = `
<Fragment>
<Banner>
You can't access this course just yet because the course hasn't started yet. The course will start on 11/11/3030.
</Banner>
</Fragment>
`;
exports[`CourseBanner too early no start date snapshot 1`] = `<Fragment />`;
exports[`CourseBanner unmet prerequisites snapshot: unmetPrerequisites 1`] = `
<Fragment>
<Banner>
You can't access this course just yet because you have not met the pre-requisites.
</Banner>
</Fragment>
`;

View File

@@ -1,53 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EntitlementBanner snapshot: expiration warning 1`] = `
<Banner>
<format-message-function
message={
{
"defaultMessage": "You must {selectSessionButton} by {changeDeadline} to access the course.",
"description": "Entitlement course message when the entitlement is expiring soon.",
"id": "learner-dash.courseCard.banners.entitlementExpiringSoon",
}
}
values={
{
"changeDeadline": "11/11/2022",
"selectSessionButton": <Button
className="m-0 p-0"
onClick={[MockFunction updateSelectSessionModalCallback(my-test-course-number)]}
size="inline"
variant="link"
>
select a session
</Button>,
}
}
/>
</Banner>
`;
exports[`EntitlementBanner snapshot: no sessions available 1`] = `
<Banner
variant="warning"
>
<format-message-function
message={
{
"defaultMessage": "There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.",
"description": "Entitlement course message when no sessions are available",
"id": "learner-dash.courseCard.banners.entitlementUnavailable",
}
}
values={
{
"emailLink": <MailtoLink
to="test-support-email"
>
test-support-email
</MailtoLink>,
}
}
/>
</Banner>
`;

View File

@@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardBanners render with isEnrolled false 1`] = `
<div
className="course-card-banners"
data-testid="CourseCardBanners"
>
<RelatedProgramsBanner
cardId="test-card-id"
/>
<CourseBannerSlot
cardId="test-card-id"
/>
<EntitlementBanner
cardId="test-card-id"
/>
</div>
`;
exports[`CourseCardBanners renders default CourseCardBanners 1`] = `
<div
className="course-card-banners"
data-testid="CourseCardBanners"
>
<RelatedProgramsBanner
cardId="test-card-id"
/>
<CourseBannerSlot
cardId="test-card-id"
/>
<EntitlementBanner
cardId="test-card-id"
/>
<CertificateBanner
cardId="test-card-id"
/>
<CreditBanner
cardId="test-card-id"
/>
</div>
`;

View File

@@ -1,14 +1,23 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import CourseCardBanners from '.'; import CourseCardBanners from '.';
jest.mock('./CourseBanner', () => 'CourseBanner'); jest.mock('./CourseBanner', () => jest.fn(() => <div>CourseBanner</div>));
jest.mock('./CertificateBanner', () => 'CertificateBanner'); jest.mock('./CertificateBanner', () => jest.fn(() => <div>CertificateBanner</div>));
jest.mock('./CreditBanner', () => 'CreditBanner'); jest.mock('./CreditBanner', () => jest.fn(() => <div>CreditBanner</div>));
jest.mock('./EntitlementBanner', () => 'EntitlementBanner'); jest.mock('./EntitlementBanner', () => jest.fn(() => <div>EntitlementBanner</div>));
jest.mock('./RelatedProgramsBanner', () => 'RelatedProgramsBanner'); jest.mock('./RelatedProgramsBanner', () => jest.fn(() => <div>RelatedProgramsBanner</div>));
const mockedComponents = [
'CourseBanner',
'CertificateBanner',
'CreditBanner',
'EntitlementBanner',
'RelatedProgramsBanner',
];
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
@@ -20,13 +29,20 @@ describe('CourseCardBanners', () => {
const props = { const props = {
cardId: 'test-card-id', cardId: 'test-card-id',
}; };
test('renders default CourseCardBanners', () => { it('renders default CourseCardBanners', () => {
const wrapper = shallow(<CourseCardBanners {...props} />); render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
expect(wrapper.snapshot).toMatchSnapshot(); mockedComponents.map((componentName) => {
const mockedComponent = screen.getByText(componentName);
return expect(mockedComponent).toBeInTheDocument();
});
}); });
test('render with isEnrolled false', () => { it('render with isEnrolled false', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false }); reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
const wrapper = shallow(<CourseCardBanners {...props} />); render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
expect(wrapper.snapshot).toMatchSnapshot(); const mockedComponentsIfNotEnrolled = mockedComponents.slice(-2);
mockedComponentsIfNotEnrolled.map((componentName) => {
const mockedComponent = screen.getByText(componentName);
return expect(mockedComponent).toBeInTheDocument();
});
}); });
}); });

View File

@@ -1,56 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCard Details component does not have change session button on regular course 1`] = `
<div>
<span
class="small"
data-testid="CourseCardDetails"
>
provider-name
test-course-number
• access-message
</span>
</div>
`;
exports[`CourseCard Details component has change session button on entitlement course 1`] = `
<div>
<span
class="small"
data-testid="CourseCardDetails"
>
provider-name
test-course-number
• access-message
<button
class="m-0 p-0"
variant="link"
>
change-or-leave-session-message
</button>
</span>
</div>
`;
exports[`CourseCard Details component has change session button on entitlement course but no access message 1`] = `
<div>
<span
class="small"
data-testid="CourseCardDetails"
>
provider-name
test-course-number
<button
class="m-0 p-0"
variant="link"
>
change-or-leave-session-message
</button>
</span>
</div>
`;

View File

@@ -20,6 +20,16 @@ jest.mock('hooks', () => ({
}, },
})); }));
jest.mock('@edx/frontend-platform/i18n', () => {
const { formatMessage } = jest.requireActual('testUtils');
return {
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({
formatMessage,
}),
};
});
const cardId = 'my-test-card-id'; const cardId = 'my-test-card-id';
const courseNumber = 'test-course-number'; const courseNumber = 'test-course-number';
const useAccessMessage = 'test-access-message'; const useAccessMessage = 'test-access-message';

View File

@@ -1,7 +1,3 @@
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
a.course-card-title { a.course-card-title {
color: $black; color: var(--pgn-color-black);
} }

View File

@@ -1,5 +1,4 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { render } from '@testing-library/react';
import CourseCardDetails from '.'; import CourseCardDetails from '.';
@@ -48,23 +47,36 @@ describe('CourseCard Details component', () => {
return separatorsCount; return separatorsCount;
}; };
test('has change session button on entitlement course', () => { it('has change session button on entitlement course', () => {
const wrapper = createWrapper(); const wrapper = createWrapper();
expect(wrapper.container).toMatchSnapshot(); const sessionButton = screen.getByRole('button', { name: defaultHooks.changeOrLeaveSessionMessage });
expect(sessionButton).toBeInTheDocument();
const accessMessage = screen.getByText((text) => text.includes(defaultHooks.accessMessage));
expect(accessMessage).toBeInTheDocument();
// it has 3 separator, 4 column // it has 3 separator, 4 column
expect(fetchSeparators(wrapper)).toBe(3); expect(fetchSeparators(wrapper)).toBe(3);
}); });
test('has change session button on entitlement course but no access message', () => { it('has change session button on entitlement course but no access message', () => {
const wrapper = createWrapper({ accessMessage: null }); const wrapper = createWrapper({ accessMessage: null });
expect(wrapper.container).toMatchSnapshot(); const sessionButton = screen.getByRole('button', { name: defaultHooks.changeOrLeaveSessionMessage });
expect(sessionButton).toBeInTheDocument();
const accessMessage = screen.queryByText((text) => text.includes(defaultHooks.accessMessage));
expect(accessMessage).toBeNull();
// it has 2 separator, 3 column // it has 2 separator, 3 column
expect(fetchSeparators(wrapper)).toBe(2); expect(fetchSeparators(wrapper)).toBe(2);
}); });
test('does not have change session button on regular course', () => { it('does not have change session button on regular course', () => {
const wrapper = createWrapper({ isEntitlement: false }); const wrapper = createWrapper({ isEntitlement: false });
expect(wrapper.container).toMatchSnapshot(); const sessionButton = screen.queryByRole('button', { name: defaultHooks.changeOrLeaveSessionMessage });
expect(sessionButton).toBeNull();
const accessMessage = screen.getByText((text) => text.includes(defaultHooks.accessMessage));
expect(accessMessage).toBeInTheDocument();
// it has 2 separator, 3 column // it has 2 separator, 3 column
expect(fetchSeparators(wrapper)).toBe(2); expect(fetchSeparators(wrapper)).toBe(2);
}); });

View File

@@ -1,61 +1,68 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import track from 'tracking';
import useActionDisabledState from './hooks'; import useActionDisabledState from './hooks';
import CourseCardImage from './CourseCardImage'; import { CourseCardImage } from './CourseCardImage';
import messages from '../messages';
const homeUrl = 'home-url'; const homeUrl = 'https://example.com';
const bannerImgSrc = 'banner-img-src.jpg';
jest.mock('tracking', () => ({
course: {
courseImageClicked: jest.fn().mockName('segment.courseImageClicked'),
},
}));
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCourseData: jest.fn(() => ({ bannerImgSrc: 'banner-img-src' })), useCardCourseData: jest.fn(() => ({ bannerImgSrc })),
useCardCourseRunData: jest.fn(() => ({ homeUrl })), useCardCourseRunData: jest.fn(() => ({ homeUrl })),
useCardEnrollmentData: jest.fn(() => ({ isVerified: true })), useCardEnrollmentData: jest.fn(),
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({ useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
trackCourseEvent: { eventName, cardId, url }, trackCourseEvent: { eventName, cardId, url },
})), })),
}, },
})); }));
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
jest.mock('./hooks', () => jest.fn());
describe('CourseCardImage', () => { describe('CourseCardImage', () => {
const props = { const props = {
cardId: 'cardId', cardId: 'test-card-id',
orientation: 'orientation', orientation: 'horizontal',
}; };
beforeEach(() => {
jest.clearAllMocks(); it('renders course image with correct attributes', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: true });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
const image = screen.getByRole('img', { name: formatMessage(messages.bannerAlt) });
expect(image).toBeInTheDocument();
expect(image.src).toContain(bannerImgSrc);
expect(image.parentElement).toHaveClass('horizontal');
}); });
describe('snapshot', () => {
test('renders clickable link course Image', () => { it('isVerified, should render badge', () => {
const wrapper = shallow(<CourseCardImage {...props} />); useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
expect(wrapper.snapshot).toMatchSnapshot(); reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
expect(wrapper.instance.type).toBe('a'); render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
expect(wrapper.instance.props.onClick).toEqual(
reduxHooks.useTrackCourseEvent( const badge = screen.getByText(formatMessage(messages.verifiedBanner));
track.course.courseImageClicked, expect(badge).toBeInTheDocument();
props.cardId, const badgeImg = screen.getByRole('img', { name: formatMessage(messages.verifiedBannerRibbonAlt) });
homeUrl, expect(badgeImg).toBeInTheDocument();
),
);
});
test('renders disabled link', () => {
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
const wrapper = shallow(<CourseCardImage {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.type).toBe('div');
});
}); });
describe('behavior', () => {
it('renders link with correct href if disableCourseTitle is false', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: false });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', homeUrl);
});
describe('hooks', () => {
it('initializes', () => { it('initializes', () => {
shallow(<CourseCardImage {...props} />); useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId); expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith( expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
props.cardId, props.cardId,

View File

@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as ReactShare from 'react-share'; import * as ReactShare from 'react-share';
import { StrictDict } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@openedx/paragon'; import { Dropdown } from '@openedx/paragon';
@@ -11,9 +10,9 @@ import { reduxHooks } from 'hooks';
import messages from './messages'; import messages from './messages';
export const testIds = StrictDict({ export const testIds = {
emailSettingsModalToggle: 'emailSettingsModalToggle', emailSettingsModalToggle: 'emailSettingsModalToggle',
}); };
export const SocialShareMenu = ({ cardId, emailSettings }) => { export const SocialShareMenu = ({ cardId, emailSettings }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();

View File

@@ -1,32 +1,19 @@
import { when } from 'jest-when'; import { when } from 'jest-when';
import * as ReactShare from 'react-share';
import { useIntl } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage, shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import track from 'tracking'; import track from 'tracking';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import { useEmailSettings } from './hooks'; import { useEmailSettings } from './hooks';
import SocialShareMenu, { testIds } from './SocialShareMenu'; import SocialShareMenu from './SocialShareMenu';
import messages from './messages'; import messages from './messages';
jest.mock('react-share', () => ({
FacebookShareButton: () => 'FacebookShareButton',
TwitterShareButton: () => 'TwitterShareButton',
}));
jest.mock('tracking', () => ({ jest.mock('tracking', () => ({
socialShare: 'test-social-share-key', socialShare: 'test-social-share-key',
})); }));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn().mockReturnValue({
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
}),
}));
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useMasqueradeData: jest.fn(), useMasqueradeData: jest.fn(),
@@ -47,9 +34,9 @@ const props = {
const mockHook = (fn, returnValue, options = {}) => { const mockHook = (fn, returnValue, options = {}) => {
if (options.isCardHook) { if (options.isCardHook) {
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue); when(fn).calledWith(props.cardId).mockReturnValue(returnValue);
} else { } else {
when(fn).calledWith().mockReturnValueOnce(returnValue); when(fn).calledWith().mockReturnValue(returnValue);
} }
}; };
@@ -89,19 +76,13 @@ const mockHooks = (returnVals = {}) => {
); );
}; };
let el; const renderComponent = () => render(<IntlProvider locale="en"><SocialShareMenu {...props} /></IntlProvider>);
const render = () => {
el = shallow(<SocialShareMenu {...props} />);
};
describe('SocialShareMenu', () => { describe('SocialShareMenu', () => {
describe('behavior', () => { describe('behavior', () => {
beforeEach(() => { beforeEach(() => {
mockHooks(); mockHooks();
render(); renderComponent();
});
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalledWith();
}); });
it('initializes local hooks', () => { it('initializes local hooks', () => {
when(useEmailSettings).expectCalledWith(); when(useEmailSettings).expectCalledWith();
@@ -118,53 +99,43 @@ describe('SocialShareMenu', () => {
describe('render', () => { describe('render', () => {
it('renders null if exec ed course', () => { it('renders null if exec ed course', () => {
mockHooks({ isExecEd2UCourse: true }); mockHooks({ isExecEd2UCourse: true });
render(); renderComponent();
expect(el.isEmptyRender()).toEqual(true); const emailSettingsButton = screen.queryByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeNull();
const facebookShareButton = screen.queryByRole('button', { name: 'facebook' });
expect(facebookShareButton).toBeNull();
const twitterShareButton = screen.queryByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeNull();
}); });
const testEmailSettingsDropdown = (isMasquerading = false) => { const testEmailSettingsDropdown = (isMasquerading = false) => {
describe('email settings dropdown', () => { describe('email settings dropdown', () => {
const loadToggle = () => el.instance.findByTestId(testIds.emailSettingsModalToggle)[0];
it('renders', () => { it('renders', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(1); const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeInTheDocument();
}); });
if (isMasquerading) { if (isMasquerading) {
it('is disabled', () => { it('is disabled', () => {
expect(loadToggle().props.disabled).toEqual(true); const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toHaveAttribute('aria-disabled', 'true');
}); });
} else { } else {
it('is enabled', () => { it('is enabled', () => {
expect(loadToggle().props.disabled).toEqual(false); const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeEnabled();
}); });
} }
test('show email settings modal on click', () => {
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
});
}); });
}; };
const testFacebookShareButton = () => { const testFacebookShareButton = () => {
test('renders facebook share button with courseName and brand', () => { it('renders facebook share button', () => {
const button = el.instance.findByType(ReactShare.FacebookShareButton)[0]; const facebookShareButton = screen.getByRole('button', { name: 'facebook' });
expect(button.props.url).toEqual(socialShare.facebook.shareUrl); expect(facebookShareButton).toBeInTheDocument();
expect(button.props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(track.socialShare, props.cardId, 'facebook'),
);
expect(button.props.title).toEqual(formatMessage(messages.shareQuote, {
courseName,
socialBrand: socialShare.facebook.socialBrand,
}));
}); });
}; };
const testTwitterShareButton = () => { const testTwitterShareButton = () => {
test('renders twitter share button with courseName and brand', () => { it('renders twitter share button', () => {
const button = el.instance.findByType(ReactShare.TwitterShareButton)[0]; const twitterShareButton = screen.getByRole('button', { name: 'twitter' });
expect(button.props.url).toEqual(socialShare.twitter.shareUrl); expect(twitterShareButton).toBeInTheDocument();
expect(button.props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(track.socialShare, props.cardId, 'twitter'),
);
expect(button.props.title).toEqual(formatMessage(messages.shareQuote, {
courseName,
socialBrand: socialShare.twitter.socialBrand,
}));
}); });
}; };
describe('all enabled', () => { describe('all enabled', () => {
@@ -174,19 +145,7 @@ describe('SocialShareMenu', () => {
twitter: { isEnabled: true }, twitter: { isEnabled: true },
isEmailEnabled: true, isEmailEnabled: true,
}); });
render(); renderComponent();
});
describe('email settings dropdown', () => {
const loadToggle = () => el.instance.findByTestId(testIds.emailSettingsModalToggle)[0];
it('renders', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(1);
});
it('is enabled', () => {
expect(loadToggle().props.disabled).toEqual(false);
});
test('show email settings modal on click', () => {
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
});
}); });
testEmailSettingsDropdown(); testEmailSettingsDropdown();
testFacebookShareButton(); testFacebookShareButton();
@@ -194,42 +153,49 @@ describe('SocialShareMenu', () => {
}); });
describe('only email enabled', () => { describe('only email enabled', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks();
mockHooks({ isEmailEnabled: true }); mockHooks({ isEmailEnabled: true });
render(); renderComponent();
}); });
testEmailSettingsDropdown(); testEmailSettingsDropdown();
it('does not render facebook or twitter controls', () => { it('does not render facebook or twitter controls', () => {
expect(el.instance.findByType(ReactShare.FacebookShareButton).length).toEqual(0); const facebookShareButton = screen.queryByRole('button', { name: 'facebook' });
expect(el.instance.findByType(ReactShare.TwitterShareButton).length).toEqual(0); expect(facebookShareButton).toBeNull();
const twitterShareButton = screen.queryByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeNull();
}); });
describe('masquerading', () => { });
beforeEach(() => { describe('masquerading', () => {
mockHooks({ isEmailEnabled: true, isMasquerading: true }); beforeEach(() => {
render(); mockHooks({ isEmailEnabled: true, isMasquerading: true });
}); renderComponent();
testEmailSettingsDropdown(true);
}); });
testEmailSettingsDropdown(true);
}); });
describe('only facebook enabled', () => { describe('only facebook enabled', () => {
beforeEach(() => { beforeEach(() => {
mockHooks({ facebook: { isEnabled: true } }); mockHooks({ facebook: { isEnabled: true } });
render(); renderComponent();
}); });
testFacebookShareButton(); testFacebookShareButton();
it('does not render email or twitter controls', () => { it('does not render email or twitter controls', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(0); const emailSettingsButton = screen.queryByRole('button', { name: messages.emailSettings.defaultMessage });
expect(el.instance.findByType(ReactShare.TwitterShareButton).length).toEqual(0); expect(emailSettingsButton).toBeNull();
const twitterShareButton = screen.queryByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeNull();
}); });
}); });
describe('only twitter enabled', () => { describe('only twitter enabled', () => {
beforeEach(() => { beforeEach(() => {
mockHooks({ twitter: { isEnabled: true } }); mockHooks({ twitter: { isEnabled: true } });
render(); renderComponent();
}); });
testTwitterShareButton(); testTwitterShareButton();
it('does not render email or facebook controls', () => { it('does not render email or facebook controls', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(0); const emailSettingsButton = screen.queryByRole('button', { name: messages.emailSettings.defaultMessage });
expect(el.instance.findByType(ReactShare.FacebookShareButton).length).toEqual(0); expect(emailSettingsButton).toBeNull();
const facebookShareButton = screen.queryByRole('button', { name: 'facebook' });
expect(facebookShareButton).toBeNull();
}); });
}); });
}); });

View File

@@ -1,81 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardMenu render show dropdown hide unenroll item and disable email snapshot 1`] = `
<Fragment>
<Dropdown
onToggle={[MockFunction hooks.handleToggleDropdown]}
>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
iconAs="Icon"
id="course-actions-dropdown-test-card-id"
src={[MockFunction icons.MoreVert]}
variant="primary"
/>
<Dropdown.Menu>
<SocialShareMenu
cardId="test-card-id"
emailSettings={
{
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
}
}
/>
</Dropdown.Menu>
</Dropdown>
<UnenrollConfirmModal
cardId="test-card-id"
closeModal={[MockFunction unenrollHide]}
show={false}
/>
</Fragment>
`;
exports[`CourseCardMenu render show dropdown show unenroll and enable email snapshot 1`] = `
<Fragment>
<Dropdown
onToggle={[MockFunction hooks.handleToggleDropdown]}
>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
iconAs="Icon"
id="course-actions-dropdown-test-card-id"
src={[MockFunction icons.MoreVert]}
variant="primary"
/>
<Dropdown.Menu>
<Dropdown.Item
data-testid="unenrollModalToggle"
disabled={false}
onClick={[MockFunction unenrollShow]}
>
Unenroll
</Dropdown.Item>
<SocialShareMenu
cardId="test-card-id"
emailSettings={
{
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
}
}
/>
</Dropdown.Menu>
</Dropdown>
<UnenrollConfirmModal
cardId="test-card-id"
closeModal={[MockFunction unenrollHide]}
show={false}
/>
<EmailSettingsModal
cardId="test-card-id"
closeModal={[MockFunction emailSettingHide]}
show={false}
/>
</Fragment>
`;

View File

@@ -1,15 +1,15 @@
import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils';
import track from 'tracking'; import track from 'tracking';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import { useState } from 'react';
import { StrictDict } from 'utils';
export const stateKeys = StrictDict({ export const state = StrictDict({
isUnenrollConfirmVisible: 'isUnenrollConfirmVisible', isUnenrollConfirmVisible: (val) => useState(val), // eslint-disable-line
isEmailSettingsVisible: 'isEmailSettingsVisible', isEmailSettingsVisible: (val) => useState(val), // eslint-disable-line
}); });
export const useUnenrollData = () => { export const useUnenrollData = () => {
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isUnenrollConfirmVisible, false); const [isVisible, setIsVisible] = state.isUnenrollConfirmVisible(false);
return { return {
show: () => setIsVisible(true), show: () => setIsVisible(true),
hide: () => setIsVisible(false), hide: () => setIsVisible(false),
@@ -18,7 +18,7 @@ export const useUnenrollData = () => {
}; };
export const useEmailSettings = () => { export const useEmailSettings = () => {
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isEmailSettingsVisible, false); const [isVisible, setIsVisible] = state.isEmailSettingsVisible(false);
return { return {
show: () => setIsVisible(true), show: () => setIsVisible(true),
hide: () => setIsVisible(false), hide: () => setIsVisible(false),

View File

@@ -1,7 +1,6 @@
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import track from 'tracking'; import track from 'tracking';
import { MockUseState } from 'testUtils';
import * as hooks from './hooks'; import * as hooks from './hooks';
@@ -19,7 +18,7 @@ reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
const cardId = 'test-card-id'; const cardId = 'test-card-id';
let out; let out;
const state = mockUseKeyedState(hooks.stateKeys); const state = new MockUseState(hooks);
describe('CourseCardMenu hooks', () => { describe('CourseCardMenu hooks', () => {
beforeEach(() => { beforeEach(() => {
@@ -28,7 +27,6 @@ describe('CourseCardMenu hooks', () => {
}); });
describe('useUnenrollData', () => { describe('useUnenrollData', () => {
beforeEach(() => { beforeEach(() => {
state.mockVals({ isUnenrollConfirmVisible: true });
out = hooks.useUnenrollData(); out = hooks.useUnenrollData();
}); });
describe('behavior', () => { describe('behavior', () => {
@@ -37,9 +35,6 @@ describe('CourseCardMenu hooks', () => {
}); });
}); });
describe('output', () => { describe('output', () => {
test('state is loaded from current state value', () => {
expect(out.isVisible).toEqual(true);
});
test('show sets state value to true', () => { test('show sets state value to true', () => {
out.show(); out.show();
expect(state.setState.isUnenrollConfirmVisible).toHaveBeenCalledWith(true); expect(state.setState.isUnenrollConfirmVisible).toHaveBeenCalledWith(true);
@@ -53,7 +48,6 @@ describe('CourseCardMenu hooks', () => {
describe('useEmailSettings', () => { describe('useEmailSettings', () => {
beforeEach(() => { beforeEach(() => {
state.mockVals({ isEmailSettingsVisible: true });
out = hooks.useEmailSettings(); out = hooks.useEmailSettings();
}); });
describe('behavior', () => { describe('behavior', () => {
@@ -62,9 +56,6 @@ describe('CourseCardMenu hooks', () => {
}); });
}); });
describe('output', () => { describe('output', () => {
test('state is loaded from current state value', () => {
expect(out.isVisible).toEqual(state.values.isEmailSettingsVisible);
});
test('show sets state value to true', () => { test('show sets state value to true', () => {
out.show(); out.show();
expect(state.setState.isEmailSettingsVisible).toHaveBeenCalledWith(true); expect(state.setState.isEmailSettingsVisible).toHaveBeenCalledWith(true);

View File

@@ -1,10 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown, Icon, IconButton } from '@openedx/paragon'; import { Dropdown, Icon, IconButton } from '@openedx/paragon';
import { MoreVert } from '@openedx/paragon/icons'; import { MoreVert } from '@openedx/paragon/icons';
import { StrictDict } from '@edx/react-unit-test-utils';
import EmailSettingsModal from 'containers/EmailSettingsModal'; import EmailSettingsModal from 'containers/EmailSettingsModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal'; import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
@@ -19,9 +17,9 @@ import {
import messages from './messages'; import messages from './messages';
export const testIds = StrictDict({ export const testIds = {
unenrollModalToggle: 'unenrollModalToggle', unenrollModalToggle: 'unenrollModalToggle',
}); };
export const CourseCardMenu = ({ cardId }) => { export const CourseCardMenu = ({ cardId }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();

View File

@@ -1,26 +1,23 @@
import { when } from 'jest-when'; import { when } from 'jest-when';
import { Dropdown } from '@openedx/paragon'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import userEvent from '@testing-library/user-event';
import { useIntl } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import EmailSettingsModal from 'containers/EmailSettingsModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import SocialShareMenu from './SocialShareMenu';
import * as hooks from './hooks'; import * as hooks from './hooks';
import CourseCardMenu, { testIds } from '.'; import CourseCardMenu from '.';
import messages from './messages';
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn().mockReturnValue({
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
}),
}));
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { useMasqueradeData: jest.fn(), useCardEnrollmentData: jest.fn() }, reduxHooks: {
useMasqueradeData: jest.fn(),
useCardEnrollmentData: jest.fn(),
},
})); }));
jest.mock('./SocialShareMenu', () => 'SocialShareMenu'); jest.mock('./SocialShareMenu', () => jest.fn(() => <div>SocialShareMenu</div>));
jest.mock('containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
jest.mock('containers/UnenrollConfirmModal', () => jest.fn(() => <div>UnenrollConfirmModal</div>));
jest.mock('./hooks', () => ({ jest.mock('./hooks', () => ({
useEmailSettings: jest.fn(), useEmailSettings: jest.fn(),
useUnenrollData: jest.fn(), useUnenrollData: jest.fn(),
@@ -44,13 +41,11 @@ const unenrollData = {
hide: jest.fn().mockName('unenrollHide'), hide: jest.fn().mockName('unenrollHide'),
}; };
let el;
const mockHook = (fn, returnValue, options = {}) => { const mockHook = (fn, returnValue, options = {}) => {
if (options.isCardHook) { if (options.isCardHook) {
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue); when(fn).calledWith(props.cardId).mockReturnValue(returnValue);
} else { } else {
when(fn).calledWith().mockReturnValueOnce(returnValue); when(fn).calledWith().mockReturnValue(returnValue);
} }
}; };
@@ -82,18 +77,13 @@ const mockHooks = (returnVals = {}) => {
); );
}; };
const render = () => { const renderComponent = () => render(<IntlProvider locale="en"><CourseCardMenu {...props} /></IntlProvider>);
el = shallow(<CourseCardMenu {...props} />);
};
describe('CourseCardMenu', () => { describe('CourseCardMenu', () => {
describe('behavior', () => { describe('hooks', () => {
beforeEach(() => { beforeEach(() => {
mockHooks(); mockHooks();
render(); renderComponent();
});
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalledWith();
}); });
it('initializes local hooks', () => { it('initializes local hooks', () => {
when(hooks.useEmailSettings).expectCalledWith(); when(hooks.useEmailSettings).expectCalledWith();
@@ -109,47 +99,30 @@ describe('CourseCardMenu', () => {
describe('render', () => { describe('render', () => {
it('renders null if showDropdown is false', () => { it('renders null if showDropdown is false', () => {
mockHooks(); mockHooks();
render(); renderComponent();
expect(el.isEmptyRender()).toEqual(true); const dropdown = screen.queryByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeNull();
}); });
const testHandleToggle = () => {
it('displays Dropdown with onToggle=handleToggleDropdown', () => {
expect(el.instance.findByType(Dropdown)[0].props.onToggle).toEqual(handleToggleDropdown);
});
};
const testUnenrollConfirmModal = () => {
it('displays UnenrollConfirmModal with cardId and unenrollModal data', () => {
const modal = el.instance.findByType(UnenrollConfirmModal)[0];
expect(modal.props.show).toEqual(unenrollData.isVisible);
expect(modal.props.closeModal).toEqual(unenrollData.hide);
expect(modal.props.cardId).toEqual(props.cardId);
});
};
const testSocialShareMenu = () => {
it('displays SocialShareMenu with cardID and emailSettings', () => {
const menu = el.instance.findByType(SocialShareMenu)[0];
expect(menu.props.cardId).toEqual(props.cardId);
expect(menu.props.emailSettings).toEqual(emailSettings);
});
};
describe('show dropdown', () => { describe('show dropdown', () => {
describe('hide unenroll item and disable email', () => { describe('hide unenroll item and disable email', () => {
beforeEach(() => { it('displays Dropdown and renders SocialShareMenu and UnenrollConfirmModal', async () => {
mockHooks({ shouldShowDropdown: true }); mockHooks({
render(); shouldShowDropdown: true,
});
renderComponent();
const user = userEvent.setup();
const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeInTheDocument();
await user.click(dropdown);
const unenrollOption = screen.queryByRole('button', { name: messages.unenroll.defaultMessage });
expect(unenrollOption).toBeNull();
const socialShareMenu = screen.getByText('SocialShareMenu');
expect(socialShareMenu).toBeInTheDocument();
const unenrollConfirmModal = screen.getByText('UnenrollConfirmModal');
expect(unenrollConfirmModal).toBeInTheDocument();
const emailSettingsModal = screen.queryByText('EmailSettingsModal');
expect(emailSettingsModal).toBeNull();
}); });
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
testHandleToggle();
testSocialShareMenu();
it('does not render unenroll modal toggle', () => {
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(0);
});
it('does not render EmailSettingsModal', () => {
expect(el.instance.findByType(EmailSettingsModal).length).toEqual(0);
});
testUnenrollConfirmModal();
}); });
describe('show unenroll and enable email', () => { describe('show unenroll and enable email', () => {
const hookProps = { const hookProps = {
@@ -157,57 +130,49 @@ describe('CourseCardMenu', () => {
isEmailEnabled: true, isEmailEnabled: true,
shouldShowUnenrollItem: true, shouldShowUnenrollItem: true,
}; };
beforeEach(() => {
mockHooks(hookProps);
render();
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
testHandleToggle();
testSocialShareMenu();
describe('unenroll modal toggle', () => { describe('unenroll modal toggle', () => {
let toggle;
describe('not masquerading', () => { describe('not masquerading', () => {
beforeEach(() => { it('renders all components', async () => {
mockHooks(hookProps); mockHooks(hookProps);
render(); renderComponent();
[toggle] = el.instance.findByTestId(testIds.unenrollModalToggle);
}); const user = userEvent.setup();
it('renders unenroll modal toggle', () => { const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(1); expect(dropdown).toBeInTheDocument();
}); await user.click(dropdown);
test('onClick from unenroll modal hook', () => {
expect(toggle.props.onClick).toEqual(unenrollData.show); const unenrollOption = screen.getByRole('button', { name: messages.unenroll.defaultMessage });
}); expect(unenrollOption).toBeInTheDocument();
test('disabled', () => { const socialShareMenu = screen.getByText('SocialShareMenu');
expect(toggle.props.disabled).toEqual(false); expect(socialShareMenu).toBeInTheDocument();
const unenrollConfirmModal = screen.getByText('UnenrollConfirmModal');
expect(unenrollConfirmModal).toBeInTheDocument();
const emailSettingsModal = screen.getByText('EmailSettingsModal');
expect(emailSettingsModal).toBeInTheDocument();
}); });
}); });
describe('masquerading', () => { describe('masquerading', () => {
beforeEach(() => { it('renders but unenroll is disabled', async () => {
mockHooks({ ...hookProps, isMasquerading: true }); mockHooks({ ...hookProps, isMasquerading: true });
render(); renderComponent();
[toggle] = el.instance.findByTestId(testIds.unenrollModalToggle);
}); const user = userEvent.setup();
it('renders', () => { const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(1); expect(dropdown).toBeInTheDocument();
}); await user.click(dropdown);
test('onClick from unenroll modal hook', () => {
expect(toggle.props.onClick).toEqual(unenrollData.show); const unenrollOption = screen.getByRole('button', { name: messages.unenroll.defaultMessage });
}); expect(unenrollOption).toBeInTheDocument();
test('disabled', () => { expect(unenrollOption).toHaveAttribute('aria-disabled', 'true');
expect(toggle.props.disabled).toEqual(true); const socialShareMenu = screen.getByText('SocialShareMenu');
expect(socialShareMenu).toBeInTheDocument();
const unenrollConfirmModal = screen.getByText('UnenrollConfirmModal');
expect(unenrollConfirmModal).toBeInTheDocument();
const emailSettingsModal = screen.getByText('EmailSettingsModal');
expect(emailSettingsModal).toBeInTheDocument();
}); });
}); });
}); });
testUnenrollConfirmModal();
it('displays EmaiSettingsModal with cardId and emailSettingsModal data', () => {
const modal = el.instance.findByType(EmailSettingsModal)[0];
expect(modal.props.show).toEqual(emailSettings.isVisible);
expect(modal.props.closeModal).toEqual(emailSettings.hide);
expect(modal.props.cardId).toEqual(props.cardId);
});
}); });
}); });
}); });

View File

@@ -1,12 +1,10 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import track from 'tracking'; import track from 'tracking';
import useActionDisabledState from './hooks'; import useActionDisabledState from './hooks';
import CourseCardTitle from './CourseCardTitle'; import CourseCardTitle from './CourseCardTitle';
const homeUrl = 'home-url';
jest.mock('tracking', () => ({ jest.mock('tracking', () => ({
course: { course: {
courseTitleClicked: jest.fn().mockName('segment.courseTitleClicked'), courseTitleClicked: jest.fn().mockName('segment.courseTitleClicked'),
@@ -15,53 +13,61 @@ jest.mock('tracking', () => ({
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCourseData: jest.fn(() => ({ courseName: 'course-name' })), useCardCourseData: jest.fn(),
useCardCourseRunData: jest.fn(() => ({ homeUrl })), useCardCourseRunData: jest.fn(),
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({ useTrackCourseEvent: jest.fn(),
trackCourseEvent: { eventName, cardId, url },
})),
}, },
})); }));
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false }))); jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
describe('CourseCardTitle', () => { describe('CourseCardTitle', () => {
const props = { const props = {
cardId: 'cardId', cardId: 'test-card-id',
}; };
const courseName = 'Test Course';
const homeUrl = 'http://test.com';
const handleTitleClick = jest.fn();
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
reduxHooks.useCardCourseData.mockReturnValue({ courseName });
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
reduxHooks.useTrackCourseEvent.mockReturnValue(handleTitleClick);
}); });
describe('snapshot', () => {
test('renders clickable link course title', () => { it('renders course name as link when not disabled', async () => {
const wrapper = shallow(<CourseCardTitle {...props} />); useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
expect(wrapper.snapshot).toMatchSnapshot(); render(<CourseCardTitle {...props} />);
const title = wrapper.instance.findByTestId('CourseCardTitle');
expect(title[0].type).toBe('a'); const user = userEvent.setup();
expect(title[0].props.onClick).toEqual( const link = screen.getByRole('link', { name: courseName });
reduxHooks.useTrackCourseEvent( expect(link).toHaveAttribute('href', homeUrl);
track.course.courseTitleClicked,
props.cardId, await user.click(link);
homeUrl, expect(handleTitleClick).toHaveBeenCalled();
),
);
});
test('renders disabled link', () => {
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
const wrapper = shallow(<CourseCardTitle {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
const title = wrapper.instance.findByTestId('CourseCardTitle');
expect(title[0].type).toBe('span');
expect(title[0].props.onClick).toBeUndefined();
});
}); });
describe('behavior', () => {
it('initializes', () => { it('renders course name as span when disabled', () => {
shallow(<CourseCardTitle {...props} />); useActionDisabledState.mockReturnValue({ disableCourseTitle: true });
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId); render(<CourseCardTitle {...props} />);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
props.cardId, const text = screen.getByText(courseName);
); expect(text).toBeInTheDocument();
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId); expect(text.tagName.toLowerCase()).toBe('span');
}); });
it('uses correct hooks with cardId', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
render(<CourseCardTitle {...props} />);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.courseTitleClicked,
props.cardId,
homeUrl,
);
}); });
}); });

View File

@@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RelatedProgramsBadge component snapshot: 3 programs 1`] = `
<Fragment>
<Button
className="pl-0 mr-0 justify-content-start align-self-start flex-shrink-1"
data-testid="RelatedProgramsBadge"
onClick={[MockFunction useRelatedProgramsBadge.openModal]}
size="sm"
variant="tertiary"
>
<Icon
className="mr-2 pr-0"
src={[MockFunction icons.Program]}
/>
useRelatedProgramsBadge.programsMessage
</Button>
<RelatedProgramsModal
cardId="test-course-number"
closeModal={[MockFunction useRelatedProgramsBadge.closeModal]}
isOpen={true}
/>
</Fragment>
`;

View File

@@ -12,6 +12,16 @@ jest.mock('hooks', () => ({
}, },
})); }));
jest.mock('@edx/frontend-platform/i18n', () => {
const { formatMessage } = jest.requireActual('testUtils');
return {
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({
formatMessage,
}),
};
});
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const state = new MockUseState(hooks); const state = new MockUseState(hooks);

View File

@@ -1,5 +1,5 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import useRelatedProgramsBadge from './hooks'; import useRelatedProgramsBadge from './hooks';
import RelatedProgramsBadge from '.'; import RelatedProgramsBadge from '.';
@@ -15,16 +15,21 @@ const hookProps = {
programsMessage: 'useRelatedProgramsBadge.programsMessage', programsMessage: 'useRelatedProgramsBadge.programsMessage',
}; };
const cardId = 'test-course-number'; const cardId = 'test-card-id';
describe('RelatedProgramsBadge component', () => { describe('RelatedProgramsBadge component', () => {
test('empty render: no programs', () => { it('should not render if no programs', () => {
useRelatedProgramsBadge.mockReturnValueOnce({ ...hookProps, numPrograms: 0 }); useRelatedProgramsBadge.mockReturnValueOnce({ ...hookProps, numPrograms: 0 });
const el = shallow(<RelatedProgramsBadge cardId={cardId} />); render(<IntlProvider locale="en"><RelatedProgramsBadge cardId={cardId} /></IntlProvider>);
expect(el.isEmptyRender()).toEqual(true); const button = screen.queryByRole('button', { name: hookProps.programsMessage });
expect(button).toBeNull();
const dialog = screen.queryByRole('dialog');
expect(dialog).toBeNull();
}); });
test('snapshot: 3 programs', () => { it('3 programs closed', () => {
useRelatedProgramsBadge.mockReturnValueOnce(hookProps); useRelatedProgramsBadge.mockReturnValue({ ...hookProps, isOpen: false });
expect(shallow(<RelatedProgramsBadge cardId={cardId} />).snapshot).toMatchSnapshot(); render(<IntlProvider locale="en"><RelatedProgramsBadge cardId={cardId} /></IntlProvider>);
const button = screen.getByRole('button', { name: hookProps.programsMessage });
expect(button).toBeInTheDocument();
}); });
}); });

View File

@@ -1,72 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
<a
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
href="home-url"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseImageClicked],
"url": "home-url",
},
}
}
tabIndex="-1"
>
<Fragment>
<img
alt="Course thumbnail"
className="pgn__card-image-cap w-100 show"
src="banner-img-src"
/>
<span
className="course-card-verify-ribbon-container"
title="You're enrolled as a verified student"
>
<Badge
as="div"
className="w-100"
variant="success"
>
Verified
</Badge>
<img
alt="ID Verified Ribbon/Badge"
src="test-file-stub"
/>
</span>
</Fragment>
</a>
`;
exports[`CourseCardImage snapshot renders disabled link 1`] = `
<div
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
>
<Fragment>
<img
alt="Course thumbnail"
className="pgn__card-image-cap w-100 show"
src="banner-img-src"
/>
<span
className="course-card-verify-ribbon-container"
title="You're enrolled as a verified student"
>
<Badge
as="div"
className="w-100"
variant="success"
>
Verified
</Badge>
<img
alt="ID Verified Ribbon/Badge"
src="test-file-stub"
/>
</span>
</Fragment>
</div>
`;

View File

@@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
<h3>
<a
className="course-card-title"
data-testid="CourseCardTitle"
href="home-url"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseTitleClicked],
"url": "home-url",
},
}
}
>
course-name
</a>
</h3>
`;
exports[`CourseCardTitle snapshot renders disabled link 1`] = `
<h3>
<span
className="course-card-title"
data-testid="CourseCardTitle"
>
course-name
</span>
</h3>
`;

View File

@@ -11,6 +11,16 @@ jest.mock('hooks', () => ({
}, },
})); }));
jest.mock('@edx/frontend-platform/i18n', () => {
const { formatMessage } = jest.requireActual('testUtils');
return {
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({
formatMessage,
}),
};
});
const cardId = 'my-test-course-number'; const cardId = 'my-test-course-number';
describe('CourseCard hooks', () => { describe('CourseCard hooks', () => {

View File

@@ -1,5 +1,5 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import CourseCard from '.'; import CourseCard from '.';
import hooks from './hooks'; import hooks from './hooks';
@@ -8,22 +8,43 @@ jest.mock('./hooks', () => ({
useIsCollapsed: jest.fn(), useIsCollapsed: jest.fn(),
})); }));
jest.mock('./components/CourseCardBanners', () => 'CourseCardBanners'); const namesMockComponents = [
jest.mock('./components/CourseCardImage', () => 'CourseCardImage'); 'CourseCardBanners',
jest.mock('./components/CourseCardMenu', () => 'CourseCardMenu'); 'CourseCardImage',
jest.mock('./components/CourseCardActions', () => 'CourseCardActions'); 'CourseCardMenu',
jest.mock('./components/CourseCardDetails', () => 'CourseCardDetails'); 'CourseCardActions',
jest.mock('./components/CourseCardTitle', () => 'CourseCardTitle'); 'CourseCardDetails',
'CourseCardTitle',
];
jest.mock('./components/CourseCardBanners', () => jest.fn(() => <div>CourseCardBanners</div>));
jest.mock('./components/CourseCardImage', () => jest.fn(() => <div>CourseCardImage</div>));
jest.mock('./components/CourseCardMenu', () => jest.fn(() => <div>CourseCardMenu</div>));
jest.mock('./components/CourseCardActions', () => jest.fn(() => <div>CourseCardActions</div>));
jest.mock('./components/CourseCardDetails', () => jest.fn(() => <div>CourseCardDetails</div>));
jest.mock('./components/CourseCardTitle', () => jest.fn(() => <div>CourseCardTitle</div>));
const cardId = 'test-card-id'; const cardId = 'test-card-id';
describe('CourseCard component', () => { describe('CourseCard component', () => {
test('snapshot: collapsed', () => { it('collapsed', () => {
hooks.useIsCollapsed.mockReturnValueOnce(true); hooks.useIsCollapsed.mockReturnValueOnce(true);
expect(shallow(<CourseCard cardId={cardId} />).snapshot).toMatchSnapshot(); render(<IntlProvider locale="en"><CourseCard cardId={cardId} /></IntlProvider>);
const cardImage = screen.getByText('CourseCardImage');
expect(cardImage.parentElement).not.toHaveClass('d-flex');
}); });
test('snapshot: not collapsed', () => { it('not collapsed', () => {
hooks.useIsCollapsed.mockReturnValueOnce(false); hooks.useIsCollapsed.mockReturnValueOnce(false);
expect(shallow(<CourseCard cardId={cardId} />).snapshot).toMatchSnapshot(); render(<IntlProvider locale="en"><CourseCard cardId={cardId} /></IntlProvider>);
const cardImage = screen.getByText('CourseCardImage');
expect(cardImage.parentElement).toHaveClass('d-flex');
});
it('renders courseCard child components', () => {
hooks.useIsCollapsed.mockReturnValueOnce(false);
render(<IntlProvider locale="en"><CourseCard cardId={cardId} /></IntlProvider>);
namesMockComponents.map((courseCardName) => {
const courseCardComponent = screen.getByText(courseCardName);
return expect(courseCardComponent).toBeInTheDocument();
});
}); });
}); });

View File

@@ -1,17 +1,28 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { FilterKeys } from 'data/constants/app'; import { FilterKeys } from 'data/constants/app';
import ActiveCourseFilters from './ActiveCourseFilters'; import ActiveCourseFilters from './ActiveCourseFilters';
import messages from './messages';
const filters = Object.values(FilterKeys);
describe('ActiveCourseFilters', () => { describe('ActiveCourseFilters', () => {
const props = { const props = {
filters: Object.values(FilterKeys), filters,
handleRemoveFilter: jest.fn().mockName('handleRemoveFilter'), handleRemoveFilter: jest.fn().mockName('handleRemoveFilter'),
}; };
describe('snapshot', () => { it('renders chips correctly', () => {
test('renders', () => { render(<IntlProvider locale="en"><ActiveCourseFilters {...props} /></IntlProvider>);
const wrapper = shallow(<ActiveCourseFilters {...props} />); filters.map((key) => {
expect(wrapper.snapshot).toMatchSnapshot(); const chip = screen.getByText(formatMessage(messages[key]));
return expect(chip).toBeInTheDocument();
}); });
}); });
it('renders button correctly', () => {
render(<IntlProvider locale="en"><ActiveCourseFilters {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: formatMessage(messages.clearAll) });
expect(button).toBeInTheDocument();
});
}); });

View File

@@ -1,9 +1,11 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { formatMessage } from 'testUtils';
import { breakpoints, useWindowSize } from '@openedx/paragon'; import { breakpoints, useWindowSize } from '@openedx/paragon';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { FilterKeys, SortKeys } from 'data/constants/app';
import messages from './messages';
import CourseFilterControls from './CourseFilterControls'; import CourseFilterControls from './CourseFilterControls';
import useCourseFilterControlsData from './hooks'; import useCourseFilterControlsData from './hooks';
@@ -11,50 +13,63 @@ jest.mock('hooks', () => ({
reduxHooks: { useHasCourses: jest.fn() }, reduxHooks: { useHasCourses: jest.fn() },
})); }));
jest.mock('./hooks', () => jest.fn().mockName('useCourseFilterControlsData')); jest.mock('./hooks', () => jest.fn());
jest.mock('./components/FilterForm', () => 'FilterForm'); jest.mock('@openedx/paragon', () => ({
jest.mock('./components/SortForm', () => 'SortForm'); ...jest.requireActual('@openedx/paragon'),
useWindowSize: jest.fn(),
}));
reduxHooks.useHasCourses.mockReturnValue(true); const filters = Object.values(FilterKeys);
const mockControlsData = {
isOpen: false,
open: jest.fn().mockName('open'),
close: jest.fn().mockName('close'),
target: 'target-test',
setTarget: jest.fn(),
handleFilterChange: jest.fn().mockName('handleFilterChange'),
handleSortChange: jest.fn().mockName('handleSortChange'),
};
describe('CourseFilterControls', () => { describe('CourseFilterControls', () => {
const props = { const props = {
sortBy: 'test-sort-by', sortBy: SortKeys.enrolled,
setSortBy: jest.fn().mockName('setSortBy'), setSortBy: jest.fn().mockName('setSortBy'),
filters: ['test-filter'], filters,
}; };
useCourseFilterControlsData.mockReturnValue({ describe('mobile and open', () => {
isOpen: false, it('should render sheet', () => {
open: jest.fn().mockName('open'), reduxHooks.useHasCourses.mockReturnValue(true);
close: jest.fn().mockName('close'), useCourseFilterControlsData.mockReturnValue({ ...mockControlsData, isOpen: true });
target: 'test-target',
setTarget: jest.fn().mockName('setTarget'),
handleFilterChange: jest.fn().mockName('handleFilterChange'),
handleSortChange: jest.fn().mockName('handleSortChange'),
});
describe('no courses', () => {
test('snapshot', () => {
reduxHooks.useHasCourses.mockReturnValueOnce(false);
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
const wrapper = shallow(<CourseFilterControls {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
describe('mobile', () => {
test('snapshot', () => {
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth - 1 }); useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth - 1 });
const wrapper = shallow(<CourseFilterControls {...props} />); render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
expect(wrapper.snapshot).toMatchSnapshot(); const sheet = screen.getByRole('presentation', { hidden: true });
expect(sheet).toBeInTheDocument();
expect(sheet.parentElement).toHaveClass('sheet-container');
}); });
}); });
describe('is not mobile', () => { describe('is not mobile', () => {
test('snapshot', () => { it('should have button disabled', () => {
reduxHooks.useHasCourses.mockReturnValue(true);
useCourseFilterControlsData.mockReturnValue({ ...mockControlsData, isOpen: true });
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth }); useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
const wrapper = shallow(<CourseFilterControls {...props} />); render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
expect(wrapper.snapshot).toMatchSnapshot(); const filterForm = screen.getByText(messages.courseStatus.defaultMessage);
const modal = filterForm.closest('div.pgn__modal-popup__tooltip');
expect(modal).toBeInTheDocument();
});
});
describe('no courses', () => {
it('should have button disabled', () => {
reduxHooks.useHasCourses.mockReturnValue(false);
useCourseFilterControlsData.mockReturnValue(mockControlsData);
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: formatMessage(messages.refine) });
expect(button).toBeInTheDocument();
expect(button).toBeDisabled();
}); });
}); });
}); });

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ActiveCourseFilters snapshot renders 1`] = `
<div
id="course-list-active-filters"
>
<Chip
key="inProgress"
>
In-Progress
</Chip>
<Chip
key="notStarted"
>
Not Started
</Chip>
<Chip
key="done"
>
Done
</Chip>
<Chip
key="notEnrolled"
>
Not Enrolled
</Chip>
<Chip
key="upgraded"
>
Upgraded
</Chip>
<Button
onClick={[Function]}
variant="link"
>
Clear all
</Button>
</div>
`;

View File

@@ -1,169 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseFilterControls is not mobile snapshot 1`] = `
<div
id="course-filter-controls"
>
<Button
disabled={false}
iconBefore={[MockFunction icons.Tune]}
onClick={[MockFunction open]}
variant="outline-primary"
>
Refine
</Button>
<Form>
<ModalPopup
isOpen={false}
onClose={[MockFunction close]}
placement="bottom-end"
positionRef="test-target"
>
<div
className="bg-white p-3 rounded shadow d-flex flex-row"
id="course-filter-controls-card"
>
<div
className="filter-form-col"
>
<FilterForm
filters={
[
"test-filter",
]
}
handleFilterChange={[MockFunction handleFilterChange]}
/>
</div>
<hr
className="h-100 bg-primary-200 mx-3 my-0"
/>
<div
className="filter-form-col text-left m-1"
>
<SortForm
handleSortChange={[MockFunction handleSortChange]}
sortBy="test-sort-by"
/>
</div>
</div>
</ModalPopup>
</Form>
</div>
`;
exports[`CourseFilterControls mobile snapshot 1`] = `
<div
id="course-filter-controls"
>
<Button
disabled={false}
iconBefore={[MockFunction icons.Tune]}
onClick={[MockFunction open]}
variant="outline-primary"
>
Refine
</Button>
<Form>
<Sheet
className="w-75"
onClose={[MockFunction close]}
position="left"
show={false}
>
<div
className="p-1 mr-3"
>
<b>
Refine
</b>
</div>
<hr />
<div
className="filter-form-row"
>
<FilterForm
filters={
[
"test-filter",
]
}
handleFilterChange={[MockFunction handleFilterChange]}
/>
</div>
<div
className="filter-form-row text-left m-1"
>
<SortForm
handleSortChange={[MockFunction handleSortChange]}
sortBy="test-sort-by"
/>
</div>
<div
className="pgn__modal-close-container"
>
<ModalCloseButton
onClick={[MockFunction close]}
variant="tertiary"
>
<Icon
src={[MockFunction icons.Close]}
/>
</ModalCloseButton>
</div>
</Sheet>
</Form>
</div>
`;
exports[`CourseFilterControls no courses snapshot 1`] = `
<div
id="course-filter-controls"
>
<Button
disabled={true}
iconBefore={[MockFunction icons.Tune]}
onClick={[MockFunction open]}
variant="outline-primary"
>
Refine
</Button>
<Form>
<ModalPopup
isOpen={false}
onClose={[MockFunction close]}
placement="bottom-end"
positionRef="test-target"
>
<div
className="bg-white p-3 rounded shadow d-flex flex-row"
id="course-filter-controls-card"
>
<div
className="filter-form-col"
>
<FilterForm
filters={
[
"test-filter",
]
}
handleFilterChange={[MockFunction handleFilterChange]}
/>
</div>
<hr
className="h-100 bg-primary-200 mx-3 my-0"
/>
<div
className="filter-form-col text-left m-1"
>
<SortForm
handleSortChange={[MockFunction handleSortChange]}
sortBy="test-sort-by"
/>
</div>
</div>
</ModalPopup>
</Form>
</div>
`;

View File

@@ -1,14 +1,17 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { formatMessage } from 'testUtils';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { FilterKeys } from 'data/constants/app'; import { FilterKeys } from 'data/constants/app';
import Checkbox from './Checkbox'; import Checkbox from './Checkbox';
import messages from '../messages';
describe('Checkbox', () => { describe('Checkbox', () => {
describe('snapshot', () => { describe('renders correctly', () => {
Object.keys(FilterKeys).forEach((filterKey) => { Object.keys(FilterKeys).forEach((filterKey) => {
it(`renders ${filterKey}`, () => { it(`renders ${filterKey}`, () => {
const wrapper = shallow(<Checkbox filterKey={filterKey} />); render(<IntlProvider locale="en"><Checkbox filterKey={filterKey} /></IntlProvider>);
expect(wrapper.snapshot).toMatchSnapshot(); expect(screen.getByText(formatMessage(messages[filterKey]))).toBeInTheDocument();
}); });
}); });
}); });

View File

@@ -1,29 +1,54 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { FilterKeys } from 'data/constants/app'; import { FilterKeys } from 'data/constants/app';
import FilterForm, { filterOrder } from './FilterForm'; import { FilterForm, filterOrder } from './FilterForm';
import messages from '../messages';
jest.mock('./Checkbox', () => 'Checkbox'); const mockHandleFilterChange = jest.fn();
const defaultProps = {
filters: [FilterKeys.inProgress],
handleFilterChange: mockHandleFilterChange,
};
const renderComponent = (props = defaultProps) => render(
<IntlProvider messages={{}}>
<FilterForm {...props} />
</IntlProvider>,
);
describe('FilterForm', () => { describe('FilterForm', () => {
const props = { beforeEach(() => {
filters: ['test-filter'], jest.clearAllMocks();
handleFilterChange: jest.fn().mockName('handleFilterChange'), });
};
describe('snapshot', () => { it('renders all filter checkboxes in correct order', () => {
test('renders', () => { renderComponent();
const wrapper = shallow(<FilterForm {...props} />); const checkboxes = screen.getAllByRole('checkbox');
expect(wrapper.snapshot).toMatchSnapshot(); expect(checkboxes).toHaveLength(filterOrder.length);
checkboxes.forEach((checkbox, index) => {
expect(checkbox).toHaveAttribute('value', filterOrder[index]);
}); });
}); });
test('filterOrder', () => { it('checks boxes based on filters prop', () => {
expect(filterOrder).toEqual([ const filters = [FilterKeys.inProgress, FilterKeys.done];
FilterKeys.inProgress, renderComponent({ ...defaultProps, filters });
FilterKeys.notStarted, filters.forEach(filter => {
FilterKeys.done, expect(screen.getByRole('checkbox', { name: formatMessage(messages[filter]) })).toBeChecked();
FilterKeys.notEnrolled, });
FilterKeys.upgraded, });
]);
it('calls handleFilterChange when checkbox is clicked', () => {
renderComponent();
const checkbox = screen.getByRole('checkbox', { name: formatMessage(messages.notStarted) });
fireEvent.click(checkbox);
expect(mockHandleFilterChange).toHaveBeenCalled();
});
it('displays course status heading', () => {
renderComponent();
expect(screen.getByText(/course status/i)).toBeInTheDocument();
}); });
}); });

View File

@@ -1,19 +1,29 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { SortKeys } from 'data/constants/app'; import { SortKeys } from 'data/constants/app';
import SortForm from './SortForm'; import SortForm from './SortForm';
import messages from '../messages';
jest.mock('./Checkbox', () => 'Checkbox');
describe('SortForm', () => { describe('SortForm', () => {
const props = { const props = {
handleSortChange: jest.fn().mockName('handleSortChange'), handleSortChange: jest.fn().mockName('handleSortChange'),
sortBy: SortKeys.enrolled, sortBy: SortKeys.enrolled,
}; };
describe('snapshot', () => { it('renders heading', () => {
test('renders', () => { render(<IntlProvider locale="en"><SortForm {...props} /></IntlProvider>);
const wrapper = shallow(<SortForm {...props} />); const heading = screen.getByText(formatMessage(messages.sort));
expect(wrapper.snapshot).toMatchSnapshot(); expect(heading).toBeInTheDocument();
}); });
it('renders radio enrolled', () => {
render(<IntlProvider locale="en"><SortForm {...props} /></IntlProvider>);
const enrolled = screen.getByRole('radio', { name: formatMessage(messages.sortLastEnrolled) });
expect(enrolled).toBeInTheDocument();
});
it('renders radio title', () => {
render(<IntlProvider locale="en"><SortForm {...props} /></IntlProvider>);
const title = screen.getByRole('radio', { name: formatMessage(messages.sortTitle) });
expect(title).toBeInTheDocument();
}); });
}); });

View File

@@ -1,46 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Checkbox snapshot renders done 1`] = `
<Form.Checkbox
className="py-2"
value="done"
>
Done
</Form.Checkbox>
`;
exports[`Checkbox snapshot renders inProgress 1`] = `
<Form.Checkbox
className="py-2"
value="inProgress"
>
In-Progress
</Form.Checkbox>
`;
exports[`Checkbox snapshot renders notEnrolled 1`] = `
<Form.Checkbox
className="py-2"
value="notEnrolled"
>
Not Enrolled
</Form.Checkbox>
`;
exports[`Checkbox snapshot renders notStarted 1`] = `
<Form.Checkbox
className="py-2"
value="notStarted"
>
Not Started
</Form.Checkbox>
`;
exports[`Checkbox snapshot renders upgraded 1`] = `
<Form.Checkbox
className="py-2"
value="upgraded"
>
Upgraded
</Form.Checkbox>
`;

View File

@@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterForm snapshot renders 1`] = `
<Form.Group>
<div
className="filter-form-heading mb-1"
>
Course Status
</div>
<Form.CheckboxSet
name="course-status-filters"
onChange={[MockFunction handleFilterChange]}
value={
[
"test-filter",
]
}
>
<Checkbox
filterKey="inProgress"
key="inProgress"
/>
<Checkbox
filterKey="notStarted"
key="notStarted"
/>
<Checkbox
filterKey="done"
key="done"
/>
<Checkbox
filterKey="notEnrolled"
key="notEnrolled"
/>
<Checkbox
filterKey="upgraded"
key="upgraded"
/>
</Form.CheckboxSet>
</Form.Group>
`;

View File

@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SortForm snapshot renders 1`] = `
<Fragment>
<div
className="filter-form-heading mb-1"
>
Sort
</div>
<Form.RadioSet
name="sort"
onChange={[MockFunction handleSortChange]}
value="enrolled"
>
<Form.Radio
className="py-2"
value="enrolled"
>
Last enrolled
</Form.Radio>
<Form.Radio
className="py-2"
value="title"
>
Title (A-Z)
</Form.Radio>
</Form.RadioSet>
</Fragment>
`;

View File

@@ -7,6 +7,15 @@ import track from 'tracking';
import * as hooks from './hooks'; import * as hooks from './hooks';
jest.mock('@openedx/paragon', () => ({
...jest.requireActual('@openedx/paragon'),
useToggle: jest.fn().mockImplementation((val) => [
val,
jest.fn().mockName('useToggle.setTrue'),
jest.fn().mockName('useToggle.setFalse'),
]),
}));
jest.mock('tracking', () => ({ jest.mock('tracking', () => ({
filter: { filter: {
filterClicked: jest.fn(), filterClicked: jest.fn(),

View File

@@ -1,70 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseList collapsed with multiple courses and pages snapshot 1`] = `
<Fragment>
<div
id="course-list-active-filters-container"
>
<ActiveCourseFilters />
</div>
<div
className="d-flex flex-column flex-grow-1"
>
<CourseCard
cardId="foo"
key="foo"
/>
<CourseCard
cardId="bar"
key="bar"
/>
<CourseCard
cardId="baz"
key="baz"
/>
<Pagination
className="mx-auto mb-2"
pageCount={3}
paginationLabel="Course List"
variant="reduced"
/>
</div>
</Fragment>
`;
exports[`CourseList no courses or filters snapshot 1`] = `
<Fragment>
<div
className="d-flex flex-column flex-grow-1"
/>
</Fragment>
`;
exports[`CourseList with filters snapshot 1`] = `undefined`;
exports[`CourseList with multiple courses and pages snapshot 1`] = `
<Fragment>
<div
className="d-flex flex-column flex-grow-1"
>
<CourseCard
cardId="foo"
key="foo"
/>
<CourseCard
cardId="bar"
key="bar"
/>
<CourseCard
cardId="baz"
key="baz"
/>
<Pagination
className="mx-auto mb-2"
pageCount={3}
paginationLabel="Course List"
variant="secondary"
/>
</div>
</Fragment>
`;

View File

@@ -1,4 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { useIsCollapsed } from './hooks'; import { useIsCollapsed } from './hooks';
import CourseList from '.'; import CourseList from '.';
@@ -7,9 +7,9 @@ jest.mock('./hooks', () => ({
useIsCollapsed: jest.fn(), useIsCollapsed: jest.fn(),
})); }));
jest.mock('containers/CourseCard', () => 'CourseCard'); jest.mock('containers/CourseCard', () => jest.fn(() => <div>CourseCard</div>));
jest.mock('containers/CourseFilterControls', () => ({ jest.mock('containers/CourseFilterControls', () => ({
ActiveCourseFilters: 'ActiveCourseFilters', ActiveCourseFilters: jest.fn(() => <div>ActiveCourseFilters</div>),
})); }));
describe('CourseList', () => { describe('CourseList', () => {
@@ -22,43 +22,60 @@ describe('CourseList', () => {
}; };
useIsCollapsed.mockReturnValue(false); useIsCollapsed.mockReturnValue(false);
const createWrapper = (courseListData = defaultCourseListData) => ( const renderList = (courseListData = defaultCourseListData) => (
shallow(<CourseList courseListData={courseListData} />) render(<CourseList courseListData={courseListData} />)
); );
describe('no courses or filters', () => { describe('no courses or filters', () => {
test('snapshot', () => { it('should not render related components', () => {
const wrapper = createWrapper(); renderList();
expect(wrapper.snapshot).toMatchSnapshot(); const filterControls = screen.queryByText('ActiveCourseFilters');
const courseCard = screen.queryByText('CourseCard');
const prevButton = screen.queryByRole('button', { name: 'Previous' });
expect(filterControls).toBeNull();
expect(courseCard).toBeNull();
expect(prevButton).toBeNull();
}); });
}); });
describe('with filters', () => { describe('with filters', () => {
test('snapshot', () => { it('should render filter component', () => {
const wrapper = createWrapper({ renderList({
filterOptions: { abitary: 'filter' }, ...defaultCourseListData,
showFilters: true, showFilters: true,
}); });
expect(wrapper.snapshot).toMatchSnapshot(); const filterControls = screen.getByText('ActiveCourseFilters');
expect(filterControls).toBeInTheDocument();
}); });
}); });
describe('with multiple courses and pages', () => { describe('with multiple courses and pages', () => {
test('snapshot', () => { it('render Course Cards and pagination', () => {
const wrapper = createWrapper({ const visibleList = [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }];
visibleList: [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }], const numPages = 3;
numPages: 3, renderList({
...defaultCourseListData,
visibleList,
numPages,
}); });
expect(wrapper.snapshot).toMatchSnapshot(); const courseCards = screen.getAllByText('CourseCard');
expect(courseCards.length).toEqual(visibleList.length);
const pageButtons = screen.getAllByRole('button', { name: /^Page/i });
expect(pageButtons.length).toBe(numPages);
}); });
}); });
describe('collapsed with multiple courses and pages', () => { describe('collapsed with multiple courses and pages', () => {
test('snapshot', () => { it('should render correct components', () => {
const visibleList = [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }];
useIsCollapsed.mockReturnValueOnce(true); useIsCollapsed.mockReturnValueOnce(true);
const wrapper = createWrapper({ renderList({
visibleList: [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }], ...defaultCourseListData,
visibleList,
numPages: 3, numPages: 3,
showFilters: true, showFilters: true,
}); });
expect(wrapper.snapshot).toMatchSnapshot(); const courseCards = screen.getAllByText('CourseCard');
expect(courseCards.length).toEqual(visibleList.length);
const reducedPagination = screen.getByRole('button', { name: '1 of 3' });
expect(reducedPagination).toBeInTheDocument();
}); });
}); });
}); });

View File

@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoCoursesView snapshot 1`] = `
<div
className="d-flex align-items-center justify-content-center mb-4.5"
id="no-courses-content-view"
>
<Image
alt="No Courses view banner"
src="icon/mock/path"
/>
<h3
className="h1"
>
Looking for a new challenge?
</h3>
<p>
Explore our courses to add them to your dashboard.
</p>
<Button
as="a"
href="http://localhost:18000/course-search-url"
iconBefore={[MockFunction icons.Search]}
variant="brand"
>
Explore courses
</Button>
</div>
`;

View File

@@ -1,15 +1,13 @@
@import "@openedx/paragon/scss/core/core";
#no-courses-content-view { #no-courses-content-view {
border: 2px solid $light-400; border: 2px solid var(--pgn-color-light-400);
flex-direction: column; flex-direction: column;
padding-bottom: map-get($spacers, 5); padding-bottom: var(--pgn-spacing-spacer-5);
padding-top: map-get($spacers, 5); padding-top: var(--pgn-spacing-spacer-5);
height: 100%; height: 100%;
& > * { & > * {
margin-top: map-get($spacers, 3); margin-top: var(--pgn-spacing-spacer-3);
margin-bottom: map-get($spacers, 3); margin-bottom: var(--pgn-spacing-spacer-3);
} }
} }

View File

@@ -1,18 +1,38 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { baseAppUrl } from 'data/services/lms/urls';
import EmptyCourse from '.'; import EmptyCourse from '.';
import messages from './messages';
const courseSearchUrl = '/course-search-url';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
usePlatformSettingsData: jest.fn(() => ({ usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: '/course-search-url', courseSearchUrl,
})), })),
}, },
})); }));
describe('NoCoursesView', () => { describe('NoCoursesView', () => {
test('snapshot', () => { it('should display image, heading and button', () => {
expect(shallow(<EmptyCourse />).snapshot).toMatchSnapshot(); render(<IntlProvider locale="en"><EmptyCourse /></IntlProvider>);
const image = screen.getByRole('img', { alt: formatMessage(messages.bannerAlt) });
expect(image).toBeInTheDocument();
});
it('should display heading and prompt', () => {
render(<IntlProvider locale="en"><EmptyCourse /></IntlProvider>);
const heading = screen.getByText(formatMessage(messages.lookingForChallengePrompt));
const prompt = screen.getByText(formatMessage(messages.exploreCoursesPrompt));
expect(heading).toBeInTheDocument();
expect(prompt).toBeInTheDocument();
});
it('should display button', () => {
render(<IntlProvider locale="en"><EmptyCourse /></IntlProvider>);
const button = screen.getByRole('link', { name: formatMessage(messages.exploreCoursesButton) });
expect(button).toBeInTheDocument();
expect(button.href).toBe(baseAppUrl(courseSearchUrl));
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More