Compare commits
6 Commits
hunia/upgr
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49d64ff5c6 | ||
|
|
fe658c1796 | ||
|
|
82dbc27aba | ||
|
|
25d3f831a2 | ||
|
|
bc68a8c674 | ||
|
|
15f9969993 |
3
.env
3
.env
@@ -32,6 +32,7 @@ ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
LEARNING_BASE_URL=''
|
||||
ZENDESK_KEY=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
@@ -39,5 +40,5 @@ ACCOUNT_SETTINGS_URL=''
|
||||
ACCOUNT_PROFILE_URL=''
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
|
||||
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL=''
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
@@ -38,6 +38,7 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
ZENDESK_KEY=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
@@ -45,5 +46,5 @@ ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
|
||||
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL=''
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
@@ -37,6 +37,7 @@ ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
ZENDESK_KEY='test-zendesk-key'
|
||||
HOTJAR_APP_ID='hot-jar-app-id'
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
@@ -44,5 +45,5 @@ ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
|
||||
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY='SDK Key'
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
||||
ENABLE_PROGRAMS=false
|
||||
|
||||
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -1 +1,8 @@
|
||||
# Root app is developed and owned by Aurora
|
||||
* @openedx/2U-aperture
|
||||
|
||||
# WIDGETS and experiments are developed and owned by separate teams below
|
||||
|
||||
# Recommendations panel
|
||||
/src/widgets/RecommendationsPanel @openedx/2U-vanguards
|
||||
/src/widgets/LookingForChallengeWidget @openedx/2U-vanguards
|
||||
|
||||
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -1,7 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Adding new check for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@@ -10,16 +10,18 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -37,7 +39,21 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
uses: dawidd6/action-send-mail@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
server_address: email-smtp.us-east-1.amazonaws.com
|
||||
server_port: 465
|
||||
username: ${{ secrets.EDX_SMTP_USERNAME }}
|
||||
password: ${{ secrets.EDX_SMTP_PASSWORD }}
|
||||
subject: CI workflow failed in ${{github.repository}}
|
||||
to: masters-grades@edx.org,aperture@2u-internal.opsgenie.net
|
||||
from: github-actions <github-actions@edx.org>
|
||||
nodemailerlog: true
|
||||
nodemailerdebug: true
|
||||
body: CI workflow in ${{github.repository}} failed!
|
||||
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
|
||||
}}"
|
||||
|
||||
35
.github/workflows/npm-publish.yml
vendored
Normal file
35
.github/workflows/npm-publish.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Release CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Create Build
|
||||
run: npm run build
|
||||
|
||||
- name: Release Package
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
|
||||
run: npm semantic-release
|
||||
@@ -1 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
|
||||
27
.releaserc
Normal file
27
.releaserc
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"branch": "master",
|
||||
"tagFormat": "v${version}",
|
||||
"verifyConditions": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"analyzeCommits": "@semantic-release/commit-analyzer",
|
||||
"generateNotes": "@semantic-release/release-notes-generator",
|
||||
"prepare": "@semantic-release/npm",
|
||||
"publish": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"success": [],
|
||||
"fail": []
|
||||
}
|
||||
@@ -59,6 +59,7 @@ module.exports = {
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
|
||||
LEARNING_BASE_URL: 'http://localhost:2000',
|
||||
SESSION_COOKIE_DOMAIN: 'localhost',
|
||||
ZENDESK_KEY: '',
|
||||
HOTJAR_APP_ID: '',
|
||||
HOTJAR_VERSION: 6,
|
||||
HOTJAR_DEBUG: '',
|
||||
@@ -68,5 +69,6 @@ module.exports = {
|
||||
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
|
||||
ENABLE_NOTICES: '',
|
||||
CAREER_LINK_URL: '',
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY: '',
|
||||
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
|
||||
};
|
||||
|
||||
24441
package-lock.json
generated
24441
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
75
package.json
75
package.json
@@ -6,9 +6,6 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
@@ -16,12 +13,11 @@
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"dev": "PUBLIC_PATH=/learner-dashboard/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"quality": "npm run lint-fix && npm run test",
|
||||
"watch-tests": "jest --watch",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"prepare": "husky"
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -31,59 +27,72 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-header": "^5.6.0",
|
||||
"@edx/frontend-enterprise-hotjar": "7.1.0",
|
||||
"@edx/frontend-platform": "8.1.5",
|
||||
"@edx/browserslist-config": "^1.1.0",
|
||||
"@edx/frontend-enterprise-hotjar": "3.0.0",
|
||||
"@edx/frontend-platform": "^7.1.4",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/react-unit-test-utils": "3.0.0",
|
||||
"@edx/react-unit-test-utils": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.1.2",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "^22.2.2",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"@optimizely/react-sdk": "^2.9.2",
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.28.0",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.40.0",
|
||||
"filesize": "^10.0.0",
|
||||
"core-js": "3.16.2",
|
||||
"dompurify": "^2.3.1",
|
||||
"email-prop-type": "^3.0.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "^8.0.6",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "5.3.0",
|
||||
"history": "5.0.1",
|
||||
"html-react-parser": "^1.3.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-when": "^3.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"prop-types": "^15.7.2",
|
||||
"query-string": "7.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "6.8.9",
|
||||
"react-intl": "6.4.7",
|
||||
"react-pdf": "^7.0.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "6.29.0",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-share": "^4.4.0",
|
||||
"redux": "4.2.1",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.1.1",
|
||||
"redux-beacon": "^2.1.0",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"reselect": "^4.0.0",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"util": "^0.12.4"
|
||||
"util": "^0.12.4",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.3.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@openedx/frontend-build": "14.2.2",
|
||||
"@openedx/frontend-build": "13.1.4",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"copy-webpack-plugin": "^12.0.0",
|
||||
"husky": "^9.0.0",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"fetch-mock": "^9.11.0",
|
||||
"husky": "^7.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jest-when": "^3.6.0",
|
||||
"react-dev-utils": "^12.0.0",
|
||||
"jest-expect-message": "^1.0.2",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"redux-mock-store": "^1.5.4"
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"semantic-release": "^20.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
21
src/App.jsx
21
src/App.jsx
@@ -17,6 +17,8 @@ import {
|
||||
} from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import ZendeskFab from 'components/ZendeskFab';
|
||||
import { ExperimentProvider } from 'ExperimentContext';
|
||||
|
||||
import track from 'tracking';
|
||||
|
||||
@@ -40,6 +42,19 @@ export const App = () => {
|
||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
||||
const loadData = reduxHooks.useLoadData();
|
||||
|
||||
const optimizelyScript = () => {
|
||||
if (getConfig().OPTIMIZELY_URL) {
|
||||
return <script src={getConfig().OPTIMIZELY_URL} />;
|
||||
} if (getConfig().OPTIMIZELY_PROJECT_ID) {
|
||||
return (
|
||||
<script
|
||||
src={`${getConfig().MARKETING_SITE_BASE_URL}/optimizelyjs/${getConfig().OPTIMIZELY_PROJECT_ID}.js`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
|
||||
window.loadEmptyData = () => {
|
||||
@@ -76,6 +91,7 @@ export const App = () => {
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages.pageTitle)}</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
{optimizelyScript()}
|
||||
</Helmet>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
@@ -87,11 +103,14 @@ export const App = () => {
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
) : (
|
||||
<Dashboard />
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
)}
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,6 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
|
||||
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
|
||||
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
|
||||
.text-ellipsis {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
||||
import { ExperimentProvider } from 'ExperimentContext';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -17,6 +18,10 @@ jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
|
||||
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
|
||||
jest.mock('ExperimentContext', () => ({
|
||||
ExperimentProvider: 'ExperimentProvider',
|
||||
}));
|
||||
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: 'redux.selectors',
|
||||
@@ -74,10 +79,11 @@ describe('App router component', () => {
|
||||
it('loads dashboard', () => {
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const dashboard = main.children[0];
|
||||
expect(dashboard.type).toEqual('Dashboard');
|
||||
const expProvider = main.children[0];
|
||||
expect(expProvider.type).toEqual('ExperimentProvider');
|
||||
expect(expProvider.children.length).toEqual(1);
|
||||
expect(
|
||||
dashboard.matches(shallow(<Dashboard />)),
|
||||
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -91,10 +97,11 @@ describe('App router component', () => {
|
||||
it('loads dashboard', () => {
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const dashboard = main.children[0];
|
||||
expect(dashboard.type).toEqual('Dashboard');
|
||||
const expProvider = main.children[0];
|
||||
expect(expProvider.type).toEqual('ExperimentProvider');
|
||||
expect(expProvider.children.length).toEqual(1);
|
||||
expect(
|
||||
dashboard.matches(shallow(<Dashboard />)),
|
||||
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -108,10 +115,11 @@ describe('App router component', () => {
|
||||
it('loads dashboard', () => {
|
||||
const main = el.instance.findByType('main')[0];
|
||||
expect(main.children.length).toEqual(1);
|
||||
const dashboard = main.children[0];
|
||||
expect(dashboard.type).toEqual('Dashboard');
|
||||
const expProvider = main.children[0];
|
||||
expect(expProvider.type).toEqual('ExperimentProvider');
|
||||
expect(expProvider.children.length).toEqual(1);
|
||||
expect(
|
||||
dashboard.matches(shallow(<Dashboard />)),
|
||||
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
64
src/ExperimentContext.jsx
Normal file
64
src/ExperimentContext.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||
import { StrictDict } from 'utils';
|
||||
import api from 'widgets/ProductRecommendations/api';
|
||||
import * as module from './ExperimentContext';
|
||||
|
||||
export const state = StrictDict({
|
||||
experiment: (val) => React.useState(val), // eslint-disable-line
|
||||
countryCode: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useCountryCode = (setCountryCode) => {
|
||||
React.useEffect(() => {
|
||||
api
|
||||
.fetchRecommendationsContext()
|
||||
.then((response) => {
|
||||
setCountryCode(response.data.countryCode);
|
||||
})
|
||||
.catch(() => {
|
||||
setCountryCode('');
|
||||
});
|
||||
/* eslint-disable */
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const ExperimentContext = React.createContext();
|
||||
|
||||
export const ExperimentProvider = ({ children }) => {
|
||||
const [countryCode, setCountryCode] = module.state.countryCode(null);
|
||||
const [experiment, setExperiment] = module.state.experiment({
|
||||
isExperimentActive: false,
|
||||
inRecommendationsVariant: true,
|
||||
});
|
||||
|
||||
module.useCountryCode(setCountryCode);
|
||||
const { width } = useWindowSize();
|
||||
const isMobile = width < breakpoints.small.minWidth;
|
||||
|
||||
const contextValue = React.useMemo(
|
||||
() => ({
|
||||
experiment,
|
||||
countryCode,
|
||||
setExperiment,
|
||||
setCountryCode,
|
||||
isMobile,
|
||||
}),
|
||||
[experiment, countryCode, setExperiment, setCountryCode, isMobile]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExperimentContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ExperimentContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useExperimentContext = () => React.useContext(ExperimentContext);
|
||||
|
||||
ExperimentProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default { useCountryCode, useExperimentContext };
|
||||
122
src/ExperimentContext.test.jsx
Normal file
122
src/ExperimentContext.test.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import { waitFor, render } from '@testing-library/react';
|
||||
import { useWindowSize } from '@openedx/paragon';
|
||||
|
||||
import api from 'widgets/ProductRecommendations/api';
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import * as experiment from 'ExperimentContext';
|
||||
|
||||
const state = new MockUseState(experiment);
|
||||
|
||||
jest.unmock('react');
|
||||
jest.spyOn(React, 'useEffect').mockImplementation((cb, prereqs) => ({ useEffect: { cb, prereqs } }));
|
||||
|
||||
jest.mock('widgets/ProductRecommendations/api', () => ({
|
||||
fetchRecommendationsContext: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('experiments context', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('useCountryCode', () => {
|
||||
describe('behaviour', () => {
|
||||
describe('useEffect call', () => {
|
||||
let calls;
|
||||
let cb;
|
||||
const setCountryCode = jest.fn();
|
||||
const successfulFetch = { data: { countryCode: 'ZA' } };
|
||||
|
||||
beforeEach(() => {
|
||||
experiment.useCountryCode(setCountryCode);
|
||||
|
||||
({ calls } = React.useEffect.mock);
|
||||
[[cb]] = calls;
|
||||
});
|
||||
|
||||
it('calls useEffect once', () => {
|
||||
expect(calls.length).toEqual(1);
|
||||
});
|
||||
describe('successful fetch', () => {
|
||||
it('sets the country code', async () => {
|
||||
let resolveFn;
|
||||
api.fetchRecommendationsContext.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveFn = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
cb();
|
||||
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
|
||||
expect(setCountryCode).not.toHaveBeenCalled();
|
||||
resolveFn(successfulFetch);
|
||||
await waitFor(() => {
|
||||
expect(setCountryCode).toHaveBeenCalledWith(successfulFetch.data.countryCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('unsuccessful fetch', () => {
|
||||
it('sets the country code to an empty string', async () => {
|
||||
let rejectFn;
|
||||
api.fetchRecommendationsContext.mockReturnValueOnce(
|
||||
new Promise((resolve, reject) => {
|
||||
rejectFn = reject;
|
||||
}),
|
||||
);
|
||||
cb();
|
||||
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
|
||||
expect(setCountryCode).not.toHaveBeenCalled();
|
||||
rejectFn();
|
||||
await waitFor(() => {
|
||||
expect(setCountryCode).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ExperimentProvider', () => {
|
||||
const { ExperimentProvider } = experiment;
|
||||
|
||||
const TestComponent = () => {
|
||||
const {
|
||||
experiment: exp,
|
||||
setExperiment,
|
||||
countryCode,
|
||||
setCountryCode,
|
||||
isMobile,
|
||||
} = experiment.useExperimentContext();
|
||||
|
||||
expect(exp.isExperimentActive).toBeFalsy();
|
||||
expect(exp.inRecommendationsVariant).toBeTruthy();
|
||||
expect(countryCode).toBeNull();
|
||||
expect(isMobile).toBe(false);
|
||||
expect(setExperiment).toBeDefined();
|
||||
expect(setCountryCode).toBeDefined();
|
||||
|
||||
return (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
|
||||
it('allows access to child components with the context stateful values', () => {
|
||||
const countryCodeSpy = jest.spyOn(experiment, 'useCountryCode').mockImplementationOnce(() => {});
|
||||
useWindowSize.mockImplementationOnce(() => ({ width: 577, height: 943 }));
|
||||
|
||||
state.mock();
|
||||
|
||||
render(
|
||||
<ExperimentProvider>
|
||||
<TestComponent />
|
||||
</ExperimentProvider>,
|
||||
);
|
||||
|
||||
expect(countryCodeSpy).toHaveBeenCalledWith(state.setState.countryCode);
|
||||
state.expectInitializedWith(state.keys.countryCode, null);
|
||||
state.expectInitializedWith(state.keys.experiment, { isExperimentActive: false, inRecommendationsVariant: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -50,10 +51,13 @@ exports[`App router component component no network failure snapshot 1`] = `
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Dashboard />
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -71,15 +75,21 @@ exports[`App router component component no network failure with optimizely proje
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<script
|
||||
src="undefined/optimizelyjs/fakeId.js"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Dashboard />
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -97,15 +107,21 @@ exports[`App router component component no network failure with optimizely url s
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<script
|
||||
src="fake.url"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Dashboard />
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -138,6 +154,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPag
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<AppProvider
|
||||
store={
|
||||
{
|
||||
Object {
|
||||
"redux": "store",
|
||||
}
|
||||
}
|
||||
|
||||
65
src/components/ZendeskFab/__snapshots__/index.test.jsx.snap
Normal file
65
src/components/ZendeskFab/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,65 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ZendeskFab snapshot 1`] = `
|
||||
<Zendesk
|
||||
cookies={true}
|
||||
defer={true}
|
||||
webWidget={
|
||||
Object {
|
||||
"answerBot": Object {
|
||||
"avatar": Object {
|
||||
"name": Object {
|
||||
"*": "edX Support",
|
||||
},
|
||||
"url": "https://edx-cdn.org/v3/prod/favicon.ico",
|
||||
},
|
||||
"contactOnlyAfterQuery": true,
|
||||
"suppress": false,
|
||||
"title": Object {
|
||||
"*": "edX Support",
|
||||
},
|
||||
},
|
||||
"chat": Object {
|
||||
"departments": Object {
|
||||
"enabled": Array [
|
||||
"account settings",
|
||||
"billing and payments",
|
||||
"certificates",
|
||||
"deadlines",
|
||||
"errors and technical issues",
|
||||
"other",
|
||||
"proctoring",
|
||||
],
|
||||
},
|
||||
"suppress": false,
|
||||
},
|
||||
"contactForm": Object {
|
||||
"attachments": true,
|
||||
"selectTicketForm": Object {
|
||||
"*": "Please choose your request type:",
|
||||
},
|
||||
"ticketForms": Array [
|
||||
Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"id": "description",
|
||||
"prefill": Object {
|
||||
"*": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": 360003368814,
|
||||
"subject": false,
|
||||
},
|
||||
],
|
||||
},
|
||||
"contactOptions": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"helpCenter": Object {
|
||||
"originalArticleButton": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
56
src/components/ZendeskFab/index.jsx
Normal file
56
src/components/ZendeskFab/index.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Zendesk from 'react-zendesk';
|
||||
import messages from './messages';
|
||||
|
||||
const ZendeskFab = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const setting = {
|
||||
cookies: true,
|
||||
webWidget: {
|
||||
contactOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
chat: {
|
||||
suppress: false,
|
||||
departments: {
|
||||
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
|
||||
},
|
||||
},
|
||||
contactForm: {
|
||||
ticketForms: [
|
||||
{
|
||||
id: 360003368814,
|
||||
subject: false,
|
||||
fields: [{ id: 'description', prefill: { '*': '' } }],
|
||||
},
|
||||
],
|
||||
selectTicketForm: {
|
||||
'*': formatMessage(messages.selectTicketForm),
|
||||
},
|
||||
attachments: true,
|
||||
},
|
||||
helpCenter: {
|
||||
originalArticleButton: true,
|
||||
},
|
||||
answerBot: {
|
||||
suppress: false,
|
||||
contactOnlyAfterQuery: true,
|
||||
title: { '*': formatMessage(messages.supportTitle) },
|
||||
avatar: {
|
||||
url: 'https://edx-cdn.org/v3/prod/favicon.ico',
|
||||
name: { '*': formatMessage(messages.supportTitle) },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ZendeskFab;
|
||||
12
src/components/ZendeskFab/index.test.jsx
Normal file
12
src/components/ZendeskFab/index.test.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import ZendeskFab from '.';
|
||||
|
||||
jest.mock('react-zendesk', () => 'Zendesk');
|
||||
|
||||
describe('ZendeskFab', () => {
|
||||
test('snapshot', () => {
|
||||
const wrapper = shallow(<ZendeskFab />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
16
src/components/ZendeskFab/messages.js
Normal file
16
src/components/ZendeskFab/messages.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
supportTitle: {
|
||||
id: 'zendesk.supportTitle',
|
||||
description: 'Title for the support button',
|
||||
defaultMessage: 'edX Support',
|
||||
},
|
||||
selectTicketForm: {
|
||||
id: 'zendesk.selectTicketForm',
|
||||
description: 'Select ticket form',
|
||||
defaultMessage: 'Please choose your request type:',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -12,13 +12,12 @@ const configuration = {
|
||||
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
|
||||
ZENDESK_KEY: process.env.ZENDESK_KEY,
|
||||
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
||||
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
||||
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
|
||||
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
|
||||
ENABLE_PROGRAMS: process.env.ENABLE_PROGRAMS === 'true',
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
@@ -27,7 +27,7 @@ reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
);
|
||||
|
||||
describe('BeginCourseButton', () => {
|
||||
|
||||
@@ -26,7 +26,7 @@ reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
|
||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
);
|
||||
|
||||
let wrapper;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Locked } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const UpgradeButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { disableUpgradeCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.upgradeClicked,
|
||||
cardId,
|
||||
upgradeUrl,
|
||||
);
|
||||
|
||||
const enabledProps = {
|
||||
as: 'a',
|
||||
href: upgradeUrl,
|
||||
onClick: trackUpgradeClick,
|
||||
};
|
||||
return (
|
||||
<ActionButton
|
||||
iconBefore={Locked}
|
||||
variant="outline-primary"
|
||||
disabled={disableUpgradeCourse}
|
||||
{...!disableUpgradeCourse && enabledProps}
|
||||
>
|
||||
{formatMessage(messages.upgrade)}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
UpgradeButton.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default UpgradeButton;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
upgradeClicked: jest.fn().mockName('segment.trackUpgradeClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableUpgradeCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
describe('UpgradeButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
const upgradeUrl = 'upgradeUrl';
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ upgradeUrl });
|
||||
describe('snapshot', () => {
|
||||
test('can upgrade', () => {
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.instance.props.disabled).toEqual(false);
|
||||
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.upgradeClicked,
|
||||
props.cardId,
|
||||
upgradeUrl,
|
||||
));
|
||||
});
|
||||
test('cannot upgrade', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.instance.props.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -6,11 +6,11 @@ exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
{
|
||||
"trackCourseEvent": {
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"url": "home-urlexec-ed-tracking-path=cardId",
|
||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,11 @@ exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
|
||||
disabled={false}
|
||||
href="#"
|
||||
onClick={
|
||||
{
|
||||
"trackCourseEvent": {
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"url": "home-urlexec-ed-tracking-path=cardId",
|
||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ exports[`ResumeButton snapshot disabled snapshot 1`] = `
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
{
|
||||
"trackCourseEvent": {
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"url": "resume-urlexec-ed-tracking-path=cardId",
|
||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,11 @@ exports[`ResumeButton snapshot enabled snapshot 1`] = `
|
||||
disabled={false}
|
||||
href="#"
|
||||
onClick={
|
||||
{
|
||||
"trackCourseEvent": {
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"url": "resume-urlexec-ed-tracking-path=cardId",
|
||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UpgradeButton snapshot can upgrade 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="upgradeUrl"
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.trackUpgradeClicked],
|
||||
"upgradeUrl": "upgradeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`UpgradeButton snapshot cannot upgrade 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</ActionButton>
|
||||
`;
|
||||
@@ -6,11 +6,11 @@ exports[`ViewCourseButton learner can view course 1`] = `
|
||||
disabled={false}
|
||||
href="#"
|
||||
onClick={
|
||||
{
|
||||
"trackCourseEvent": {
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"url": "homeUrl",
|
||||
"upgradeUrl": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,11 @@ exports[`ViewCourseButton learner cannot view course 1`] = `
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
{
|
||||
"trackCourseEvent": {
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"url": "homeUrl",
|
||||
"upgradeUrl": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ActionRow } from '@openedx/paragon';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
@@ -14,13 +14,15 @@ import ViewCourseButton from './ViewCourseButton';
|
||||
export const CourseCardActions = ({ cardId }) => {
|
||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const {
|
||||
isVerified,
|
||||
hasStarted,
|
||||
isExecEd2UCourse,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
||||
|
||||
return (
|
||||
<ActionRow data-test-id="CourseCardActions">
|
||||
<CourseCardActionSlot cardId={cardId} />
|
||||
{!(isEntitlement || isVerified || isExecEd2UCourse) && <UpgradeButton cardId={cardId} />}
|
||||
{isEntitlement && (isFulfilled
|
||||
? <ViewCourseButton cardId={cardId} />
|
||||
: <SelectSessionButton cardId={cardId} />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
@@ -19,7 +19,7 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton');
|
||||
jest.mock('./UpgradeButton', () => 'UpgradeButton');
|
||||
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
|
||||
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
|
||||
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
|
||||
@@ -57,7 +57,19 @@ describe('CourseCardActions', () => {
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('Exec Ed course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isExecEd2UCourse: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe('entitlement course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isEntitlement: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
});
|
||||
it('renders ViewCourseButton if fulfilled', () => {
|
||||
mockHooks({ isEntitlement: true, isFulfilled: true });
|
||||
render();
|
||||
@@ -69,26 +81,33 @@ describe('CourseCardActions', () => {
|
||||
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
describe('not entitlement, verified, or exec ed', () => {
|
||||
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
|
||||
describe('verified course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isVerified: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe('not entielement, verified, or exec ed', () => {
|
||||
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
|
||||
mockHooks({ isArchived: true });
|
||||
render();
|
||||
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
describe('unstarted courses', () => {
|
||||
it('renders CourseCardActionSlot and BeginCourseButton', () => {
|
||||
it('renders UpgradeButton and BeginCourseButton', () => {
|
||||
mockHooks();
|
||||
render();
|
||||
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
describe('active courses (started, and not archived)', () => {
|
||||
it('renders CourseCardActionSlot and ResumeButton', () => {
|
||||
it('renders UpgradeButton and ResumeButton', () => {
|
||||
mockHooks({ hasStarted: true });
|
||||
render();
|
||||
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(ResumeButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
upgrade: {
|
||||
id: 'learner-dash.courseCard.actions.upgrade',
|
||||
description: 'Course card upgrade button text',
|
||||
defaultMessage: 'Upgrade',
|
||||
},
|
||||
beginCourse: {
|
||||
id: 'learner-dash.courseCard.actions.beginCourse',
|
||||
description: 'Course card begin-course button text',
|
||||
|
||||
@@ -10,14 +10,14 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
{
|
||||
Object {
|
||||
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
|
||||
"description": "",
|
||||
"id": "learner-dash.courseCard.banners.credit.error",
|
||||
}
|
||||
}
|
||||
values={
|
||||
{
|
||||
Object {
|
||||
"supportEmailLink": <MailtoLink
|
||||
to="test-support-email"
|
||||
>
|
||||
|
||||
@@ -27,8 +27,8 @@ exports[`CreditContent component render with action snapshot 1`] = `
|
||||
</ActionRow>
|
||||
<CreditRequestForm
|
||||
requestData={
|
||||
{
|
||||
"parameters": {
|
||||
Object {
|
||||
"parameters": Object {
|
||||
"key1": "val1",
|
||||
},
|
||||
"url": "test-request-data-url",
|
||||
@@ -48,8 +48,8 @@ exports[`CreditContent component render without action snapshot 1`] = `
|
||||
</div>
|
||||
<CreditRequestForm
|
||||
requestData={
|
||||
{
|
||||
"parameters": {
|
||||
Object {
|
||||
"parameters": Object {
|
||||
"key1": "val1",
|
||||
},
|
||||
"url": "test-request-data-url",
|
||||
|
||||
@@ -13,12 +13,12 @@ exports[`RelatedProgramsBanner render with programs 1`] = `
|
||||
</span>
|
||||
<ProgramsList
|
||||
programs={
|
||||
[
|
||||
{
|
||||
Array [
|
||||
Object {
|
||||
"title": "Program 1",
|
||||
"url": "http://example.com/program1",
|
||||
},
|
||||
{
|
||||
Object {
|
||||
"title": "Program 2",
|
||||
"url": "http://example.com/program2",
|
||||
},
|
||||
|
||||
@@ -43,14 +43,14 @@ exports[`CertificateBanner snapshot is restricted and verified with billing emai
|
||||
|
||||
<format-message-function
|
||||
message={
|
||||
{
|
||||
Object {
|
||||
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
|
||||
"description": "Message to learners to contact billing for certificate refunds",
|
||||
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
|
||||
}
|
||||
}
|
||||
values={
|
||||
{
|
||||
Object {
|
||||
"billingEmail": <MailtoLink
|
||||
to="billing@email"
|
||||
>
|
||||
@@ -68,14 +68,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support and
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
{
|
||||
Object {
|
||||
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
|
||||
"description": "Restricted certificate warning message",
|
||||
"id": "learner-dash.courseCard.banners.certificateRestricted",
|
||||
}
|
||||
}
|
||||
values={
|
||||
{
|
||||
Object {
|
||||
"supportEmail": <MailtoLink
|
||||
to="suport@email"
|
||||
>
|
||||
@@ -87,14 +87,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support and
|
||||
|
||||
<format-message-function
|
||||
message={
|
||||
{
|
||||
Object {
|
||||
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
|
||||
"description": "Message to learners to contact billing for certificate refunds",
|
||||
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
|
||||
}
|
||||
}
|
||||
values={
|
||||
{
|
||||
Object {
|
||||
"billingEmail": <MailtoLink
|
||||
to="billing@email"
|
||||
>
|
||||
@@ -112,14 +112,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support emai
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
{
|
||||
Object {
|
||||
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
|
||||
"description": "Restricted certificate warning message",
|
||||
"id": "learner-dash.courseCard.banners.certificateRestricted",
|
||||
}
|
||||
}
|
||||
values={
|
||||
{
|
||||
Object {
|
||||
"supportEmail": <MailtoLink
|
||||
to="suport@email"
|
||||
>
|
||||
@@ -147,14 +147,14 @@ exports[`CertificateBanner snapshot is restricted with support email 1`] = `
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
{
|
||||
Object {
|
||||
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
|
||||
"description": "Restricted certificate warning message",
|
||||
"id": "learner-dash.courseCard.banners.certificateRestricted",
|
||||
}
|
||||
}
|
||||
values={
|
||||
{
|
||||
Object {
|
||||
"supportEmail": <MailtoLink
|
||||
to="suport@email"
|
||||
>
|
||||
|
||||
@@ -4,14 +4,14 @@ exports[`EntitlementBanner snapshot: expiration warning 1`] = `
|
||||
<Banner>
|
||||
<format-message-function
|
||||
message={
|
||||
{
|
||||
Object {
|
||||
"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={
|
||||
{
|
||||
Object {
|
||||
"changeDeadline": "11/11/2022",
|
||||
"selectSessionButton": <Button
|
||||
className="m-0 p-0"
|
||||
@@ -33,14 +33,14 @@ exports[`EntitlementBanner snapshot: no sessions available 1`] = `
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
{
|
||||
Object {
|
||||
"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={
|
||||
{
|
||||
Object {
|
||||
"emailLink": <MailtoLink
|
||||
to="test-support-email"
|
||||
>
|
||||
|
||||
@@ -8,7 +8,7 @@ exports[`CourseCardBanners render with isEnrolled false 1`] = `
|
||||
<RelatedProgramsBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CourseBannerSlot
|
||||
<CourseBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<EntitlementBanner
|
||||
@@ -25,7 +25,7 @@ exports[`CourseCardBanners renders default CourseCardBanners 1`] = `
|
||||
<RelatedProgramsBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CourseBannerSlot
|
||||
<CourseBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<EntitlementBanner
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
|
||||
import CourseBanner from './CourseBanner';
|
||||
import CertificateBanner from './CertificateBanner';
|
||||
import CreditBanner from './CreditBanner';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
@@ -14,7 +14,7 @@ export const CourseCardBanners = ({ cardId }) => {
|
||||
return (
|
||||
<div className="course-card-banners" data-testid="CourseCardBanners">
|
||||
<RelatedProgramsBanner cardId={cardId} />
|
||||
<CourseBannerSlot cardId={cardId} />
|
||||
<CourseBanner cardId={cardId} />
|
||||
<EntitlementBanner cardId={cardId} />
|
||||
{isEnrolled && <CertificateBanner cardId={cardId} />}
|
||||
{isEnrolled && <CreditBanner cardId={cardId} />}
|
||||
|
||||
@@ -20,13 +20,11 @@ export const CourseCardImage = ({ cardId, orientation }) => {
|
||||
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { disableCourseTitle } = useActionDisabledState(cardId);
|
||||
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
|
||||
const wrapperClassName = `pgn__card-wrapper-image-cap d-inline-block overflow-visible ${orientation}`;
|
||||
const wrapperClassName = `pgn__card-wrapper-image-cap overflow-visible ${orientation}`;
|
||||
const image = (
|
||||
<>
|
||||
<img
|
||||
// w-100 is necessary for images on Safari, otherwise stretches full height of the image
|
||||
// https://stackoverflow.com/a/44250830
|
||||
className="pgn__card-image-cap w-100 show"
|
||||
className="pgn__card-image-cap show"
|
||||
src={bannerImgSrc}
|
||||
alt={formatMessage(messages.bannerAlt)}
|
||||
/>
|
||||
|
||||
@@ -18,8 +18,8 @@ jest.mock('hooks', () => ({
|
||||
useCardCourseData: jest.fn(() => ({ bannerImgSrc: 'banner-img-src' })),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||
useCardEnrollmentData: jest.fn(() => ({ isVerified: true })),
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
|
||||
trackCourseEvent: { eventName, cardId, url },
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
|
||||
trackCourseEvent: { eventName, cardId, upgradeUrl },
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -17,7 +17,7 @@ exports[`CourseCardMenu render show dropdown hide unenroll item and disable emai
|
||||
<SocialShareMenu
|
||||
cardId="test-card-id"
|
||||
emailSettings={
|
||||
{
|
||||
Object {
|
||||
"hide": [MockFunction emailSettingHide],
|
||||
"isVisible": false,
|
||||
"show": [MockFunction emailSettingShow],
|
||||
@@ -58,7 +58,7 @@ exports[`CourseCardMenu render show dropdown show unenroll and enable email snap
|
||||
<SocialShareMenu
|
||||
cardId="test-card-id"
|
||||
emailSettings={
|
||||
{
|
||||
Object {
|
||||
"hide": [MockFunction emailSettingHide],
|
||||
"isVisible": false,
|
||||
"show": [MockFunction emailSettingShow],
|
||||
|
||||
@@ -17,8 +17,8 @@ jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(() => ({ courseName: 'course-name' })),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
|
||||
trackCourseEvent: { eventName, cardId, url },
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
|
||||
trackCourseEvent: { eventName, cardId, upgradeUrl },
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
||||
<a
|
||||
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
|
||||
className="pgn__card-wrapper-image-cap overflow-visible orientation"
|
||||
href="home-url"
|
||||
onClick={
|
||||
{
|
||||
"trackCourseEvent": {
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.courseImageClicked],
|
||||
"url": "home-url",
|
||||
"upgradeUrl": "home-url",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
||||
<Fragment>
|
||||
<img
|
||||
alt="Course thumbnail"
|
||||
className="pgn__card-image-cap w-100 show"
|
||||
className="pgn__card-image-cap show"
|
||||
src="banner-img-src"
|
||||
/>
|
||||
<span
|
||||
@@ -43,12 +43,12 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
||||
|
||||
exports[`CourseCardImage snapshot renders disabled link 1`] = `
|
||||
<div
|
||||
className="pgn__card-wrapper-image-cap d-inline-block overflow-visible orientation"
|
||||
className="pgn__card-wrapper-image-cap overflow-visible orientation"
|
||||
>
|
||||
<Fragment>
|
||||
<img
|
||||
alt="Course thumbnail"
|
||||
className="pgn__card-image-cap w-100 show"
|
||||
className="pgn__card-image-cap show"
|
||||
src="banner-img-src"
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -7,11 +7,11 @@ exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
|
||||
data-testid="CourseCardTitle"
|
||||
href="home-url"
|
||||
onClick={
|
||||
{
|
||||
"trackCourseEvent": {
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.courseTitleClicked],
|
||||
"url": "home-url",
|
||||
"upgradeUrl": "home-url",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,19 @@ import { reduxHooks } from 'hooks';
|
||||
export const useActionDisabledState = (cardId) => {
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const {
|
||||
hasAccess, isAudit, isAuditAccessExpired,
|
||||
canUpgrade, hasAccess, isAudit, isAuditAccessExpired,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const {
|
||||
isEntitlement, isFulfilled, canChange, hasSessions,
|
||||
} = reduxHooks.useCardEntitlementData(cardId);
|
||||
|
||||
const { resumeUrl, homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { resumeUrl, homeUrl, upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
|
||||
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
|
||||
const disableSelectSession = !isEntitlement || isMasquerading || !hasAccess || (!canChange || !hasSessions);
|
||||
const disableUpgradeCourse = !upgradeUrl || (isMasquerading && !canUpgrade);
|
||||
|
||||
const disableCourseTitle = (isEntitlement && !isFulfilled) || disableViewCourse;
|
||||
|
||||
@@ -22,6 +23,7 @@ export const useActionDisabledState = (cardId) => {
|
||||
disableBeginCourse,
|
||||
disableResumeCourse,
|
||||
disableViewCourse,
|
||||
disableUpgradeCourse,
|
||||
disableSelectSession,
|
||||
disableCourseTitle,
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ const cardId = 'my-test-course-number';
|
||||
describe('useActionDisabledState', () => {
|
||||
const defaultData = {
|
||||
isMasquerading: false,
|
||||
canUpgrade: false,
|
||||
isEntitlement: false,
|
||||
isFulfilled: false,
|
||||
canChange: false,
|
||||
@@ -25,10 +26,12 @@ describe('useActionDisabledState', () => {
|
||||
isAuditAccessExpired: false,
|
||||
resumeUrl: 'resume.url',
|
||||
homeUrl: 'home.url',
|
||||
upgradeUrl: 'upgrade.url',
|
||||
};
|
||||
const mockHooksData = (args) => {
|
||||
const {
|
||||
isMasquerading,
|
||||
canUpgrade,
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
@@ -38,9 +41,11 @@ describe('useActionDisabledState', () => {
|
||||
isAuditAccessExpired,
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
upgradeUrl,
|
||||
} = { ...defaultData, ...args };
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
canUpgrade,
|
||||
hasAccess,
|
||||
isAudit,
|
||||
isAuditAccessExpired,
|
||||
@@ -54,6 +59,7 @@ describe('useActionDisabledState', () => {
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
upgradeUrl,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -115,6 +121,21 @@ describe('useActionDisabledState', () => {
|
||||
testDisabled({ hasAccess: true }, false);
|
||||
});
|
||||
});
|
||||
describe('disableUpgradeCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableUpgradeCourse).toBe(expected);
|
||||
};
|
||||
it('disable when upgradeUrl is invalid', () => {
|
||||
testDisabled({ upgradeUrl: null }, true);
|
||||
});
|
||||
it('disable when isMasquerading is true and canUpgrade is false', () => {
|
||||
testDisabled({ isMasquerading: true, canUpgrade: false }, true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ canUpgrade: true }, false);
|
||||
});
|
||||
});
|
||||
describe('disableSelectSession', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
|
||||
@@ -28,7 +28,7 @@ exports[`CourseFilterControls is not mobile snapshot 1`] = `
|
||||
>
|
||||
<FilterForm
|
||||
filters={
|
||||
[
|
||||
Array [
|
||||
"test-filter",
|
||||
]
|
||||
}
|
||||
@@ -84,7 +84,7 @@ exports[`CourseFilterControls mobile snapshot 1`] = `
|
||||
>
|
||||
<FilterForm
|
||||
filters={
|
||||
[
|
||||
Array [
|
||||
"test-filter",
|
||||
]
|
||||
}
|
||||
@@ -144,7 +144,7 @@ exports[`CourseFilterControls no courses snapshot 1`] = `
|
||||
>
|
||||
<FilterForm
|
||||
filters={
|
||||
[
|
||||
Array [
|
||||
"test-filter",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`FilterForm snapshot renders 1`] = `
|
||||
name="course-status-filters"
|
||||
onChange={[MockFunction handleFilterChange]}
|
||||
value={
|
||||
[
|
||||
Array [
|
||||
"test-filter",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@ import CourseCard from 'containers/CourseCard';
|
||||
|
||||
import { useIsCollapsed } from './hooks';
|
||||
|
||||
export const CourseList = ({ courseListData }) => {
|
||||
const {
|
||||
filterOptions, setPageNumber, numPages, showFilters, visibleList,
|
||||
} = courseListData;
|
||||
export const CourseList = ({
|
||||
filterOptions, setPageNumber, numPages, showFilters, visibleList,
|
||||
}) => {
|
||||
const isCollapsed = useIsCollapsed();
|
||||
return (
|
||||
<>
|
||||
@@ -39,16 +38,14 @@ export const CourseList = ({ courseListData }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const courseListDataShape = PropTypes.shape({
|
||||
CourseList.propTypes = {
|
||||
showFilters: PropTypes.bool.isRequired,
|
||||
visibleList: PropTypes.arrayOf(PropTypes.shape()).isRequired,
|
||||
filterOptions: PropTypes.shape().isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
visibleList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
filterOptions: PropTypes.object.isRequired,
|
||||
numPages: PropTypes.number.isRequired,
|
||||
setPageNumber: PropTypes.func.isRequired,
|
||||
});
|
||||
|
||||
CourseList.propTypes = {
|
||||
courseListData: courseListDataShape,
|
||||
};
|
||||
|
||||
export default CourseList;
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('CourseList', () => {
|
||||
useIsCollapsed.mockReturnValue(false);
|
||||
|
||||
const createWrapper = (courseListData = defaultCourseListData) => (
|
||||
shallow(<CourseList courseListData={courseListData} />)
|
||||
shallow(<CourseList {...courseListData} />)
|
||||
);
|
||||
|
||||
describe('no courses or filters', () => {
|
||||
|
||||
@@ -18,7 +18,11 @@ exports[`CoursesPanel no courses snapshot 1`] = `
|
||||
<CourseFilterControls />
|
||||
</div>
|
||||
</div>
|
||||
<NoCoursesViewSlot />
|
||||
<PluginSlot
|
||||
id="no_courses_view"
|
||||
>
|
||||
<NoCoursesView />
|
||||
</PluginSlot>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -40,16 +44,16 @@ exports[`CoursesPanel with courses snapshot 1`] = `
|
||||
<CourseFilterControls />
|
||||
</div>
|
||||
</div>
|
||||
<CourseListSlot
|
||||
courseListData={
|
||||
{
|
||||
"filterOptions": {},
|
||||
"numPages": 1,
|
||||
"setPageNumber": [MockFunction setPageNumber],
|
||||
"showFilters": false,
|
||||
"visibleList": [],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<PluginSlot
|
||||
id="course_list"
|
||||
>
|
||||
<CourseList
|
||||
filterOptions={Object {}}
|
||||
numPages={1}
|
||||
setPageNumber={[MockFunction setPageNumber]}
|
||||
showFilters={false}
|
||||
visibleList={Array []}
|
||||
/>
|
||||
</PluginSlot>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import {
|
||||
CourseFilterControls,
|
||||
} from 'containers/CourseFilterControls';
|
||||
import CourseListSlot from 'plugin-slots/CourseListSlot';
|
||||
import NoCoursesViewSlot from 'plugin-slots/NoCoursesViewSlot';
|
||||
import NoCoursesView from './NoCoursesView';
|
||||
|
||||
import CourseList from './CourseList';
|
||||
|
||||
import { useCourseListData } from './hooks';
|
||||
|
||||
@@ -32,7 +34,19 @@ export const CoursesPanel = () => {
|
||||
<CourseFilterControls {...courseListData.filterOptions} />
|
||||
</div>
|
||||
</div>
|
||||
{hasCourses ? <CourseListSlot courseListData={courseListData} /> : <NoCoursesViewSlot />}
|
||||
{hasCourses ? (
|
||||
<PluginSlot
|
||||
id="course_list"
|
||||
>
|
||||
<CourseList {...courseListData} />
|
||||
</PluginSlot>
|
||||
) : (
|
||||
<PluginSlot
|
||||
id="no_courses_view"
|
||||
>
|
||||
<NoCoursesView />
|
||||
</PluginSlot>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { Container, Col, Row } from '@openedx/paragon';
|
||||
|
||||
import WidgetSidebarSlot from 'plugin-slots/WidgetSidebarSlot';
|
||||
|
||||
import WidgetFooter from 'containers/WidgetContainers/WidgetFooter';
|
||||
import hooks from './hooks';
|
||||
|
||||
export const columnConfig = {
|
||||
@@ -24,10 +23,11 @@ export const columnConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export const DashboardLayout = ({ children }) => {
|
||||
export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
|
||||
const {
|
||||
isCollapsed,
|
||||
sidebarShowing,
|
||||
setSidebarShowing,
|
||||
} = hooks.useDashboardLayoutData();
|
||||
|
||||
const courseListColumnProps = sidebarShowing
|
||||
@@ -42,7 +42,12 @@ export const DashboardLayout = ({ children }) => {
|
||||
</Col>
|
||||
<Col {...columnConfig.sidebar} className="sidebar-column">
|
||||
{!isCollapsed && (<h2 className="course-list-title"> </h2>)}
|
||||
<WidgetSidebarSlot />
|
||||
<Sidebar setSidebarShowing={setSidebarShowing} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -50,6 +55,7 @@ export const DashboardLayout = ({ children }) => {
|
||||
};
|
||||
DashboardLayout.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
sidebar: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DashboardLayout;
|
||||
|
||||
@@ -16,13 +16,17 @@ const hookProps = {
|
||||
};
|
||||
hooks.useDashboardLayoutData.mockReturnValue(hookProps);
|
||||
|
||||
const props = {
|
||||
sidebar: jest.fn(() => 'test-sidebar-content'),
|
||||
};
|
||||
|
||||
const children = 'test-children';
|
||||
|
||||
let el;
|
||||
describe('DashboardLayout', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<DashboardLayout>{children}</DashboardLayout>);
|
||||
el = shallow(<DashboardLayout {...props}>{children}</DashboardLayout>);
|
||||
});
|
||||
|
||||
const testColumns = () => {
|
||||
@@ -36,13 +40,17 @@ describe('DashboardLayout', () => {
|
||||
const columns = el.instance.findByType(Row)[0].findByType(Col);
|
||||
expect(columns[0].children).not.toHaveLength(0);
|
||||
});
|
||||
it('displays WidgetSidebarSlot in second column', () => {
|
||||
it('displays sidebar prop in second column', () => {
|
||||
const columns = el.instance.findByType(Row)[0].findByType(Col);
|
||||
expect(columns[1].findByType('WidgetSidebarSlot')).toHaveLength(1);
|
||||
expect(columns[1].findByType(props.sidebar)).toHaveLength(1);
|
||||
});
|
||||
it('displays a footer in the second row', () => {
|
||||
const columns = el.instance.findByType(Row)[1].findByType(Col);
|
||||
expect(columns[0].children[0].type).toEqual('WidgetFooter');
|
||||
});
|
||||
};
|
||||
const testSidebarLayout = () => {
|
||||
it('displays withSidebar width for course list column', () => {
|
||||
it('displays widthSidebar width for course list column', () => {
|
||||
const columns = el.instance.findByType(Row)[0].findByType(Col);
|
||||
Object.keys(columnConfig.courseList.withSidebar).forEach(size => {
|
||||
expect(columns[0].props[size]).toEqual(columnConfig.courseList.withSidebar[size]);
|
||||
|
||||
@@ -9,13 +9,13 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
|
||||
<Col
|
||||
className="course-list-column"
|
||||
lg={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
@@ -26,19 +26,26 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
lg={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<WidgetSidebarSlot />
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -53,13 +60,13 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
|
||||
<Col
|
||||
className="course-list-column"
|
||||
lg={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 8,
|
||||
}
|
||||
@@ -70,19 +77,26 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
lg={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<WidgetSidebarSlot />
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -97,13 +111,13 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
|
||||
<Col
|
||||
className="course-list-column"
|
||||
lg={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
@@ -114,13 +128,13 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
lg={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 4,
|
||||
}
|
||||
@@ -131,7 +145,14 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
|
||||
>
|
||||
|
||||
</h2>
|
||||
<WidgetSidebarSlot />
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -146,13 +167,13 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
||||
<Col
|
||||
className="course-list-column"
|
||||
lg={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 8,
|
||||
}
|
||||
@@ -163,13 +184,13 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
lg={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
{
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 4,
|
||||
}
|
||||
@@ -180,7 +201,14 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
||||
>
|
||||
|
||||
</h2>
|
||||
<WidgetSidebarSlot />
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -17,7 +17,9 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
|
||||
data-testid="dashboard-content"
|
||||
id="dashboard-content"
|
||||
>
|
||||
<DashboardLayout>
|
||||
<DashboardLayout
|
||||
sidebar="LoadedWidgetSidebar"
|
||||
>
|
||||
<CoursesPanel />
|
||||
</DashboardLayout>
|
||||
</div>
|
||||
@@ -60,7 +62,9 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
|
||||
data-testid="dashboard-content"
|
||||
id="dashboard-content"
|
||||
>
|
||||
<DashboardLayout>
|
||||
<DashboardLayout
|
||||
sidebar="NoCoursesWidgetSidebar"
|
||||
>
|
||||
<CoursesPanel />
|
||||
</DashboardLayout>
|
||||
</div>
|
||||
|
||||
@@ -26,8 +26,7 @@ export const useDashboardMessages = () => {
|
||||
|
||||
export const useDashboardLayoutData = () => {
|
||||
const { width } = useWindowSize();
|
||||
|
||||
const [sidebarShowing, setSidebarShowing] = module.state.sidebarShowing(true);
|
||||
const [sidebarShowing, setSidebarShowing] = module.state.sidebarShowing(false);
|
||||
return {
|
||||
isDashboardCollapsed: width < breakpoints.large.maxWidth,
|
||||
sidebarShowing,
|
||||
|
||||
@@ -40,9 +40,9 @@ describe('CourseCard hooks', () => {
|
||||
describe('useDashboardLayoutData', () => {
|
||||
beforeEach(() => { state.mock(); });
|
||||
describe('behavior', () => {
|
||||
it('initializes sidebarShowing to default true value', () => {
|
||||
it('initializes sidebarShowing to default false value', () => {
|
||||
hooks.useDashboardLayoutData();
|
||||
state.expectInitializedWith(state.keys.sidebarShowing, true);
|
||||
state.expectInitializedWith(state.keys.sidebarShowing, false);
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
|
||||
@@ -6,6 +6,9 @@ import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
|
||||
import SelectSessionModal from 'containers/SelectSessionModal';
|
||||
import CoursesPanel from 'containers/CoursesPanel';
|
||||
|
||||
import LoadedSidebar from 'containers/WidgetContainers/LoadedSidebar';
|
||||
import NoCoursesSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
|
||||
|
||||
import LoadingView from './LoadingView';
|
||||
import DashboardLayout from './DashboardLayout';
|
||||
import hooks from './hooks';
|
||||
@@ -32,7 +35,7 @@ export const Dashboard = () => {
|
||||
{initIsPending
|
||||
? (<LoadingView />)
|
||||
: (
|
||||
<DashboardLayout>
|
||||
<DashboardLayout sidebar={hasCourses ? LoadedSidebar : NoCoursesSidebar}>
|
||||
<CoursesPanel />
|
||||
</DashboardLayout>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,9 @@ import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
|
||||
import SelectSessionModal from 'containers/SelectSessionModal';
|
||||
import CoursesPanel from 'containers/CoursesPanel';
|
||||
|
||||
import LoadedWidgetSidebar from 'containers/WidgetContainers/LoadedSidebar';
|
||||
import NoCoursesWidgetSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
|
||||
|
||||
import DashboardLayout from './DashboardLayout';
|
||||
import LoadingView from './LoadingView';
|
||||
import hooks from './hooks';
|
||||
@@ -22,6 +25,8 @@ jest.mock('hooks', () => ({
|
||||
|
||||
jest.mock('containers/EnterpriseDashboardModal', () => 'EnterpriseDashboardModal');
|
||||
jest.mock('containers/CoursesPanel', () => 'CoursesPanel');
|
||||
jest.mock('containers/WidgetContainers/LoadedSidebar', () => 'LoadedWidgetSidebar');
|
||||
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => 'NoCoursesWidgetSidebar');
|
||||
jest.mock('./LoadingView', () => 'LoadingView');
|
||||
jest.mock('./DashboardLayout', () => 'DashboardLayout');
|
||||
|
||||
@@ -111,7 +116,7 @@ describe('Dashboard', () => {
|
||||
showSelectSessionModal: true,
|
||||
},
|
||||
content: ['LoadedView', (
|
||||
<DashboardLayout><CoursesPanel /></DashboardLayout>
|
||||
<DashboardLayout sidebar={LoadedWidgetSidebar}><CoursesPanel /></DashboardLayout>
|
||||
)],
|
||||
showEnterpriseModal: false,
|
||||
showSelectSessionModal: true,
|
||||
@@ -127,7 +132,7 @@ describe('Dashboard', () => {
|
||||
showSelectSessionModal: false,
|
||||
},
|
||||
content: ['Dashboard layout with no courses sidebar and content', (
|
||||
<DashboardLayout><CoursesPanel /></DashboardLayout>
|
||||
<DashboardLayout sidebar={NoCoursesWidgetSidebar}><CoursesPanel /></DashboardLayout>
|
||||
)],
|
||||
showEnterpriseModal: true,
|
||||
showSelectSessionModal: false,
|
||||
|
||||
@@ -10,7 +10,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: false 1`] =
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: true 1`] = `
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ exports[`EmailSettingsModal render snapshot: emails enabled, show: true 1`] = `
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`EnterpriseDashboard snapshot 1`] = `
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
@@ -12,7 +10,7 @@ export const BrandLogo = () => {
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
|
||||
return (
|
||||
<a href={dashboard?.url || '/'} className="mx-auto">
|
||||
<a href={dashboard?.url || getConfig().LMS_BASE_URL} className="mx-auto">
|
||||
<img
|
||||
className="logo py-3"
|
||||
src={getConfig().LOGO_URL}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import BrandLogo from './BrandLogo';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
@@ -23,6 +24,6 @@ describe('BrandLogo', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
|
||||
const wrapper = shallow(<BrandLogo />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.instance.findByType('a')[0].props.href).toEqual('/');
|
||||
expect(wrapper.instance.findByType('a')[0].props.href).toEqual(getConfig().LMS_BASE_URL);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { Button, Badge } from '@openedx/paragon';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
import { reduxHooks, apiHooks } from 'hooks';
|
||||
|
||||
import { findCoursesNavDropdownClicked } from '../hooks';
|
||||
import messages from '../messages';
|
||||
|
||||
export const CollapseMenuBody = ({ isOpen }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
|
||||
const { enabled: programsEnabled } = apiHooks.useProgramsConfig();
|
||||
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
|
||||
const exploreCoursesClick = findCoursesNavDropdownClicked(
|
||||
urls.baseAppUrl(courseSearchUrl),
|
||||
);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column shadow-sm nav-small-menu">
|
||||
<Button as="a" href={`${getConfig().LMS_BASE_URL}/dashboard/`} variant="inverse-primary">
|
||||
{formatMessage(messages.course)}
|
||||
</Button>
|
||||
{programsEnabled && (
|
||||
<Button as="a" href={urls.programsUrl()} variant="inverse-primary">
|
||||
{formatMessage(messages.program)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
as="a"
|
||||
href={urls.baseAppUrl(courseSearchUrl)}
|
||||
variant="inverse-primary"
|
||||
onClick={exploreCoursesClick}
|
||||
>
|
||||
{formatMessage(messages.discoverNew)}
|
||||
</Button>
|
||||
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
|
||||
{formatMessage(messages.help)}
|
||||
</Button>
|
||||
{authenticatedUser && (
|
||||
<>
|
||||
{!!dashboard && (
|
||||
<Button as="a" href={dashboard.url} variant="inverse-primary">
|
||||
{formatMessage(messages.dashboard)}
|
||||
</Button>
|
||||
)}
|
||||
{!dashboard && getConfig().CAREER_LINK_URL && (
|
||||
<Button href={`${getConfig().CAREER_LINK_URL}`}>
|
||||
{formatMessage(messages.career)}
|
||||
<Badge className="px-2 mx-2" variant="warning">
|
||||
{formatMessage(messages.newAlert)}
|
||||
</Badge>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
as="a"
|
||||
href={`${getConfig().LMS_BASE_URL}/u/${authenticatedUser.username}`}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
{formatMessage(messages.profile)}
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={`${getConfig().LMS_BASE_URL}/account/settings`}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
{formatMessage(messages.account)}
|
||||
</Button>
|
||||
{getConfig().ORDER_HISTORY_URL && (
|
||||
<Button
|
||||
as="a"
|
||||
variant="inverse-primary"
|
||||
href={getConfig().ORDER_HISTORY_URL}
|
||||
>
|
||||
{formatMessage(messages.orderHistory)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
as="a"
|
||||
href={getConfig().LOGOUT_URL}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
{formatMessage(messages.signOut)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CollapseMenuBody.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default CollapseMenuBody;
|
||||
@@ -0,0 +1,60 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { apiHooks } from 'hooks';
|
||||
import CollapseMenuBody from './CollapseMenuBody';
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppContext: {
|
||||
authenticatedUser: {
|
||||
username: 'username',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: () => ({
|
||||
url: 'url',
|
||||
}),
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: '/courseSearchUrl',
|
||||
}),
|
||||
},
|
||||
apiHooks: {
|
||||
useProgramsConfig: () => ({
|
||||
enabled: true,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
findCoursesNavDropdownClicked: (url) => jest.fn().mockName(`findCoursesNavDropdownClicked("${url}")`),
|
||||
}));
|
||||
|
||||
describe('CollapseMenuBody', () => {
|
||||
test('render', () => {
|
||||
const wrapper = shallow(<CollapseMenuBody isOpen />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('render empty if not open', () => {
|
||||
const wrapper = shallow(<CollapseMenuBody isOpen={false} />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
test('render unauthenticated', () => {
|
||||
const { authenticatedUser } = AppContext;
|
||||
AppContext.authenticatedUser = null;
|
||||
const wrapper = shallow(<CollapseMenuBody isOpen />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
AppContext.authenticatedUser = authenticatedUser;
|
||||
});
|
||||
|
||||
test('render with disabled programs', () => {
|
||||
apiHooks.useProgramsConfig = () => ({ enabled: false });
|
||||
const wrapper = shallow(<CollapseMenuBody isOpen />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CollapseMenuBody render 1`] = `
|
||||
<div
|
||||
className="d-flex flex-column shadow-sm nav-small-menu"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Courses
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/dashboard/programs"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Programs
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/support"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
<Fragment>
|
||||
<Button
|
||||
as="a"
|
||||
href="url"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Dashboard
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/u/username"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/account/settings"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Account
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/logout"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</Fragment>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CollapseMenuBody render empty if not open 1`] = `null`;
|
||||
|
||||
exports[`CollapseMenuBody render unauthenticated 1`] = `
|
||||
<div
|
||||
className="d-flex flex-column shadow-sm nav-small-menu"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Courses
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/dashboard/programs"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Programs
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/support"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CollapseMenuBody render with disabled programs 1`] = `
|
||||
<div
|
||||
className="d-flex flex-column shadow-sm nav-small-menu"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Courses
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/support"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
<Fragment>
|
||||
<Button
|
||||
as="a"
|
||||
href="url"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Dashboard
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/u/username"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/account/settings"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Account
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/logout"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</Fragment>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,48 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CollapsedHeader render nothing if not collapsed 1`] = `false`;
|
||||
|
||||
exports[`CollapsedHeader renders 1`] = `
|
||||
<Fragment>
|
||||
<header
|
||||
className="d-flex shadow-sm align-items-center learner-variant-header"
|
||||
>
|
||||
<IconButton
|
||||
alt="Close"
|
||||
className="p-4"
|
||||
iconAs="Icon"
|
||||
invertColors={true}
|
||||
isActive={true}
|
||||
onClick={[MockFunction toggleIsOpen]}
|
||||
variant="primary"
|
||||
/>
|
||||
<mockConstructor />
|
||||
</header>
|
||||
<mockConstructor
|
||||
isOpen={false}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CollapsedHeader renders with isOpen true 1`] = `
|
||||
<Fragment>
|
||||
<header
|
||||
className="d-flex shadow-sm align-items-center learner-variant-header"
|
||||
>
|
||||
<IconButton
|
||||
alt="Menu"
|
||||
className="p-4"
|
||||
iconAs="Icon"
|
||||
invertColors={true}
|
||||
isActive={true}
|
||||
onClick={[MockFunction toggleIsOpen]}
|
||||
src={[MockFunction icons.Close]}
|
||||
variant="primary"
|
||||
/>
|
||||
<mockConstructor />
|
||||
</header>
|
||||
<mockConstructor
|
||||
isOpen={true}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { MenuIcon, Close } from '@openedx/paragon/icons';
|
||||
import { IconButton, Icon } from '@openedx/paragon';
|
||||
|
||||
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
|
||||
|
||||
import CollapseMenuBody from './CollapseMenuBody';
|
||||
import BrandLogo from '../BrandLogo';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
export const CollapsedHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderData();
|
||||
|
||||
return (
|
||||
isCollapsed && (
|
||||
<>
|
||||
<header className="d-flex shadow-sm align-items-center learner-variant-header">
|
||||
<IconButton
|
||||
invertColors
|
||||
isActive
|
||||
src={isOpen ? Close : MenuIcon}
|
||||
iconAs={Icon}
|
||||
alt={
|
||||
isOpen
|
||||
? formatMessage(messages.collapseMenuOpenAltText)
|
||||
: formatMessage(messages.collapseMenuClosedAltText)
|
||||
}
|
||||
onClick={toggleIsOpen}
|
||||
variant="primary"
|
||||
className="p-4"
|
||||
/>
|
||||
<BrandLogo />
|
||||
</header>
|
||||
<CollapseMenuBody isOpen={isOpen} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
CollapsedHeader.propTypes = {};
|
||||
|
||||
export default CollapsedHeader;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import CollapsedHeader from '.';
|
||||
|
||||
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
|
||||
|
||||
jest.mock('../BrandLogo', () => jest.fn(() => 'BrandLogo'));
|
||||
jest.mock('./CollapseMenuBody', () => jest.fn(() => 'CollapseMenuBody'));
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useIsCollapsed: jest.fn(() => true),
|
||||
useLearnerDashboardHeaderData: jest.fn(() => ({
|
||||
isOpen: false,
|
||||
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('CollapsedHeader', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<CollapsedHeader />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render nothing if not collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
const wrapper = shallow(<CollapsedHeader />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders with isOpen true', () => {
|
||||
useLearnerDashboardHeaderData.mockReturnValueOnce({
|
||||
isOpen: true,
|
||||
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
|
||||
});
|
||||
const wrapper = shallow(<CollapsedHeader />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -9,14 +9,14 @@ exports[`ConfirmEmailBanner snapshot Show on unverified 1`] = `
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
{
|
||||
Object {
|
||||
"defaultMessage": "Remember to confirm your email so that you can keep learning! {confirmNowButton}.",
|
||||
"description": "Text for reminding user to confirm email",
|
||||
"id": "leanerDashboard.confirmEmailTextReminderBanner",
|
||||
}
|
||||
}
|
||||
values={
|
||||
{
|
||||
Object {
|
||||
"confirmNowButton": <Button
|
||||
className="confirm-email-now-button"
|
||||
onClick={[MockFunction openConfirmModalButtonClick]}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { AvatarButton, Dropdown, Badge } from '@openedx/paragon';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
export const AuthenticatedUserDropdown = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
|
||||
return (
|
||||
authenticatedUser && (
|
||||
<Dropdown className="user-dropdown pr4">
|
||||
<Dropdown.Toggle
|
||||
as={AvatarButton}
|
||||
src={authenticatedUser.profileImage}
|
||||
id="user"
|
||||
variant="light"
|
||||
className="p-4"
|
||||
>
|
||||
<span data-hj-suppress className="d-md-inline">
|
||||
{authenticatedUser.username}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
{ getConfig().ENABLE_EDX_PERSONAL_DASHBOARD && (
|
||||
<>
|
||||
<Dropdown.Header>{formatMessage(messages.dashboardSwitch)}</Dropdown.Header>
|
||||
<Dropdown.Item as="a" href="/edx-dashboard" className="active">
|
||||
{formatMessage(messages.dashboardPersonal)}
|
||||
</Dropdown.Item>
|
||||
{!!dashboard && (
|
||||
<Dropdown.Item as="a" href={dashboard.url} key={dashboard.label}>
|
||||
{dashboard.label} {formatMessage(messages.dashboard)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!dashboard && getConfig().CAREER_LINK_URL && (
|
||||
<Dropdown.Item href={`${getConfig().CAREER_LINK_URL}`}>
|
||||
{formatMessage(messages.career)}
|
||||
<Badge className="px-2 mx-2" variant="warning">
|
||||
{formatMessage(messages.newAlert)}
|
||||
</Badge>
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`}>
|
||||
{formatMessage(messages.profile)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href={getConfig().ACCOUNT_SETTINGS_URL}>
|
||||
{formatMessage(messages.account)}
|
||||
</Dropdown.Item>
|
||||
{getConfig().ORDER_HISTORY_URL && (
|
||||
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
||||
{formatMessage(messages.orderHistory)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
||||
{formatMessage(messages.signOut)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
AuthenticatedUserDropdown.propTypes = {};
|
||||
|
||||
export default AuthenticatedUserDropdown;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { AuthenticatedUserDropdown } from './AuthenticatedUserDropdown';
|
||||
import { useIsCollapsed } from '../hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppContext: {
|
||||
authenticatedUser: {
|
||||
profileImage: 'profileImage',
|
||||
username: 'username',
|
||||
},
|
||||
},
|
||||
}));
|
||||
const COURSE_SEARCH_URL = 'test-course-search-url';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: COURSE_SEARCH_URL,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => ({
|
||||
useIsCollapsed: jest.fn(),
|
||||
findCoursesNavDropdownClicked: (href) => jest.fn().mockName(`findCoursesNavDropdownClicked('${href}')`),
|
||||
}));
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
baseAppUrl: (url) => (url),
|
||||
programsUrl: 'http://localhost:18000/dashboard/programs',
|
||||
}));
|
||||
|
||||
const config = {
|
||||
ACCOUNT_PROFILE_URL: 'http://account-profile-url.test',
|
||||
ACCOUNT_SETTINGS_URL: 'http://account-settings-url.test',
|
||||
LOGOUT_URL: 'http://logout-url.test',
|
||||
ORDER_HISTORY_URL: 'http://order-history-url.test',
|
||||
SUPPORT_URL: 'http://localhost:18000/support',
|
||||
CAREER_LINK_URL: 'http://localhost:18000/career',
|
||||
LMS_BASE_URL: 'http:/localhost:18000',
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD: true,
|
||||
};
|
||||
getConfig.mockReturnValue(config);
|
||||
|
||||
describe('AuthenticatedUserDropdown', () => {
|
||||
const defaultDashboardData = {
|
||||
label: 'label',
|
||||
url: 'url',
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
test('no auth render empty', () => {
|
||||
const { authenticatedUser } = AppContext;
|
||||
AppContext.authenticatedUser = null;
|
||||
const wrapper = shallow(<AuthenticatedUserDropdown />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
AppContext.authenticatedUser = authenticatedUser;
|
||||
});
|
||||
test('with enterprise dashboard', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(defaultDashboardData);
|
||||
useIsCollapsed.mockReturnValueOnce(true);
|
||||
const wrapper = shallow(<AuthenticatedUserDropdown />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('without enterprise dashboard and expanded', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
const wrapper = shallow(<AuthenticatedUserDropdown />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AuthenticatedUserDropdown snapshots no auth render empty 1`] = `null`;
|
||||
|
||||
exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
|
||||
<Dropdown
|
||||
className="user-dropdown pr4"
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
className="p-4"
|
||||
id="user"
|
||||
src="profileImage"
|
||||
variant="light"
|
||||
>
|
||||
<span
|
||||
className="d-md-inline"
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
username
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu
|
||||
className="dropdown-menu-right"
|
||||
>
|
||||
<Fragment>
|
||||
<Dropdown.Header>
|
||||
SWITCH DASHBOARD
|
||||
</Dropdown.Header>
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
className="active"
|
||||
href="/edx-dashboard"
|
||||
>
|
||||
Personal
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
href="url"
|
||||
key="label"
|
||||
>
|
||||
label
|
||||
|
||||
Dashboard
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
</Fragment>
|
||||
<Dropdown.Item
|
||||
href="http://account-profile-url.test/u/username"
|
||||
>
|
||||
Profile
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://account-settings-url.test"
|
||||
>
|
||||
Account
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://order-history-url.test"
|
||||
>
|
||||
Order History
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://logout-url.test"
|
||||
>
|
||||
Sign Out
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
`;
|
||||
|
||||
exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and expanded 1`] = `
|
||||
<Dropdown
|
||||
className="user-dropdown pr4"
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
className="p-4"
|
||||
id="user"
|
||||
src="profileImage"
|
||||
variant="light"
|
||||
>
|
||||
<span
|
||||
className="d-md-inline"
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
username
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu
|
||||
className="dropdown-menu-right"
|
||||
>
|
||||
<Fragment>
|
||||
<Dropdown.Header>
|
||||
SWITCH DASHBOARD
|
||||
</Dropdown.Header>
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
className="active"
|
||||
href="/edx-dashboard"
|
||||
>
|
||||
Personal
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
</Fragment>
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/career"
|
||||
>
|
||||
Career
|
||||
<Badge
|
||||
className="px-2 mx-2"
|
||||
variant="warning"
|
||||
>
|
||||
New
|
||||
</Badge>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://account-profile-url.test/u/username"
|
||||
>
|
||||
Profile
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://account-settings-url.test"
|
||||
>
|
||||
Account
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://order-history-url.test"
|
||||
>
|
||||
Order History
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://logout-url.test"
|
||||
>
|
||||
Sign Out
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
`;
|
||||
@@ -0,0 +1,93 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ExpandedHeader render 1`] = `
|
||||
<header
|
||||
className="d-flex shadow-sm align-items-center learner-variant-header pl-4"
|
||||
>
|
||||
<div
|
||||
className="flex-grow-1 d-flex align-items-center"
|
||||
>
|
||||
<BrandLogo />
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4 course-link"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Courses
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4"
|
||||
href="programsUrl"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Programs
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4"
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<span
|
||||
className="flex-grow-1"
|
||||
/>
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4"
|
||||
href="http://localhost:18000/support"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
<AuthenticatedUserDropdown />
|
||||
</header>
|
||||
`;
|
||||
|
||||
exports[`ExpandedHeader render empty if collapsed 1`] = `null`;
|
||||
|
||||
exports[`ExpandedHeader render with disabled programs 1`] = `
|
||||
<header
|
||||
className="d-flex shadow-sm align-items-center learner-variant-header pl-4"
|
||||
>
|
||||
<div
|
||||
className="flex-grow-1 d-flex align-items-center"
|
||||
>
|
||||
<BrandLogo />
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4 course-link"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Courses
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4"
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<span
|
||||
className="flex-grow-1"
|
||||
/>
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4"
|
||||
href="http://localhost:18000/support"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
<AuthenticatedUserDropdown />
|
||||
</header>
|
||||
`;
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
import { reduxHooks, apiHooks } from 'hooks';
|
||||
|
||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
|
||||
import messages from '../messages';
|
||||
import BrandLogo from '../BrandLogo';
|
||||
|
||||
export const ExpandedHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
|
||||
const { enabled: programsEnabled } = apiHooks.useProgramsConfig();
|
||||
|
||||
const exploreCoursesClick = findCoursesNavClicked(
|
||||
urls.baseAppUrl(courseSearchUrl),
|
||||
);
|
||||
|
||||
if (isCollapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="d-flex shadow-sm align-items-center learner-variant-header pl-4">
|
||||
<div className="flex-grow-1 d-flex align-items-center">
|
||||
<BrandLogo />
|
||||
|
||||
<Button
|
||||
as="a"
|
||||
href={`${getConfig().LMS_BASE_URL}/dashboard/`}
|
||||
variant="inverse-primary"
|
||||
className="p-4 course-link"
|
||||
>
|
||||
{formatMessage(messages.course)}
|
||||
</Button>
|
||||
{programsEnabled && (
|
||||
<Button
|
||||
as="a"
|
||||
href={urls.programsUrl()}
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
>
|
||||
{formatMessage(messages.program)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
as="a"
|
||||
href={urls.baseAppUrl(courseSearchUrl)}
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
onClick={exploreCoursesClick}
|
||||
>
|
||||
{formatMessage(messages.discoverNew)}
|
||||
</Button>
|
||||
<span className="flex-grow-1" />
|
||||
<Button
|
||||
as="a"
|
||||
href={getConfig().SUPPORT_URL}
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
>
|
||||
{formatMessage(messages.help)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AuthenticatedUserDropdown />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
ExpandedHeader.propTypes = {};
|
||||
|
||||
export default ExpandedHeader;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { apiHooks } from 'hooks';
|
||||
import ExpandedHeader from '.';
|
||||
|
||||
import { useIsCollapsed } from '../hooks';
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
programsUrl: () => 'programsUrl',
|
||||
baseAppUrl: url => (`http://localhost:18000${url}`),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: '/courseSearchUrl',
|
||||
}),
|
||||
},
|
||||
apiHooks: {
|
||||
useProgramsConfig: () => ({
|
||||
enabled: true,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useIsCollapsed: jest.fn(),
|
||||
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
|
||||
}));
|
||||
|
||||
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
|
||||
jest.mock('../BrandLogo', () => 'BrandLogo');
|
||||
|
||||
describe('ExpandedHeader', () => {
|
||||
test('render', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
const wrapper = shallow(<ExpandedHeader />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('render empty if collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(true);
|
||||
const wrapper = shallow(<ExpandedHeader />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
test('render with disabled programs', () => {
|
||||
apiHooks.useProgramsConfig = () => ({ enabled: false });
|
||||
const wrapper = shallow(<ExpandedHeader />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const getLearnerHeaderMenu = (
|
||||
formatMessage,
|
||||
courseSearchUrl,
|
||||
authenticatedUser,
|
||||
exploreCoursesClick,
|
||||
) => ({
|
||||
mainMenu: [
|
||||
{
|
||||
type: 'item',
|
||||
href: '/',
|
||||
content: formatMessage(messages.course),
|
||||
isActive: true,
|
||||
},
|
||||
...(getConfig().ENABLE_PROGRAMS ? [{
|
||||
type: 'item',
|
||||
href: `${urls.programsUrl()}`,
|
||||
content: formatMessage(messages.program),
|
||||
}] : []),
|
||||
{
|
||||
type: 'item',
|
||||
href: `${urls.baseAppUrl(courseSearchUrl)}`,
|
||||
content: formatMessage(messages.discoverNew),
|
||||
onClick: (e) => {
|
||||
exploreCoursesClick(e);
|
||||
},
|
||||
},
|
||||
],
|
||||
secondaryMenu: [
|
||||
...(getConfig().SUPPORT_URL ? [{
|
||||
type: 'item',
|
||||
href: `${getConfig().SUPPORT_URL}`,
|
||||
content: formatMessage(messages.help),
|
||||
}] : []),
|
||||
],
|
||||
userMenu: [
|
||||
{
|
||||
heading: '',
|
||||
items: [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser?.username}`,
|
||||
content: formatMessage(messages.profile),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
href: `${getConfig().ACCOUNT_SETTINGS_URL}`,
|
||||
content: formatMessage(messages.account),
|
||||
},
|
||||
...(getConfig().ORDER_HISTORY_URL ? [{
|
||||
type: 'item',
|
||||
href: getConfig().ORDER_HISTORY_URL,
|
||||
content: formatMessage(messages.orderHistory),
|
||||
}] : []),
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '',
|
||||
items: [
|
||||
{
|
||||
type: 'item',
|
||||
href: `${getConfig().LOGOUT_URL}`,
|
||||
content: formatMessage(messages.signOut),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
export default getLearnerHeaderMenu;
|
||||
@@ -16,7 +16,7 @@ exports[`BrandLogo dashboard defined 1`] = `
|
||||
exports[`BrandLogo dashboard undefined 1`] = `
|
||||
<a
|
||||
className="mx-auto"
|
||||
href="/"
|
||||
href="http://localhost:18000"
|
||||
>
|
||||
<img
|
||||
alt="edX, Inc. Dashboard"
|
||||
|
||||
@@ -3,59 +3,8 @@
|
||||
exports[`LearnerDashboardHeader render 1`] = `
|
||||
<Fragment>
|
||||
<ConfirmEmailBanner />
|
||||
<Header
|
||||
mainMenuItems={
|
||||
[
|
||||
{
|
||||
"content": "Courses",
|
||||
"href": "/",
|
||||
"isActive": true,
|
||||
"type": "item",
|
||||
},
|
||||
{
|
||||
"content": "Discover New",
|
||||
"href": "http://localhost:18000/course-search-url",
|
||||
"onClick": [Function],
|
||||
"type": "item",
|
||||
},
|
||||
]
|
||||
}
|
||||
secondaryMenuItems={[]}
|
||||
userMenuItems={
|
||||
[
|
||||
{
|
||||
"heading": "",
|
||||
"items": [
|
||||
{
|
||||
"content": "Profile",
|
||||
"href": "http://account-profile-url.test/u/undefined",
|
||||
"type": "item",
|
||||
},
|
||||
{
|
||||
"content": "Account",
|
||||
"href": "http://account-settings-url.test",
|
||||
"type": "item",
|
||||
},
|
||||
{
|
||||
"content": "Order History",
|
||||
"href": "test-url",
|
||||
"type": "item",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"heading": "",
|
||||
"items": [
|
||||
{
|
||||
"content": "Sign Out",
|
||||
"href": "http://localhost:18000/logout",
|
||||
"type": "item",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<CollapsedHeader />
|
||||
<ExpandedHeader />
|
||||
<MasqueradeBar />
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import track from 'tracking';
|
||||
import { StrictDict } from 'utils';
|
||||
import { linkNames } from 'tracking/constants';
|
||||
|
||||
import getLearnerHeaderMenu from './LearnerDashboardMenu';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
@@ -27,13 +24,6 @@ export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCou
|
||||
linkName: linkNames.learnerHomeNavDropdownExplore,
|
||||
});
|
||||
|
||||
export const useLearnerDashboardHeaderMenu = ({
|
||||
courseSearchUrl, authenticatedUser, exploreCoursesClick,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick);
|
||||
};
|
||||
|
||||
export const useLearnerDashboardHeaderData = () => {
|
||||
const [isOpen, setIsOpen] = module.state.isOpen(false);
|
||||
const toggleIsOpen = () => setIsOpen(!isOpen);
|
||||
@@ -49,5 +39,4 @@ export default {
|
||||
findCoursesNavClicked,
|
||||
findCoursesNavDropdownClicked,
|
||||
useLearnerDashboardHeaderData,
|
||||
useLearnerDashboardHeaderMenu,
|
||||
};
|
||||
|
||||
@@ -13,7 +13,6 @@ const {
|
||||
findCoursesNavClicked,
|
||||
findCoursesNavDropdownClicked,
|
||||
useLearnerDashboardHeaderData,
|
||||
useLearnerDashboardHeaderMenu,
|
||||
} = hooks;
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
@@ -49,17 +48,6 @@ describe('LearnerDashboardHeader hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLearnerDashboardHeaderMenu', () => {
|
||||
test('calls header menu data hook', () => {
|
||||
const courseSearchUrl = '/courses';
|
||||
const authenticatedUser = {
|
||||
username: 'test',
|
||||
};
|
||||
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({ courseSearchUrl, authenticatedUser });
|
||||
expect(learnerHomeHeaderMenu.mainMenu.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findCoursesNavDropdownClicked', () => {
|
||||
test('calls tracking with dropdown link name', () => {
|
||||
findCoursesNavDropdownClicked(url);
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import MasqueradeBar from 'containers/MasqueradeBar';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import urls from 'data/services/lms/urls';
|
||||
|
||||
import ConfirmEmailBanner from './ConfirmEmailBanner';
|
||||
|
||||
import { useLearnerDashboardHeaderMenu, findCoursesNavClicked } from './hooks';
|
||||
import CollapsedHeader from './CollapsedHeader';
|
||||
import ExpandedHeader from './ExpandedHeader';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const LearnerDashboardHeader = () => {
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
|
||||
const exploreCoursesClick = () => {
|
||||
findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
|
||||
};
|
||||
|
||||
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({
|
||||
courseSearchUrl,
|
||||
authenticatedUser,
|
||||
exploreCoursesClick,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmEmailBanner />
|
||||
<Header
|
||||
mainMenuItems={learnerHomeHeaderMenu.mainMenu}
|
||||
secondaryMenuItems={learnerHomeHeaderMenu.secondaryMenu}
|
||||
userMenuItems={learnerHomeHeaderMenu.userMenu}
|
||||
/>
|
||||
<MasqueradeBar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const LearnerDashboardHeader = () => (
|
||||
<>
|
||||
<ConfirmEmailBanner />
|
||||
<CollapsedHeader />
|
||||
<ExpandedHeader />
|
||||
<MasqueradeBar />
|
||||
</>
|
||||
);
|
||||
|
||||
LearnerDashboardHeader.propTypes = {};
|
||||
|
||||
|
||||
@@ -1,47 +1,18 @@
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
import LearnerDashboardHeader from '.';
|
||||
import { findCoursesNavClicked } from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: '/course-search-url',
|
||||
})),
|
||||
},
|
||||
}));
|
||||
jest.mock('./hooks', () => ({
|
||||
...jest.requireActual('./hooks'),
|
||||
findCoursesNavClicked: jest.fn(),
|
||||
}));
|
||||
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');
|
||||
jest.mock('./CollapsedHeader', () => 'CollapsedHeader');
|
||||
jest.mock('./ConfirmEmailBanner', () => 'ConfirmEmailBanner');
|
||||
jest.mock('@edx/frontend-component-header', () => 'Header');
|
||||
jest.mock('./ExpandedHeader', () => 'ExpandedHeader');
|
||||
|
||||
describe('LearnerDashboardHeader', () => {
|
||||
test('render', () => {
|
||||
mergeConfig({ ORDER_HISTORY_URL: 'test-url' });
|
||||
const wrapper = shallow(<LearnerDashboardHeader />);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
expect(wrapper.instance.findByType('ConfirmEmailBanner')).toHaveLength(1);
|
||||
expect(wrapper.instance.findByType('MasqueradeBar')).toHaveLength(1);
|
||||
expect(wrapper.instance.findByType(Header)).toHaveLength(1);
|
||||
wrapper.instance.findByType(Header)[0].props.mainMenuItems[1].onClick();
|
||||
expect(findCoursesNavClicked).toHaveBeenCalledWith(urls.baseAppUrl('/course-search-url'));
|
||||
expect(wrapper.instance.findByType(Header)[0].props.secondaryMenuItems.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should display Help link if SUPPORT_URL is set', () => {
|
||||
mergeConfig({ SUPPORT_URL: 'http://localhost:18000/support' });
|
||||
const wrapper = shallow(<LearnerDashboardHeader />);
|
||||
expect(wrapper.instance.findByType(Header)[0].props.secondaryMenuItems.length).toBe(1);
|
||||
});
|
||||
test('should display Programs link if it is enabled by configuration', () => {
|
||||
mergeConfig({ ENABLE_PROGRAMS: true });
|
||||
const wrapper = shallow(<LearnerDashboardHeader />);
|
||||
expect(wrapper.instance.findByType(Header)[0].props.mainMenuItems.length).toBe(3);
|
||||
expect(wrapper.instance.findByType('CollapsedHeader')).toHaveLength(1);
|
||||
expect(wrapper.instance.findByType('ExpandedHeader')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ exports[`MasqueradeBar snapshot can masquerade 1`] = `
|
||||
className="mr-3"
|
||||
disabled={true}
|
||||
labels={
|
||||
{
|
||||
Object {
|
||||
"default": "Submit",
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ exports[`MasqueradeBar snapshot can masquerade with input 1`] = `
|
||||
className="mr-3"
|
||||
disabled={false}
|
||||
labels={
|
||||
{
|
||||
Object {
|
||||
"default": "Submit",
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ exports[`MasqueradeBar snapshot is masquerading failed with error 1`] = `
|
||||
className="mr-3"
|
||||
disabled={true}
|
||||
labels={
|
||||
{
|
||||
Object {
|
||||
"default": "Submit",
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ exports[`MasqueradeBar snapshot is masquerading pending 1`] = `
|
||||
className="mr-3"
|
||||
disabled={true}
|
||||
labels={
|
||||
{
|
||||
Object {
|
||||
"default": "Submit",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
{
|
||||
"programData": {
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program1",
|
||||
},
|
||||
"programUrl": "program-1-url",
|
||||
@@ -53,8 +53,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
{
|
||||
"programData": {
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program2",
|
||||
},
|
||||
"programUrl": "program-2-url",
|
||||
@@ -69,8 +69,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
{
|
||||
"programData": {
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program3",
|
||||
},
|
||||
"programUrl": "program-3-url",
|
||||
@@ -121,8 +121,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
{
|
||||
"programData": {
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program1",
|
||||
},
|
||||
"programUrl": "program-1-url",
|
||||
@@ -137,8 +137,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
{
|
||||
"programData": {
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program2",
|
||||
},
|
||||
"programUrl": "program-2-url",
|
||||
@@ -153,8 +153,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
{
|
||||
"programData": {
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program3",
|
||||
},
|
||||
"programUrl": "program-3-url",
|
||||
|
||||
@@ -7,7 +7,7 @@ exports[`RelatedProgramsModal ProgramCard snapshot 1`] = `
|
||||
href="props.data.programUrl"
|
||||
isClickable={true}
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"color": "white",
|
||||
"width": "18rem",
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.confirm 1`] = `
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason g
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason s
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
@@ -83,14 +83,14 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.reason, should be
|
||||
<div
|
||||
className="bg-white p-3 rounded"
|
||||
style={
|
||||
{
|
||||
Object {
|
||||
"textAlign": "start",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ReasonPane
|
||||
reason={
|
||||
{
|
||||
Object {
|
||||
"isSkipped": false,
|
||||
"reasonProps": "other",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WidgetSidebar snapshots default 1`] = `
|
||||
<div
|
||||
className="widget-sidebar"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<PluginSlot
|
||||
id="widget_sidebar_plugin_slot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
29
src/containers/WidgetContainers/LoadedSidebar/index.jsx
Normal file
29
src/containers/WidgetContainers/LoadedSidebar/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||
|
||||
export const WidgetSidebar = ({ setSidebarShowing }) => {
|
||||
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
|
||||
|
||||
if (!inRecommendationsVariant && isExperimentActive) {
|
||||
setSidebarShowing(true);
|
||||
|
||||
return (
|
||||
<div className="widget-sidebar">
|
||||
<div className="d-flex flex-column">
|
||||
<PluginSlot id="widget_sidebar_plugin_slot" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
WidgetSidebar.propTypes = {
|
||||
setSidebarShowing: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default WidgetSidebar;
|
||||
52
src/containers/WidgetContainers/LoadedSidebar/index.test.jsx
Normal file
52
src/containers/WidgetContainers/LoadedSidebar/index.test.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
|
||||
import WidgetSidebar from '.';
|
||||
|
||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||
jest.mock('widgets/ProductRecommendations/hooks', () => ({
|
||||
useShowRecommendationsFooter: jest.fn(),
|
||||
}));
|
||||
jest.mock('@openedx/frontend-plugin-framework', () => ({
|
||||
PluginSlot: 'PluginSlot',
|
||||
}));
|
||||
|
||||
describe('WidgetSidebar', () => {
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
const props = {
|
||||
setSidebarShowing: jest.fn(),
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
test('default', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.activeControl,
|
||||
);
|
||||
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||
|
||||
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
|
||||
expect(wrapper.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('is hidden when the has the default values', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.default,
|
||||
);
|
||||
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||
|
||||
expect(props.setSidebarShowing).not.toHaveBeenCalled();
|
||||
expect(wrapper.shallowWrapper).toBeNull();
|
||||
});
|
||||
|
||||
test('is hidden when the has the treatment values', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.activeTreatment,
|
||||
);
|
||||
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||
|
||||
expect(props.setSidebarShowing).not.toHaveBeenCalled();
|
||||
expect(wrapper.shallowWrapper).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WidgetSidebar snapshots default 1`] = `
|
||||
<div
|
||||
className="widget-sidebar px-2"
|
||||
>
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<PluginSlot
|
||||
id="widget_sidebar_plugin_slot"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
29
src/containers/WidgetContainers/NoCoursesSidebar/index.jsx
Normal file
29
src/containers/WidgetContainers/NoCoursesSidebar/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||
|
||||
export const WidgetSidebar = ({ setSidebarShowing }) => {
|
||||
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
|
||||
|
||||
if (!inRecommendationsVariant && isExperimentActive) {
|
||||
setSidebarShowing(true);
|
||||
|
||||
return (
|
||||
<div className="widget-sidebar px-2">
|
||||
<div className="d-flex">
|
||||
<PluginSlot id="widget_sidebar_plugin_slot" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
WidgetSidebar.propTypes = {
|
||||
setSidebarShowing: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default WidgetSidebar;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user