Compare commits
28 Commits
open-relea
...
manwar/VAN
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae284d881a | ||
|
|
9ad10108ec | ||
|
|
338498543a | ||
|
|
788476193c | ||
|
|
cb658776c6 | ||
|
|
a4eff6991f | ||
|
|
ba9bd466a3 | ||
|
|
e44f5dde44 | ||
|
|
d46ce000bb | ||
|
|
bf3b37caa4 | ||
|
|
295048b4e9 | ||
|
|
1c70458590 | ||
|
|
247e9f3668 | ||
|
|
5e96dbf614 | ||
|
|
44197f673d | ||
|
|
11b62cce1d | ||
|
|
78d521cd95 | ||
|
|
10cac378b1 | ||
|
|
9a92e39b6c | ||
|
|
39bff6e276 | ||
|
|
3be81e02ea | ||
|
|
ffecce993e | ||
|
|
ae1702d182 | ||
|
|
67789481fb | ||
|
|
543cd623e1 | ||
|
|
ba31b713e2 | ||
|
|
84fe2c6628 | ||
|
|
b87447b543 |
2
.env
2
.env
@@ -10,6 +10,7 @@ DATA_API_BASE_URL=''
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=''
|
||||
NEW_RELIC_APP_ID=''
|
||||
NEW_RELIC_LICENSE_KEY=''
|
||||
SITE_NAME=''
|
||||
@@ -32,3 +33,4 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
DISPLAY_FEEDBACK_WIDGET='true'
|
||||
|
||||
@@ -12,6 +12,7 @@ FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SITE_NAME=localhost
|
||||
DATA_API_BASE_URL='http://localhost:8000'
|
||||
@@ -39,3 +40,4 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
DISPLAY_FEEDBACK_WIDGET='false'
|
||||
|
||||
@@ -3,3 +3,4 @@ dist/
|
||||
node_modules/
|
||||
src/postcss.config.js
|
||||
src/segment.js
|
||||
src/lightning.js
|
||||
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -11,22 +11,18 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
npm: [8.5.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install npm 8.5.x
|
||||
run: npm install -g npm@${{ matrix.npm }}
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
|
||||
5
.github/workflows/npm-publish.yml
vendored
5
.github/workflows/npm-publish.yml
vendored
@@ -15,10 +15,13 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
21
Makefile
21
Makefile
@@ -1,9 +1,10 @@
|
||||
npm-install-%: ## install specified % npm package
|
||||
npm install $* --save-dev
|
||||
npm ci $* --save-dev
|
||||
git add package.json
|
||||
export TRANSIFEX_RESOURCE = frontend-app-gradebook
|
||||
transifex_langs = "ar,de,es_419,fa_IR,fr,fr_CA,hi,it,pt,ru,uk,zh_CN"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
@@ -11,7 +12,7 @@ tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transi
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test is-es5
|
||||
|
||||
@@ -54,9 +55,25 @@ push_translations:
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-app-gradebook/src/i18n/messages:frontend-app-gradebook
|
||||
|
||||
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-gradebook
|
||||
endif
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
75
README.md
75
README.md
@@ -1,3 +1,5 @@
|
||||
# frontend-app-gradebook
|
||||
|
||||
[](https://travis-ci.com/edx/frontend-app-gradebook)
|
||||
[](https://app.codecov.io/gh/openedx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
@@ -5,7 +7,7 @@
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](https://github.com/semantic-release/semantic-release)
|
||||
|
||||
# Gradebook
|
||||
# Purpose
|
||||
|
||||
Gradebook allows course staff to view, filter, and override subsection grades for a course. Additionally for Masters courses, Gradebook enables bulk management of subsection grades.
|
||||
|
||||
@@ -47,7 +49,8 @@ depending on their needs. Instructors that expect to review grades infrequently
|
||||
to the problem in question will have a worse UX than the legacy gradebook provides. Instructors that rely on the graphs
|
||||
generated by the current gradebook might find the lack of autogenerated graphs to be frustrating.
|
||||
|
||||
## Quickstart
|
||||
## Getting Started
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -55,6 +58,20 @@ To install gradebook into your project:
|
||||
```
|
||||
npm i --save @edx/frontend-app-gradebook
|
||||
```
|
||||
Cloning and Startup
|
||||
===================
|
||||
|
||||
1. Clone your new repo:
|
||||
|
||||
``git clone https://github.com/openedx/frontend-app-gradebook.git``
|
||||
|
||||
2. Install npm dependencies:
|
||||
|
||||
``cd frontend-app-gradebook && npm install``
|
||||
|
||||
3. Start the dev server:
|
||||
|
||||
``npm start``
|
||||
|
||||
## Running the UI Standalone
|
||||
|
||||
@@ -119,3 +136,57 @@ running gradebook container.
|
||||
## Authentication with backend API services
|
||||
|
||||
See the [`@edx/frontend-auth`](https://github.com/edx-unsupported/frontend-auth) repo for information about securing routes in your application that require user authentication.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
The code in this repository is licensed under the AGPLv3 unless otherwise
|
||||
noted.
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
Contributions are very welcome. Please read `How To Contribute`_ for details.
|
||||
|
||||
.. _How To Contribute: https://openedx.org/r/how-to-contribute
|
||||
|
||||
This project is currently accepting all types of contributions, bug fixes,
|
||||
security fixes, maintenance work, or new features. However, please make sure
|
||||
to have a discussion about your new feature idea with the maintainers prior to
|
||||
beginning development to maximize the chances of your change being accepted.
|
||||
You can start a conversation by creating a new issue on this repo summarizing
|
||||
your idea.
|
||||
|
||||
Getting Help
|
||||
===========
|
||||
|
||||
If you're having trouble, we have discussion forums at
|
||||
https://discuss.openedx.org where you can connect with others in the community.
|
||||
|
||||
Our real-time conversations are on Slack. You can request a `Slack
|
||||
invitation`_, then join our `community Slack workspace`_. Because this is a
|
||||
frontend repository, the best place to discuss it would be in the `#wg-frontend
|
||||
channel`_.
|
||||
|
||||
For anything non-trivial, the best path is to open an issue in this repository
|
||||
with as many details about the issue you are facing as you can provide.
|
||||
|
||||
https://github.com/openedx/frontend-app-gradebook/issues
|
||||
|
||||
For more information about these options, see the `Getting Help`_ page.
|
||||
|
||||
.. _Slack invitation: https://openedx.org/slack
|
||||
.. _community Slack workspace: https://openedx.slack.com/
|
||||
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
||||
.. _Getting Help: https://openedx.org/community/connect
|
||||
|
||||
The Open edX Code of Conduct
|
||||
============================
|
||||
|
||||
All community members are expected to follow the `Open edX Code of Conduct`_.
|
||||
|
||||
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
|
||||
|
||||
Reporting Security Issues
|
||||
=========================
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
|
||||
@@ -5,9 +5,6 @@ module.exports = createConfig('jest', {
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
modulePaths: ['<rootDir>/src/'],
|
||||
snapshotSerializers: [
|
||||
'enzyme-to-json/serializer',
|
||||
],
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
|
||||
42197
package-lock.json
generated
42197
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.6.1",
|
||||
"version": "1.6.2",
|
||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"prepush": "npm run lint",
|
||||
@@ -28,11 +28,13 @@
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
|
||||
"@edx/frontend-component-footer": "^11.1.1",
|
||||
"@edx/frontend-component-header": "^3.1.1",
|
||||
"@edx/frontend-platform": "2.5.0",
|
||||
"@edx/paragon": "^19.25.4",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "12.2.0",
|
||||
"@edx/frontend-component-header": "4.6.0",
|
||||
"@edx/frontend-platform": "5.5.4",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/paragon": "20.45.0",
|
||||
"@edx/react-unit-test-utils": "1.7.1",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
||||
@@ -43,19 +45,16 @@
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "3.6.5",
|
||||
"email-prop-type": "^1.1.7",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "4.10.1",
|
||||
"prop-types": "15.7.2",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "6.13.0",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^2.9.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-router": "6.15.0",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
"redux": "4.0.5",
|
||||
"redux-beacon": "^2.1.0",
|
||||
@@ -68,18 +67,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "^12.4.15",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"@edx/frontend-build": "13.0.1",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"axios": "0.21.2",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"es-check": "^2.3.0",
|
||||
"fetch-mock": "^6.5.2",
|
||||
"husky": "2.7.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "29.3.1",
|
||||
"jest": "^26.6.3",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-test-renderer": "^16.10.1",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"semantic-release": "^19.0.3"
|
||||
|
||||
30
src/App.jsx
30
src/App.jsx
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
import './App.scss';
|
||||
@@ -15,21 +14,18 @@ import Head from './head/Head';
|
||||
const App = () => (
|
||||
<AppProvider store={store}>
|
||||
<Head />
|
||||
<Router>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={routePath}
|
||||
component={GradebookPage}
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</Router>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/:courseId"
|
||||
element={<GradebookPage />}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,80 +1,70 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
|
||||
import App from './App';
|
||||
import Head from './head/Head';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
BrowserRouter: () => 'BrowserRouter',
|
||||
Route: () => 'Route',
|
||||
Switch: () => 'Switch',
|
||||
Routes: () => 'Routes',
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppProvider: () => 'AppProvider',
|
||||
}));
|
||||
jest.mock('data/constants/app', () => ({
|
||||
routePath: '/:courseId',
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
jest.mock('data/store', () => 'testStore');
|
||||
jest.mock('containers/GradebookPage', () => 'GradebookPage');
|
||||
jest.mock('@edx/frontend-component-header', () => 'Header');
|
||||
jest.mock('./head/Head', () => 'Head');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
let router;
|
||||
let secondChild;
|
||||
|
||||
describe('App router component', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<App />)).toMatchSnapshot();
|
||||
expect(shallow(<App />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.childAt(1);
|
||||
secondChild = el.instance.children;
|
||||
});
|
||||
describe('AppProvider', () => {
|
||||
test('AppProvider is the parent component, passed the redux store props', () => {
|
||||
expect(el.type()).toBe(AppProvider);
|
||||
expect(el.props().store).toEqual(store);
|
||||
expect(el.instance.type).toBe('AppProvider');
|
||||
expect(el.instance.props.store).toEqual(store);
|
||||
});
|
||||
});
|
||||
describe('Head', () => {
|
||||
test('first child of AppProvider', () => {
|
||||
expect(el.childAt(0).type()).toBe(Head);
|
||||
expect(el.instance.children[0].type).toBe('Head');
|
||||
});
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('second child of AppProvider', () => {
|
||||
expect(router.type()).toBe(Router);
|
||||
expect(secondChild[1].type).toBe('div');
|
||||
});
|
||||
test('Header is above/outside-of the routing', () => {
|
||||
expect(router.childAt(0).childAt(0).type()).toBe(Header);
|
||||
expect(router.childAt(0).childAt(1).type()).toBe('main');
|
||||
expect(secondChild[1].children[0].type).toBe('Header');
|
||||
expect(secondChild[1].children[1].type).toBe('main');
|
||||
});
|
||||
test('Routing - GradebookPage is only route', () => {
|
||||
expect(router.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path={routePath} component={GradebookPage} />
|
||||
</Switch>
|
||||
</main>,
|
||||
));
|
||||
expect(secondChild[1].findByType(Route)).toHaveLength(1);
|
||||
expect(secondChild[1].findByType(Route)[0].props.path).toEqual('/:courseId');
|
||||
expect(secondChild[1].findByType(Route)[0].props.element.type).toEqual(GradebookPage);
|
||||
});
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(router.find(Footer).props().logo).toEqual(logo);
|
||||
expect(secondChild[1].findByType(Footer)[0].props.logo).toEqual(logo);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,20 +5,17 @@ exports[`App router component snapshot 1`] = `
|
||||
store="testStore"
|
||||
>
|
||||
<Head />
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
component="GradebookPage"
|
||||
exact={true}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
element={<GradebookPage />}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</AppProvider>
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -35,17 +35,17 @@ describe('BulkManagementAlerts', () => {
|
||||
el = shallow(<BulkManagementAlerts />);
|
||||
});
|
||||
test('snapshot - bulkImportError closed, success closed', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('closed danger alert', () => {
|
||||
expect(el.childAt(0).is(Alert)).toEqual(true);
|
||||
expect(el.childAt(0).props().show).toEqual(false);
|
||||
expect(el.childAt(0).props().variant).toEqual('danger');
|
||||
expect(el.instance.children[0].type).toBe('Alert');
|
||||
expect(el.instance.findByType(Alert)[0].props.show).toEqual(false);
|
||||
expect(el.instance.findByType(Alert)[0].props.variant).toEqual('danger');
|
||||
});
|
||||
test('closed success alert', () => {
|
||||
expect(el.childAt(1).is(Alert)).toEqual(true);
|
||||
expect(el.childAt(1).props().show).toEqual(false);
|
||||
expect(el.childAt(1).props().variant).toEqual('success');
|
||||
expect(el.instance.children[1].type).toBe('Alert');
|
||||
expect(el.instance.findByType(Alert)[1].props.show).toEqual(false);
|
||||
expect(el.instance.findByType(Alert)[1].props.variant).toEqual('success');
|
||||
});
|
||||
});
|
||||
describe('no errer, no upload success', () => {
|
||||
@@ -57,19 +57,19 @@ describe('BulkManagementAlerts', () => {
|
||||
'success alert open with messages.successDialog',
|
||||
];
|
||||
test(`snapshot - ${assertions.join(', ')}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('open danger alert with bulkImportError content', () => {
|
||||
expect(el.childAt(0).is(Alert)).toEqual(true);
|
||||
expect(el.childAt(0).children().text()).toEqual(errorMessage);
|
||||
expect(el.childAt(0).props().show).toEqual(true);
|
||||
expect(el.instance.children[0].type).toBe('Alert');
|
||||
expect(el.instance.findByType(Alert)[0].children[0].el).toEqual(errorMessage);
|
||||
expect(el.instance.findByType(Alert)[0].props.show).toEqual(true);
|
||||
});
|
||||
test('open success alert with messages.successDialog content', () => {
|
||||
expect(el.childAt(1).is(Alert)).toEqual(true);
|
||||
expect(el.childAt(1).children().getElement()).toEqual(
|
||||
expect(el.instance.children[1].type).toBe('Alert');
|
||||
expect(el.shallowWrapper.props.children[1].props.children).toEqual(
|
||||
<FormattedMessage {...messages.successDialog} />,
|
||||
);
|
||||
expect(el.childAt(1).props().show).toEqual(true);
|
||||
expect(el.instance.children[1].props.show).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
@@ -56,12 +56,12 @@ describe('HistoryTable', () => {
|
||||
el = shallow(<HistoryTable {...props} />);
|
||||
});
|
||||
test('snapshot - loads formatted table', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
describe('history table', () => {
|
||||
let table;
|
||||
beforeEach(() => {
|
||||
table = el.find(DataTable);
|
||||
table = el.instance.findByType(DataTable);
|
||||
});
|
||||
describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => {
|
||||
const fieldAssertions = [
|
||||
@@ -70,10 +70,10 @@ describe('HistoryTable', () => {
|
||||
'forwards the rest',
|
||||
];
|
||||
test(`snapshot: ${fieldAssertions.join(', ')}`, () => {
|
||||
expect(table.props().data).toMatchSnapshot();
|
||||
expect(table[0].props.data).toMatchSnapshot();
|
||||
});
|
||||
test(fieldAssertions.join(', '), () => {
|
||||
const rows = table.props().data;
|
||||
const rows = table[0].props.data;
|
||||
expect(rows[0].resultsSummary).toEqual(<ResultsSummary {...entry1.resultsSummary} />);
|
||||
expect(rows[0].user).toEqual(<span className="wrap-text-in-cell">{entry1.user}</span>);
|
||||
expect(
|
||||
@@ -87,7 +87,7 @@ describe('HistoryTable', () => {
|
||||
});
|
||||
});
|
||||
test('columns from bulkManagementColumns', () => {
|
||||
expect(table.props().columns).toEqual(bulkManagementColumns);
|
||||
expect(table[0].props.columns).toEqual(bulkManagementColumns);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { Download } from '@edx/paragon/icons';
|
||||
|
||||
import lms from 'data/services/lms';
|
||||
@@ -35,19 +34,19 @@ describe('ResultsSummary component', () => {
|
||||
el = shallow(<ResultsSummary {...props} />);
|
||||
});
|
||||
test(`snapshot - ${assertions.join(', ')}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('Hyperlink has target="_blank" and rel="noopener noreferrer"', () => {
|
||||
expect(el.props().target).toEqual('_blank');
|
||||
expect(el.props().rel).toEqual('noopener noreferrer');
|
||||
expect(el.instance.props.target).toEqual('_blank');
|
||||
expect(el.instance.props.rel).toEqual('noopener noreferrer');
|
||||
});
|
||||
test('Hyperlink has href to bulkGradesUrl', () => {
|
||||
expect(el.props().href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
|
||||
expect(el.instance.props.href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
|
||||
});
|
||||
test('displays Download Icon and text', () => {
|
||||
const icon = el.childAt(0);
|
||||
expect(icon.is(Icon)).toEqual(true);
|
||||
expect(icon.props().src).toEqual(Download);
|
||||
expect(el.childAt(1).text()).toEqual(props.text);
|
||||
const icon = el.instance.children[0];
|
||||
expect(icon.type).toEqual('Icon');
|
||||
expect(icon.props.src).toEqual(Download);
|
||||
expect(el.instance.children[1].el).toEqual(props.text);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { BulkManagementHistoryView } from '.';
|
||||
@@ -24,20 +24,23 @@ describe('BulkManagementHistoryView', () => {
|
||||
'<HistoryTable />',
|
||||
];
|
||||
test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('heading - h4 loaded from messages', () => {
|
||||
const heading = el.find('h4');
|
||||
expect(heading.getElement()).toEqual((
|
||||
const heading = el.instance.findByType('h4')[0];
|
||||
const expectedHeading = shallow(
|
||||
<h4>
|
||||
<FormattedMessage {...messages.heading} />
|
||||
</h4>
|
||||
));
|
||||
</h4>,
|
||||
);
|
||||
|
||||
expect(heading.el.type).toEqual(expectedHeading.type);
|
||||
expect(heading.el.props).toEqual(expectedHeading.props);
|
||||
});
|
||||
test('heading, then alerts, then upload form, then table', () => {
|
||||
expect(el.childAt(0).is('h4')).toEqual(true);
|
||||
expect(el.childAt(2).is(BulkManagementAlerts)).toEqual(true);
|
||||
expect(el.childAt(3).is(HistoryTable)).toEqual(true);
|
||||
expect(el.instance.children[0].type).toEqual('h4');
|
||||
expect(el.instance.children[2].type).toEqual(BulkManagementAlerts);
|
||||
expect(el.instance.children[3].type).toEqual(HistoryTable);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
@@ -16,6 +16,6 @@ describe('Header', () => {
|
||||
test('snapshot - has edx link with logo url', () => {
|
||||
const url = 'www.ourLogo.url';
|
||||
getConfig.mockReturnValue({ LOGO_URL: url });
|
||||
expect(shallow(<Header />)).toMatchSnapshot();
|
||||
expect(shallow(<Header />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
@@ -38,10 +38,10 @@ describe('AssignmentFilter component', () => {
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('filter options', () => {
|
||||
const { options } = el.find(SelectGroup).props();
|
||||
const { options } = el.instance.findByType(SelectGroup)[0].props;
|
||||
expect(options.length).toEqual(5);
|
||||
const testOption = assignmentFilterOptions[0];
|
||||
const optionProps = options[1].props;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
@@ -37,20 +37,20 @@ describe('AssignmentFilter component', () => {
|
||||
describe('render', () => {
|
||||
describe('with selected assignment', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
it('renders a PercentGroup for both Max and Min filters', () => {
|
||||
let props = el.find(PercentGroup).at(0).props();
|
||||
let { props } = el.instance.findByType(PercentGroup)[0];
|
||||
expect(props.value).toEqual(hookData.assignmentGradeMin);
|
||||
expect(props.disabled).toEqual(false);
|
||||
expect(props.onChange).toEqual(hookData.handleSetMin);
|
||||
props = el.find(PercentGroup).at(1).props();
|
||||
props = el.instance.findByType(PercentGroup)[1].props;
|
||||
expect(props.value).toEqual(hookData.assignmentGradeMax);
|
||||
expect(props.disabled).toEqual(false);
|
||||
expect(props.onChange).toEqual(hookData.handleSetMax);
|
||||
});
|
||||
it('renders a submit button', () => {
|
||||
const props = el.find(Button).props();
|
||||
const { props } = el.instance.findByType(Button)[0];
|
||||
expect(props.disabled).toEqual(false);
|
||||
expect(props.onClick).toEqual(hookData.handleSubmit);
|
||||
});
|
||||
@@ -64,12 +64,12 @@ describe('AssignmentFilter component', () => {
|
||||
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
it('disables controls', () => {
|
||||
let props = el.find(PercentGroup).at(0).props();
|
||||
let { props } = el.instance.findByType(PercentGroup)[0];
|
||||
expect(props.disabled).toEqual(true);
|
||||
props = el.find(PercentGroup).at(1).props();
|
||||
props = el.instance.findByType(PercentGroup)[1].props;
|
||||
expect(props.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
@@ -34,10 +34,10 @@ describe('AssignmentFilterType component', () => {
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('filter options', () => {
|
||||
const { options } = el.find(SelectGroup).props();
|
||||
const { options } = el.instance.findByType(SelectGroup)[0].props;
|
||||
expect(options.length).toEqual(5);
|
||||
const optionProps = options[1].props;
|
||||
expect(optionProps.value).toEqual(assignmentTypes[0]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
@@ -42,18 +42,18 @@ describe('CourseFilter component', () => {
|
||||
describe('render', () => {
|
||||
describe('with selected assignment', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
it('renders a PercentGroup for both Max and Min filters', () => {
|
||||
let props = el.find(PercentGroup).at(0).props();
|
||||
let { props } = el.instance.findByType(PercentGroup)[0];
|
||||
expect(props.value).toEqual(hookData.min.value);
|
||||
expect(props.onChange).toEqual(hookData.min.onChange);
|
||||
props = el.find(PercentGroup).at(1).props();
|
||||
props = el.instance.findByType(PercentGroup)[1].props;
|
||||
expect(props.value).toEqual(hookData.max.value);
|
||||
expect(props.onChange).toEqual(hookData.max.onChange);
|
||||
});
|
||||
it('renders a submit button', () => {
|
||||
const props = el.find(Button).props();
|
||||
const { props } = el.instance.findByType(Button)[0];
|
||||
expect(props.disabled).toEqual(false);
|
||||
expect(props.onClick).toEqual(hookData.handleApplyClick);
|
||||
});
|
||||
@@ -64,10 +64,10 @@ describe('CourseFilter component', () => {
|
||||
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
it('disables submit', () => {
|
||||
const props = el.find(Button).props();
|
||||
const { props } = el.instance.findByType(Button)[0];
|
||||
expect(props.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import PercentGroup from './PercentGroup';
|
||||
|
||||
@@ -22,11 +22,11 @@ describe('PercentGroup', () => {
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const el = shallow(<PercentGroup {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('disabled', () => {
|
||||
const el = shallow(<PercentGroup {...props} disabled />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import SelectGroup from './SelectGroup';
|
||||
|
||||
@@ -27,11 +27,11 @@ describe('SelectGroup', () => {
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const el = shallow(<SelectGroup {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('disabled', () => {
|
||||
const el = shallow(<SelectGroup {...props} disabled />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
@@ -48,14 +48,14 @@ describe('StudentGroupsFilter component', () => {
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('track options', () => {
|
||||
const {
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
} = el.find(SelectGroup).at(0).props();
|
||||
} = el.instance.findByType(SelectGroup)[0].props;
|
||||
expect(value).toEqual(props.tracks.value);
|
||||
expect(onChange).toEqual(props.tracks.handleChange);
|
||||
expect(options.length).toEqual(5);
|
||||
@@ -70,7 +70,7 @@ describe('StudentGroupsFilter component', () => {
|
||||
onChange,
|
||||
disabled,
|
||||
value,
|
||||
} = el.find(SelectGroup).at(1).props();
|
||||
} = el.instance.findByType(SelectGroup)[1].props;
|
||||
expect(value).toEqual(props.cohorts.value);
|
||||
expect(disabled).toEqual(false);
|
||||
expect(onChange).toEqual(props.cohorts.handleChange);
|
||||
|
||||
@@ -16,7 +16,7 @@ exports[`GradebookFilters render snapshot 1`] = `
|
||||
className="p-1"
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction hook.closeMenu]}
|
||||
src={[Function]}
|
||||
src="Close"
|
||||
/>
|
||||
</div>
|
||||
<Collapsible
|
||||
|
||||
@@ -3,7 +3,7 @@ import { actions, selectors, thunkActions } from 'data/redux/hooks';
|
||||
export const useGradebookFiltersData = ({ updateQueryParams }) => {
|
||||
const includeCourseRoleMembers = selectors.filters.useIncludeCourseRoleMembers();
|
||||
const updateIncludeCourseRoleMembers = actions.filters.useUpdateIncludeCourseRoleMembers();
|
||||
const closeMenu = thunkActions.app.useCloseFilterMenu();
|
||||
const closeMenu = thunkActions.app.filterMenu.useCloseMenu();
|
||||
const fetchGrades = thunkActions.grades.useFetchGrades();
|
||||
|
||||
const handleIncludeTeamMembersChange = ({ target: { checked } }) => {
|
||||
|
||||
@@ -9,7 +9,9 @@ jest.mock('data/redux/hooks', () => ({
|
||||
filters: { useIncludeCourseRoleMembers: jest.fn() },
|
||||
},
|
||||
thunkActions: {
|
||||
app: { useCloseFilterMenu: jest.fn() },
|
||||
app: {
|
||||
filterMenu: { useCloseMenu: jest.fn() },
|
||||
},
|
||||
grades: { useFetchGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
@@ -18,7 +20,7 @@ selectors.filters.useIncludeCourseRoleMembers.mockReturnValue(true);
|
||||
const updateIncludeCourseRoleMembers = jest.fn();
|
||||
actions.filters.useUpdateIncludeCourseRoleMembers.mockReturnValue(updateIncludeCourseRoleMembers);
|
||||
const closeFilterMenu = jest.fn();
|
||||
thunkActions.app.useCloseFilterMenu.mockReturnValue(closeFilterMenu);
|
||||
thunkActions.app.filterMenu.useCloseMenu.mockReturnValue(closeFilterMenu);
|
||||
const fetchGrades = jest.fn();
|
||||
thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades);
|
||||
|
||||
@@ -34,7 +36,7 @@ describe('GradebookFiltersData component hooks', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(actions.filters.useUpdateIncludeCourseRoleMembers).toHaveBeenCalledWith();
|
||||
expect(selectors.filters.useIncludeCourseRoleMembers).toHaveBeenCalledWith();
|
||||
expect(thunkActions.app.useCloseFilterMenu).toHaveBeenCalledWith();
|
||||
expect(thunkActions.app.filterMenu.useCloseMenu).toHaveBeenCalledWith();
|
||||
expect(thunkActions.grades.useFetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
@@ -49,10 +49,10 @@ describe('GradebookFilters', () => {
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('Assignment filters', () => {
|
||||
expect(el.find(Collapsible).at(0).children()).toMatchObject(shallow(
|
||||
expect(el.instance.findByType(Collapsible)[0].children[0]).toMatchObject(shallow(
|
||||
<div>
|
||||
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
|
||||
<AssignmentFilter updateQueryParams={updateQueryParams} />
|
||||
@@ -61,22 +61,22 @@ describe('GradebookFilters', () => {
|
||||
));
|
||||
});
|
||||
test('CourseGrade filters', () => {
|
||||
expect(el.find(Collapsible).at(1).children()).toMatchObject(shallow(
|
||||
expect(el.instance.findByType(Collapsible)[1].children[0]).toMatchObject(shallow(
|
||||
<CourseGradeFilter updateQueryParams={updateQueryParams} />,
|
||||
));
|
||||
});
|
||||
test('StudentGroups filters', () => {
|
||||
expect(el.find(Collapsible).at(2).children()).toMatchObject(shallow(
|
||||
expect(el.instance.findByType(Collapsible)[2].children[0]).toMatchObject(shallow(
|
||||
<StudentGroupsFilter updateQueryParams={updateQueryParams} />,
|
||||
));
|
||||
});
|
||||
test('includeCourseTeamMembers', () => {
|
||||
const checkbox = el.find(Collapsible).at(3).children();
|
||||
expect(checkbox.props()).toEqual({
|
||||
const checkbox = el.instance.findByType(Collapsible)[3].children[0];
|
||||
expect(checkbox.props).toEqual({
|
||||
checked: true,
|
||||
onChange: hookProps.includeCourseTeamMembers.handleChange,
|
||||
children: formatMessage(messages.includeCourseTeamMembers),
|
||||
});
|
||||
expect(checkbox.children[0].el).toEqual(formatMessage(messages.includeCourseTeamMembers));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
131
src/components/GradebookHeader/__snapshots__/index.test.jsx.snap
Normal file
131
src/components/GradebookHeader/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,131 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookHeader component render default view shapshot 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component render frozen grades snapshot: show frozen warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component render show bulk management snapshot: show toggle view message button with handleToggleViewClick method 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction hooks.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
toggle-view-message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component render user cannot view gradebook snapshot: show unauthorized warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
You are not authorized to view the gradebook for this course.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,261 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookHeader component snapshots default values (grades frozen, cannot view). unauthorized warning, but no grades frozen warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots grades frozen, can view. grades frozen warning but no unauthorized warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
|
||||
description="Warning message in Gradebook Header for frozen messages"
|
||||
id="gradebook.GradebookHeader.frozenWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots grades frozen, cannot view unauthorized warning, and grades frozen warning. 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
|
||||
description="Warning message in Gradebook Header for frozen messages"
|
||||
id="gradebook.GradebookHeader.frozenWarning"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots show bulk management, active view is bulkManagementHistory view toggle view button to grades 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Return to Gradebook"
|
||||
description="Button text for button navigating to Grades view."
|
||||
id="gradebook.GradebookHeader.toGradesView"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots show bulk management, active view is grades view toggle view button to activity log 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View Bulk Management History"
|
||||
description="Button text for button navigating to Bulk Managment Activity Log"
|
||||
id="gradebook.GradebookHeader.toActivityLogButton"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
35
src/components/GradebookHeader/hooks.js
Normal file
35
src/components/GradebookHeader/hooks.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { views } from 'data/constants/app';
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const useGradebookHeaderData = () => {
|
||||
const activeView = selectors.app.useActiveView();
|
||||
const courseId = selectors.app.useCourseId();
|
||||
const areGradesFrozen = selectors.assignmentTypes.useAreGradesFrozen();
|
||||
const canUserViewGradebook = selectors.roles.useCanUserViewGradebook();
|
||||
const showBulkManagement = selectors.root.useShowBulkManagement();
|
||||
const setView = actions.app.useSetView();
|
||||
|
||||
const handleToggleViewClick = () => setView(
|
||||
activeView === views.grades
|
||||
? views.bulkManagementHistory
|
||||
: views.grades,
|
||||
);
|
||||
|
||||
const toggleViewMessage = activeView === views.grades
|
||||
? messages.toActivityLog
|
||||
: messages.toGradesView;
|
||||
|
||||
return {
|
||||
areGradesFrozen,
|
||||
canUserViewGradebook,
|
||||
courseId,
|
||||
showBulkManagement,
|
||||
|
||||
handleToggleViewClick,
|
||||
toggleViewMessage,
|
||||
};
|
||||
};
|
||||
|
||||
export default useGradebookHeaderData;
|
||||
90
src/components/GradebookHeader/hooks.test.js
Normal file
90
src/components/GradebookHeader/hooks.test.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { views } from 'data/constants/app';
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import useGradebookHeaderData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
actions: {
|
||||
app: {
|
||||
useSetView: jest.fn(),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
app: {
|
||||
useActiveView: jest.fn(),
|
||||
useCourseId: jest.fn(),
|
||||
},
|
||||
assignmentTypes: {
|
||||
useAreGradesFrozen: jest.fn(),
|
||||
},
|
||||
roles: {
|
||||
useCanUserViewGradebook: jest.fn(),
|
||||
},
|
||||
root: {
|
||||
useShowBulkManagement: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const activeView = 'test-active-view';
|
||||
selectors.app.useActiveView.mockReturnValue(activeView);
|
||||
const courseId = 'test-course-id';
|
||||
selectors.app.useCourseId.mockReturnValue(courseId);
|
||||
const areGradesFrozen = 'test-are-grades-frozen';
|
||||
selectors.assignmentTypes.useAreGradesFrozen.mockReturnValue(areGradesFrozen);
|
||||
const canUserViewGradebook = 'test-can-user-view-gradebook';
|
||||
selectors.roles.useCanUserViewGradebook.mockReturnValue(canUserViewGradebook);
|
||||
const showBulkManagement = 'test-show-bulk-management';
|
||||
selectors.root.useShowBulkManagement.mockReturnValue(showBulkManagement);
|
||||
|
||||
const setView = jest.fn();
|
||||
actions.app.useSetView.mockReturnValue(setView);
|
||||
|
||||
let out;
|
||||
describe('useGradebookHeaderData hooks', () => {
|
||||
describe('initialization', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
out = useGradebookHeaderData();
|
||||
expect(selectors.app.useActiveView).toHaveBeenCalled();
|
||||
expect(selectors.app.useCourseId).toHaveBeenCalled();
|
||||
expect(selectors.assignmentTypes.useAreGradesFrozen).toHaveBeenCalled();
|
||||
expect(selectors.roles.useCanUserViewGradebook).toHaveBeenCalled();
|
||||
expect(selectors.root.useShowBulkManagement).toHaveBeenCalled();
|
||||
expect(actions.app.useSetView).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
test('redux fields', () => {
|
||||
out = useGradebookHeaderData();
|
||||
expect(out.areGradesFrozen).toEqual(areGradesFrozen);
|
||||
expect(out.canUserViewGradebook).toEqual(canUserViewGradebook);
|
||||
expect(out.courseId).toEqual(courseId);
|
||||
expect(out.showBulkManagement).toEqual(showBulkManagement);
|
||||
});
|
||||
describe('handleToggleViewClick', () => {
|
||||
it('calls setView with bulkManagemnetHistory message if grades view is active', () => {
|
||||
selectors.app.useActiveView.mockReturnValueOnce(views.grades);
|
||||
out = useGradebookHeaderData();
|
||||
out.handleToggleViewClick();
|
||||
expect(setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||
});
|
||||
it('calls setView with grades view if grades view is not active', () => {
|
||||
out = useGradebookHeaderData();
|
||||
out.handleToggleViewClick();
|
||||
expect(setView).toHaveBeenCalledWith(views.grades);
|
||||
});
|
||||
});
|
||||
describe('toggleViewMessage', () => {
|
||||
it('returns toActivityLog message if grades view is active', () => {
|
||||
selectors.app.useActiveView.mockReturnValueOnce(views.grades);
|
||||
out = useGradebookHeaderData();
|
||||
expect(out.toggleViewMessage).toEqual(messages.toActivityLog);
|
||||
});
|
||||
it('returns toGradesView message if grades view is not active', () => {
|
||||
out = useGradebookHeaderData();
|
||||
expect(out.toggleViewMessage).toEqual(messages.toGradesView);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,106 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { views } from 'data/constants/app';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import { instructorDashboardUrl } from 'data/services/lms/urls';
|
||||
import useGradebookHeaderData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export class GradebookHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleToggleViewClick = this.handleToggleViewClick.bind(this);
|
||||
}
|
||||
|
||||
handleToggleViewClick() {
|
||||
const newView = this.props.activeView === views.grades ? views.bulkManagementHistory : views.grades;
|
||||
this.props.setView(newView);
|
||||
}
|
||||
|
||||
get toggleViewMessage() {
|
||||
return this.props.activeView === views.grades
|
||||
? messages.toActivityLog
|
||||
: messages.toGradesView;
|
||||
}
|
||||
|
||||
lmsInstructorDashboardUrl = courseId => (
|
||||
`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor`
|
||||
);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="gradebook-header">
|
||||
<a
|
||||
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
|
||||
className="mb-3"
|
||||
>
|
||||
<span aria-hidden="true">{'<< '}</span>
|
||||
<FormattedMessage {...messages.backToDashboard} />
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage {...messages.gradebook} />
|
||||
</h1>
|
||||
<div className="subtitle-row d-flex justify-content-between align-items-center">
|
||||
<h2>{this.props.courseId}</h2>
|
||||
{ this.props.showBulkManagement && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={this.handleToggleViewClick}
|
||||
>
|
||||
<FormattedMessage {...this.toggleViewMessage} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{this.props.areGradesFrozen
|
||||
&& (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<FormattedMessage {...messages.frozenWarning} />
|
||||
</div>
|
||||
)}
|
||||
{(this.props.canUserViewGradebook === false) && (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<FormattedMessage {...messages.unauthorizedWarning} />
|
||||
</div>
|
||||
export const GradebookHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
areGradesFrozen,
|
||||
canUserViewGradebook,
|
||||
courseId,
|
||||
handleToggleViewClick,
|
||||
showBulkManagement,
|
||||
toggleViewMessage,
|
||||
} = useGradebookHeaderData();
|
||||
const dashboardUrl = instructorDashboardUrl();
|
||||
return (
|
||||
<div className="gradebook-header">
|
||||
<a href={dashboardUrl} className="mb-3">
|
||||
<span aria-hidden="true">{'<< '}</span>
|
||||
{formatMessage(messages.backToDashboard)}
|
||||
</a>
|
||||
<h1>{formatMessage(messages.gradebook)}</h1>
|
||||
<div className="subtitle-row d-flex justify-content-between align-items-center">
|
||||
<h2>{courseId}</h2>
|
||||
{showBulkManagement && (
|
||||
<Button variant="tertiary" onClick={handleToggleViewClick}>
|
||||
{formatMessage(toggleViewMessage)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GradebookHeader.defaultProps = {
|
||||
// redux
|
||||
courseId: '',
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: false,
|
||||
showBulkManagement: false,
|
||||
{areGradesFrozen && (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
{formatMessage(messages.frozenWarning)}
|
||||
</div>
|
||||
)}
|
||||
{(canUserViewGradebook === false) && (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
{formatMessage(messages.unauthorizedWarning)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GradebookHeader.propTypes = {
|
||||
// redux
|
||||
activeView: PropTypes.string.isRequired,
|
||||
courseId: PropTypes.string,
|
||||
areGradesFrozen: PropTypes.bool,
|
||||
canUserViewGradebook: PropTypes.bool,
|
||||
setView: PropTypes.func.isRequired,
|
||||
showBulkManagement: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
activeView: selectors.app.activeView(state),
|
||||
courseId: selectors.app.courseId(state),
|
||||
areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
|
||||
canUserViewGradebook: selectors.roles.canUserViewGradebook(state),
|
||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
setView: actions.app.setView,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GradebookHeader);
|
||||
export default GradebookHeader;
|
||||
|
||||
77
src/components/GradebookHeader/index.test.jsx
Normal file
77
src/components/GradebookHeader/index.test.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { instructorDashboardUrl } from 'data/services/lms/urls';
|
||||
|
||||
import useGradebookHeaderData from './hooks';
|
||||
import GradebookHeader from '.';
|
||||
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
instructorDashboardUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
instructorDashboardUrl.mockReturnValue('test-dashboard-url');
|
||||
|
||||
const hookProps = {
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'test-course-id',
|
||||
handleToggleViewClick: jest.fn().mockName('hooks.handleToggleViewClick'),
|
||||
showBulkManagement: false,
|
||||
toggleViewMessage: { defaultMessage: 'toggle-view-message' },
|
||||
};
|
||||
useGradebookHeaderData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('GradebookHeader component', () => {
|
||||
beforeAll(() => {
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useGradebookHeaderData).toHaveBeenCalledWith();
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('default view', () => {
|
||||
test('shapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('show bulk management', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true });
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
test('snapshot: show toggle view message button with handleToggleViewClick method', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
const { onClick } = el.instance.findByType(Button)[0].props;
|
||||
expect(onClick).toEqual(hookProps.handleToggleViewClick);
|
||||
expect(el.instance.findByType(Button)[0].children[0].el).toEqual(formatMessage(hookProps.toggleViewMessage));
|
||||
});
|
||||
});
|
||||
describe('frozen grades', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true });
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
test('snapshot: show frozen warning', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('user cannot view gradebook', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false });
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
test('snapshot: show unauthorized warning', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import { views } from 'data/constants/app';
|
||||
import messages from './messages';
|
||||
import { GradebookHeader, mapDispatchToProps, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setView: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
activeView: jest.fn(state => ({ aciveView: state })),
|
||||
courseId: jest.fn(state => ({ courseId: state })),
|
||||
},
|
||||
assignmentTypes: { areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })) },
|
||||
roles: { canUserViewGradebook: jest.fn(state => ({ canUserViewGradebook: state })) },
|
||||
root: { showBulkManagement: jest.fn(state => ({ showBulkManagement: state })) },
|
||||
},
|
||||
}));
|
||||
|
||||
const courseId = 'fakeID';
|
||||
describe('GradebookHeader component', () => {
|
||||
const props = {
|
||||
activeView: views.grades,
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: false,
|
||||
courseId,
|
||||
showBulkManagement: false,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.setView = jest.fn();
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookHeader {...props} />);
|
||||
el.instance().handleToggleViewClick = jest.fn().mockName('this.handleToggleViewClick');
|
||||
});
|
||||
describe('default values (grades frozen, cannot view).', () => {
|
||||
test('unauthorized warning, but no grades frozen warning', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('grades frozen, cannot view', () => {
|
||||
test('unauthorized warning, and grades frozen warning.', () => {
|
||||
el.setProps({ areGradesFrozen: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('grades frozen, can view.', () => {
|
||||
test('grades frozen warning but no unauthorized warning', () => {
|
||||
el.setProps({ areGradesFrozen: true, canUserViewGradebook: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('show bulk management, active view is grades view', () => {
|
||||
test('toggle view button to activity log', () => {
|
||||
el.setProps({ showBulkManagement: true });
|
||||
expect(el.find(Button).getElement()).toEqual((
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={el.instance().handleToggleViewClick}
|
||||
>
|
||||
<FormattedMessage {...messages.toActivityLog} />
|
||||
</Button>
|
||||
));
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('show bulk management, active view is bulkManagementHistory view', () => {
|
||||
test('toggle view button to grades', () => {
|
||||
el.setProps({ showBulkManagement: true, activeView: views.bulkManagementHistory });
|
||||
expect(el.find(Button).getElement()).toEqual((
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={el.instance().handleToggleViewClick}
|
||||
>
|
||||
<FormattedMessage {...messages.toGradesView} />
|
||||
</Button>
|
||||
));
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookHeader {...props} />);
|
||||
});
|
||||
describe('handleToggleViewClick', () => {
|
||||
test('calls setView with activity view if activeView is grades', () => {
|
||||
el.instance().handleToggleViewClick();
|
||||
expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||
});
|
||||
test('calls setView with grades view if activeView is bulkManagementHistory', () => {
|
||||
el.setProps({ activeView: views.bulkManagementHistory });
|
||||
el.instance().handleToggleViewClick();
|
||||
expect(props.setView).toHaveBeenCalledWith(views.grades);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { a: 'test', example: 'state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('activeView from app.activeView', () => {
|
||||
expect(mapped.activeView).toEqual(selectors.app.activeView(testState));
|
||||
});
|
||||
test('courseId from app.courseId', () => {
|
||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
||||
});
|
||||
test('areGradesFrozen from assignmentTypes selector', () => {
|
||||
expect(
|
||||
mapped.areGradesFrozen,
|
||||
).toEqual(selectors.assignmentTypes.areGradesFrozen(testState));
|
||||
});
|
||||
test('canUserViewGradebook from roles selector', () => {
|
||||
expect(
|
||||
mapped.canUserViewGradebook,
|
||||
).toEqual(selectors.roles.canUserViewGradebook(testState));
|
||||
});
|
||||
test('showBulkManagement from root showBulkManagement selector', () => {
|
||||
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('setView from actions.app.setView', () => {
|
||||
expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { views } from 'data/constants/app';
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import NetworkButton from 'components/NetworkButton';
|
||||
import ImportGradesButton from './ImportGradesButton';
|
||||
|
||||
import messages from './BulkManagementControls.messages';
|
||||
|
||||
/**
|
||||
* <BulkManagementControls />
|
||||
* Provides download buttons for Bulk Management and Intervention reports, only if
|
||||
* showBulkManagement is set in redus.
|
||||
*/
|
||||
export class BulkManagementControls extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClickExportGrades = this.handleClickExportGrades.bind(this);
|
||||
this.handleViewActivityLog = this.handleViewActivityLog.bind(this);
|
||||
}
|
||||
|
||||
handleClickExportGrades() {
|
||||
this.props.downloadBulkGradesReport();
|
||||
window.location.assign(this.props.gradeExportUrl);
|
||||
}
|
||||
|
||||
handleViewActivityLog() {
|
||||
this.props.setView(views.bulkManagementHistory);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.showBulkManagement && (
|
||||
<div className="d-flex">
|
||||
<NetworkButton
|
||||
label={messages.downloadGradesBtn}
|
||||
onClick={this.handleClickExportGrades}
|
||||
/>
|
||||
<ImportGradesButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BulkManagementControls.defaultProps = {
|
||||
showBulkManagement: false,
|
||||
};
|
||||
|
||||
BulkManagementControls.propTypes = {
|
||||
// redux
|
||||
downloadBulkGradesReport: PropTypes.func.isRequired,
|
||||
gradeExportUrl: PropTypes.string.isRequired,
|
||||
showBulkManagement: PropTypes.bool,
|
||||
setView: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
gradeExportUrl: selectors.root.gradeExportUrl(state),
|
||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
|
||||
setView: actions.app.setView,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);
|
||||
@@ -1,124 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import { views } from 'data/constants/app';
|
||||
|
||||
import {
|
||||
BulkManagementControls,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './BulkManagementControls';
|
||||
|
||||
jest.mock('./ImportGradesButton', () => 'ImportGradesButton');
|
||||
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
root: {
|
||||
gradeExportUrl: (state) => ({ gradeExportUrl: state }),
|
||||
interventionExportUrl: (state) => ({ interventionExportUrl: state }),
|
||||
showBulkManagement: (state) => ({ showBulkManagement: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setView: jest.fn() },
|
||||
grades: {
|
||||
downloadReport: {
|
||||
bulkGrades: jest.fn(),
|
||||
intervention: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('BulkManagementControls', () => {
|
||||
describe('component', () => {
|
||||
let el;
|
||||
let props = {
|
||||
gradeExportUrl: 'gradesGoHere',
|
||||
interventionExportUrl: 'interventionsGoHere',
|
||||
};
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
downloadBulkGradesReport: jest.fn(),
|
||||
downloadInterventionReport: jest.fn(),
|
||||
setView: jest.fn(),
|
||||
};
|
||||
});
|
||||
test('snapshot - empty if showBulkManagement is not truthy', () => {
|
||||
expect(shallow(<BulkManagementControls {...props} />)).toEqual({});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
const oldWindowLocation = window.location;
|
||||
|
||||
beforeAll(() => {
|
||||
delete window.location;
|
||||
window.location = Object.defineProperties(
|
||||
{},
|
||||
{
|
||||
...Object.getOwnPropertyDescriptors(oldWindowLocation),
|
||||
assign: {
|
||||
configurable: true,
|
||||
value: jest.fn(),
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
beforeEach(() => {
|
||||
window.location.assign.mockReset();
|
||||
el = shallow(<BulkManagementControls {...props} showBulkManagement />);
|
||||
});
|
||||
afterAll(() => {
|
||||
// restore `window.location` to the `jsdom` `Location` object
|
||||
window.location = oldWindowLocation;
|
||||
});
|
||||
describe('handleViewActivityLog', () => {
|
||||
it('calls props.setView(views.bulkManagementHistory)', () => {
|
||||
el.instance().handleViewActivityLog();
|
||||
expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||
});
|
||||
});
|
||||
describe('handleClickExportGrades', () => {
|
||||
const assertions = [
|
||||
'calls props.downloadBulkGradesReport',
|
||||
'sets location to props.gradeExportUrl',
|
||||
];
|
||||
it(assertions.join(' and '), () => {
|
||||
el.instance().handleClickExportGrades();
|
||||
expect(props.downloadBulkGradesReport).toHaveBeenCalled();
|
||||
expect(window.location.assign).toHaveBeenCalledWith(props.gradeExportUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { do: 'not', test: 'me' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('gradeExportUrl from root.gradeExportUrl', () => {
|
||||
expect(mapped.gradeExportUrl).toEqual(selectors.root.gradeExportUrl(testState));
|
||||
});
|
||||
test('showBulkManagement from root.showBulkManagement', () => {
|
||||
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('downloadBulkGradesReport from actions.grades.downloadReport.bulkGrades', () => {
|
||||
expect(
|
||||
mapDispatchToProps.downloadBulkGradesReport,
|
||||
).toEqual(actions.grades.downloadReport.bulkGrades);
|
||||
});
|
||||
test('setView from actions.app.setView', () => {
|
||||
expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = `
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<NetworkButton
|
||||
label={
|
||||
Object {
|
||||
"defaultMessage": "Download Grades",
|
||||
"description": "A labeled button that allows an admin user to download course grades all at once (in bulk).",
|
||||
"id": "gradebook.GradesView.BulkManagementControls.bulkManagementLabel",
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
<ImportGradesButton />
|
||||
</div>
|
||||
`;
|
||||
18
src/components/GradesView/BulkManagementControls/hooks.js
Normal file
18
src/components/GradesView/BulkManagementControls/hooks.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
export const useBulkManagementControlsData = () => {
|
||||
const gradeExportUrl = selectors.root.useGradeExportUrl();
|
||||
const showBulkManagement = selectors.root.useShowBulkManagement();
|
||||
const downloadBulkGradesReport = actions.grades.useDownloadBulkGradesReport();
|
||||
|
||||
const handleClickExportGrades = () => {
|
||||
downloadBulkGradesReport();
|
||||
window.location.assign(gradeExportUrl);
|
||||
};
|
||||
|
||||
return {
|
||||
show: showBulkManagement,
|
||||
handleClickExportGrades,
|
||||
};
|
||||
};
|
||||
export default useBulkManagementControlsData;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
import useBulkManagementControlsData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
actions: {
|
||||
grades: {
|
||||
useDownloadBulkGradesReport: jest.fn(),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
root: {
|
||||
useGradeExportUrl: jest.fn(),
|
||||
useShowBulkManagement: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const downloadBulkGrades = jest.fn();
|
||||
actions.grades.useDownloadBulkGradesReport.mockReturnValue(downloadBulkGrades);
|
||||
const gradeExportUrl = 'test-grade-export-url';
|
||||
selectors.root.useGradeExportUrl.mockReturnValue(gradeExportUrl);
|
||||
selectors.root.useShowBulkManagement.mockReturnValue(true);
|
||||
|
||||
let hook;
|
||||
describe('useBulkManagementControlsData', () => {
|
||||
const oldWindowLocation = window.location;
|
||||
beforeAll(() => {
|
||||
delete window.location;
|
||||
window.location = Object.defineProperties(
|
||||
{},
|
||||
{
|
||||
...Object.getOwnPropertyDescriptors(oldWindowLocation),
|
||||
assign: { configurable: true, value: jest.fn() },
|
||||
},
|
||||
);
|
||||
});
|
||||
beforeEach(() => {
|
||||
window.location.assign.mockReset();
|
||||
hook = useBulkManagementControlsData();
|
||||
});
|
||||
afterAll(() => {
|
||||
// restore `window.location` to the `jsdom` `Location` object
|
||||
window.location = oldWindowLocation;
|
||||
});
|
||||
describe('initialization', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.root.useGradeExportUrl).toHaveBeenCalledWith();
|
||||
expect(selectors.root.useShowBulkManagement).toHaveBeenCalledWith();
|
||||
expect(actions.grades.useDownloadBulkGradesReport).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards show from showBulkManagement', () => {
|
||||
expect(hook.show).toEqual(true);
|
||||
selectors.root.useShowBulkManagement.mockReturnValue(false);
|
||||
hook = useBulkManagementControlsData();
|
||||
expect(hook.show).toEqual(false);
|
||||
});
|
||||
describe('handleClickExportGrades', () => {
|
||||
beforeEach(() => {
|
||||
hook.handleClickExportGrades();
|
||||
});
|
||||
it('downloads bulk grades report', () => {
|
||||
expect(downloadBulkGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
it('sets window location to grade export url', () => {
|
||||
expect(window.location.assign).toHaveBeenCalledWith(gradeExportUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
33
src/components/GradesView/BulkManagementControls/index.jsx
Normal file
33
src/components/GradesView/BulkManagementControls/index.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
|
||||
import NetworkButton from 'components/NetworkButton';
|
||||
import ImportGradesButton from '../ImportGradesButton';
|
||||
|
||||
import useBulkManagementControlsData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <BulkManagementControls />
|
||||
* Provides download buttons for Bulk Management and Intervention reports, only if
|
||||
* showBulkManagement is set in redus.
|
||||
*/
|
||||
export const BulkManagementControls = () => {
|
||||
const {
|
||||
show,
|
||||
handleClickExportGrades,
|
||||
} = useBulkManagementControlsData();
|
||||
|
||||
if (!show) { return null; }
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<NetworkButton
|
||||
label={messages.downloadGradesBtn}
|
||||
onClick={handleClickExportGrades}
|
||||
/>
|
||||
<ImportGradesButton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkManagementControls;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import useBulkManagementControlsData from './hooks';
|
||||
import BulkManagementControls from '.';
|
||||
|
||||
jest.mock('../ImportGradesButton', () => 'ImportGradesButton');
|
||||
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
show: true,
|
||||
handleClickExportGrades: jest.fn(),
|
||||
};
|
||||
useBulkManagementControlsData.mockReturnValue(hookProps);
|
||||
|
||||
describe('BulkManagementControls', () => {
|
||||
describe('behavior', () => {
|
||||
shallow(<BulkManagementControls />);
|
||||
expect(useBulkManagementControlsData).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot - show - network and import buttons', () => {
|
||||
expect(shallow(<BulkManagementControls />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot - empty if show is not truthy', () => {
|
||||
useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false });
|
||||
expect(shallow(<BulkManagementControls />).isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import HistoryHeader from './HistoryHeader';
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('HistoryHeader', () => {
|
||||
};
|
||||
describe('Component', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<HistoryHeader {...props} />)).toMatchSnapshot();
|
||||
expect(shallow(<HistoryHeader {...props} />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,68 +1,53 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { StrictDict } from 'utils';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import HistoryHeader from './HistoryHeader';
|
||||
|
||||
export const HistoryKeys = StrictDict({
|
||||
assignment: 'assignment',
|
||||
student: 'student',
|
||||
originalGrade: 'original-grade',
|
||||
currentGrade: 'current-grade',
|
||||
});
|
||||
|
||||
/**
|
||||
* <ModalHeaders />
|
||||
* Provides a list of HistoryHeaders for the student name, assignment,
|
||||
* original grade, and current override grade.
|
||||
*/
|
||||
export const ModalHeaders = ({
|
||||
modalState,
|
||||
originalGrade,
|
||||
currentGrade,
|
||||
}) => (
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label={<FormattedMessage {...messages.assignmentHeader} />}
|
||||
value={modalState.assignmentName}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label={<FormattedMessage {...messages.studentHeader} />}
|
||||
value={modalState.updateUserName}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label={<FormattedMessage {...messages.originalGradeHeader} />}
|
||||
value={originalGrade}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label={<FormattedMessage {...messages.currentGradeHeader} />}
|
||||
value={currentGrade}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
ModalHeaders.defaultProps = {
|
||||
currentGrade: null,
|
||||
originalGrade: null,
|
||||
};
|
||||
ModalHeaders.propTypes = {
|
||||
// redux
|
||||
currentGrade: PropTypes.number,
|
||||
originalGrade: PropTypes.number,
|
||||
modalState: PropTypes.shape({
|
||||
assignmentName: PropTypes.string.isRequired,
|
||||
updateUserName: PropTypes.string,
|
||||
}).isRequired,
|
||||
export const ModalHeaders = () => {
|
||||
const { assignmentName, updateUserName } = selectors.app.useModalData();
|
||||
const { currentGrade, originalGrade } = selectors.grades.useGradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id={HistoryKeys.assignment}
|
||||
label={formatMessage(messages.assignmentHeader)}
|
||||
value={assignmentName}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id={HistoryKeys.student}
|
||||
label={formatMessage(messages.studentHeader)}
|
||||
value={updateUserName}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id={HistoryKeys.originalGrade}
|
||||
label={formatMessage(messages.originalGradeHeader)}
|
||||
value={originalGrade}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id={HistoryKeys.currentGrade}
|
||||
label={formatMessage(messages.currentGradeHeader)}
|
||||
value={currentGrade}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
modalState: {
|
||||
assignmentName: selectors.app.modalState.assignmentName(state),
|
||||
updateUserName: selectors.app.modalState.updateUserName(state),
|
||||
},
|
||||
currentGrade: selectors.grades.gradeOverrideCurrentEarnedGradedOverride(state),
|
||||
originalGrade: selectors.grades.gradeOriginalEarnedGraded(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(ModalHeaders);
|
||||
export default ModalHeaders;
|
||||
|
||||
@@ -1,93 +1,84 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
import {
|
||||
ModalHeaders,
|
||||
mapStateToProps,
|
||||
} from './ModalHeaders';
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import HistoryHeader from './HistoryHeader';
|
||||
import ModalHeaders, { HistoryKeys } from './ModalHeaders';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('./HistoryHeader', () => 'HistoryHeader');
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
editUpdateData: jest.fn(state => ({ editUpdateData: state })),
|
||||
modalState: {
|
||||
assignmentName: jest.fn(state => ({ assignmentName: state })),
|
||||
updateUserName: jest.fn(state => ({ updateUserName: state })),
|
||||
},
|
||||
},
|
||||
grades: {
|
||||
gradeOverrideCurrentEarnedGradedOverride: jest.fn(state => ({ currentGrade: state })),
|
||||
gradeOriginalEarnedGraded: jest.fn(state => ({ originalGrade: state })),
|
||||
},
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
app: { useModalData: jest.fn() },
|
||||
grades: { useGradeData: jest.fn() },
|
||||
},
|
||||
}));
|
||||
describe('ModalHeaders', () => {
|
||||
let el;
|
||||
const props = {
|
||||
currentGrade: 2,
|
||||
originalGrade: 20,
|
||||
modalState: {
|
||||
assignmentName: 'Qwerty',
|
||||
updateUserName: 'Uiop',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Component', () => {
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
});
|
||||
describe('gradeOverrideHistoryError is and empty and open is true', () => {
|
||||
test('modal open and StatusAlert showing', () => {
|
||||
el = shallow(<ModalHeaders {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('gradeOverrideHistoryError is empty and open is false', () => {
|
||||
test('modal closed and StatusAlert closed', () => {
|
||||
el = shallow(
|
||||
<ModalHeaders {...props} open={false} gradeOverrideHistoryError="" />,
|
||||
);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
const modalData = {
|
||||
assignmentName: 'test-assignment-name',
|
||||
updateUserName: 'test-user-name',
|
||||
};
|
||||
selectors.app.useModalData.mockReturnValue(modalData);
|
||||
const gradeData = {
|
||||
currentGrade: 'test-current-grade',
|
||||
originalGrade: 'test-original-grade',
|
||||
};
|
||||
selectors.grades.useGradeData.mockReturnValue(gradeData);
|
||||
|
||||
let el;
|
||||
describe('ModalHeaders', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<ModalHeaders />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { he: 'lives in a', pineapple: 'under the sea' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
describe('modalState', () => {
|
||||
test('assignmentName from app.modalState.assignmentName', () => {
|
||||
expect(
|
||||
mapped.modalState.assignmentName,
|
||||
).toEqual(selectors.app.modalState.assignmentName(testState));
|
||||
});
|
||||
test('updateUserName from app.modalState.updateUserName', () => {
|
||||
expect(
|
||||
mapped.modalState.updateUserName,
|
||||
).toEqual(selectors.app.modalState.updateUserName(testState));
|
||||
test('assignment header', () => {
|
||||
const headerProps = el.instance.findByType(HistoryHeader)[0].props;
|
||||
expect(headerProps).toMatchObject({
|
||||
id: HistoryKeys.assignment,
|
||||
label: formatMessage(messages.assignmentHeader),
|
||||
value: modalData.assignmentName,
|
||||
});
|
||||
});
|
||||
describe('originalGrade', () => {
|
||||
test('from grades.gradeOverrideCurrentEarnedGradedOverride', () => {
|
||||
expect(mapped.currentGrade).toEqual(
|
||||
selectors.grades.gradeOverrideCurrentEarnedGradedOverride(testState),
|
||||
);
|
||||
test('student header', () => {
|
||||
const headerProps = el.instance.findByType(HistoryHeader)[1].props;
|
||||
expect(headerProps).toMatchObject({
|
||||
id: HistoryKeys.student,
|
||||
label: formatMessage(messages.studentHeader),
|
||||
value: modalData.updateUserName,
|
||||
});
|
||||
});
|
||||
describe('originalGrade', () => {
|
||||
test('from grades.gradeOriginalEarnedGrades', () => {
|
||||
expect(mapped.originalGrade).toEqual(
|
||||
selectors.grades.gradeOriginalEarnedGraded(testState),
|
||||
);
|
||||
test('originalGrade header', () => {
|
||||
const headerProps = el.instance.findByType(HistoryHeader)[2].props;
|
||||
expect(headerProps).toMatchObject({
|
||||
id: HistoryKeys.originalGrade,
|
||||
label: formatMessage(messages.originalGradeHeader),
|
||||
value: gradeData.originalGrade,
|
||||
});
|
||||
});
|
||||
test('currentGrade header', () => {
|
||||
const headerProps = el.instance.findByType(HistoryHeader)[3].props;
|
||||
expect(headerProps).toMatchObject({
|
||||
id: HistoryKeys.currentGrade,
|
||||
label: formatMessage(messages.currentGradeHeader),
|
||||
value: gradeData.currentGrade,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { getLocalizedSlash } from 'i18n';
|
||||
|
||||
/**
|
||||
* <AdjustedGradeInput />
|
||||
* Input control for adjusting the grade of a unit
|
||||
* displays an "/ ${possibleGrade} if there is one in the data model.
|
||||
*/
|
||||
export class AdjustedGradeInput extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
onChange = ({ target }) => {
|
||||
this.props.setModalState({ adjustedGradeValue: target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="adjustedGradeValue"
|
||||
value={this.props.value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
{this.props.possibleGrade && ` ${getLocalizedSlash()} ${this.props.possibleGrade}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
AdjustedGradeInput.defaultProps = {
|
||||
possibleGrade: null,
|
||||
};
|
||||
AdjustedGradeInput.propTypes = {
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
possibleGrade: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
setModalState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
possibleGrade: selectors.root.editModalPossibleGrade(state),
|
||||
value: selectors.app.modalState.adjustedGradeValue(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
setModalState: actions.app.setModalState,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AdjustedGradeInput);
|
||||
@@ -1,92 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import {
|
||||
AdjustedGradeInput,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './AdjustedGradeInput';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Form: { Control: () => 'Form.Control' },
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
root: {
|
||||
editModalPossibleGrade: jest.fn(state => ({ updateUserName: state })),
|
||||
},
|
||||
app: {
|
||||
modalState: { adjustedGradeValue: jest.fn(state => ({ adjustedGradeValue: state })) },
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setModalState: jest.fn() },
|
||||
},
|
||||
}));
|
||||
describe('AdjustedGradeInput', () => {
|
||||
let el;
|
||||
let props = {
|
||||
value: 1,
|
||||
possibleGrade: 5,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
setModalState: jest.fn(),
|
||||
};
|
||||
});
|
||||
describe('Component', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<AdjustedGradeInput {...props} />);
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('displays input control and "out of possible grade" label', () => {
|
||||
el.instance().onChange = jest.fn().mockName('this.onChange');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('onChange', () => {
|
||||
it('calls props.setModalState event target value', () => {
|
||||
const value = 42;
|
||||
el.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
adjustedGradeValue: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { like: 'no one', ever: 'was' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
describe('modalState', () => {
|
||||
test('possibleGrade from root.editModalPossibleGrade', () => {
|
||||
expect(
|
||||
mapped.possibleGrade,
|
||||
).toEqual(selectors.root.editModalPossibleGrade(testState));
|
||||
});
|
||||
test('updateUserName from app.modalState.updateUserName', () => {
|
||||
expect(
|
||||
mapped.value,
|
||||
).toEqual(selectors.app.modalState.adjustedGradeValue(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('setModalState from actions.app.setModalState', () => {
|
||||
expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AdjustedGradeInput component render snapshot 1`] = `
|
||||
<span>
|
||||
<Form.Control
|
||||
name="adjustedGradeValue"
|
||||
onChange={[MockFunction hook.onChange]}
|
||||
type="text"
|
||||
value="test-value"
|
||||
/>
|
||||
some-hint-text
|
||||
</span>
|
||||
`;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
import { getLocalizedSlash } from 'i18n/utils';
|
||||
|
||||
const useAdjustedGradeInputData = () => {
|
||||
const possibleGrade = selectors.root.useEditModalPossibleGrade();
|
||||
const value = selectors.app.useModalData().adjustedGradeValue;
|
||||
const setModalState = actions.app.useSetModalState();
|
||||
const hintText = possibleGrade && ` ${getLocalizedSlash()} ${possibleGrade}`;
|
||||
|
||||
const onChange = ({ target }) => {
|
||||
setModalState({ adjustedGradeValue: target.value });
|
||||
};
|
||||
|
||||
return {
|
||||
value,
|
||||
onChange,
|
||||
hintText,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAdjustedGradeInputData;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { getLocalizedSlash } from 'i18n/utils';
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
import useAdjustedGradeInputData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
root: {
|
||||
useEditModalPossibleGrade: jest.fn(),
|
||||
},
|
||||
app: {
|
||||
useModalData: jest.fn(),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
app: {
|
||||
useSetModalState: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('i18n/utils', () => ({ getLocalizedSlash: jest.fn() }));
|
||||
|
||||
const localizedSlash = 'localized-slash';
|
||||
getLocalizedSlash.mockReturnValue(localizedSlash);
|
||||
|
||||
const possibleGrade = 105;
|
||||
selectors.root.useEditModalPossibleGrade.mockReturnValue(possibleGrade);
|
||||
const modalData = { adjustedGradeValue: 70 };
|
||||
const setModalState = jest.fn();
|
||||
selectors.app.useModalData.mockReturnValue(modalData);
|
||||
actions.app.useSetModalState.mockReturnValue(setModalState);
|
||||
|
||||
let out;
|
||||
describe('useAdjustedGradeInputData hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useAdjustedGradeInputData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.root.useEditModalPossibleGrade).toHaveBeenCalled();
|
||||
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||
expect(actions.app.useSetModalState).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards adjusted grade value as value from modal data', () => {
|
||||
expect(out.value).toEqual(modalData.adjustedGradeValue);
|
||||
});
|
||||
describe('hintText', () => {
|
||||
it('passes an undefined value if possibleGrade is not available', () => {
|
||||
selectors.root.useEditModalPossibleGrade.mockReturnValueOnce(undefined);
|
||||
out = useAdjustedGradeInputData();
|
||||
expect(out.hintText).toEqual(undefined);
|
||||
});
|
||||
it('passes localized slash and possible grade if available', () => {
|
||||
expect(out.hintText).toEqual(` ${localizedSlash} ${possibleGrade}`);
|
||||
});
|
||||
});
|
||||
describe('onChange', () => {
|
||||
it('sets modal state with event target value', () => {
|
||||
const testValue = 'test-value';
|
||||
out.onChange({ target: { value: testValue } });
|
||||
expect(setModalState).toHaveBeenCalledWith({ adjustedGradeValue: testValue });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import useAdjustedGradeInputData from './hooks';
|
||||
|
||||
/**
|
||||
* <AdjustedGradeInput />
|
||||
* Input control for adjusting the grade of a unit
|
||||
* displays an "/ ${possibleGrade} if there is one in the data model.
|
||||
*/
|
||||
export const AdjustedGradeInput = () => {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
hintText,
|
||||
} = useAdjustedGradeInputData();
|
||||
return (
|
||||
<span>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="adjustedGradeValue"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{hintText}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
AdjustedGradeInput.propTypes = {};
|
||||
|
||||
export default AdjustedGradeInput;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import useAdjustedGradeInputData from './hooks';
|
||||
import AdjustedGradeInput from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
hintText: 'some-hint-text',
|
||||
onChange: jest.fn().mockName('hook.onChange'),
|
||||
value: 'test-value',
|
||||
};
|
||||
useAdjustedGradeInputData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('AdjustedGradeInput component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<AdjustedGradeInput />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hook data', () => {
|
||||
expect(useAdjustedGradeInputData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
const control = el.instance.findByType(Form.Control)[0];
|
||||
expect(control.props.value).toEqual(hookProps.value);
|
||||
expect(control.props.onChange).toEqual(hookProps.onChange);
|
||||
expect(el.instance.children[1].el).toContain(hookProps.hintText);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
|
||||
/**
|
||||
* <ReasonInput />
|
||||
* Input control for the "reason for change" field in the Edit modal.
|
||||
*/
|
||||
export class ReasonInput extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.ref = React.createRef();
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.ref.current.focus();
|
||||
}
|
||||
|
||||
onChange = (event) => {
|
||||
this.props.setModalState({ reasonForChange: event.target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="reasonForChange"
|
||||
value={this.props.value}
|
||||
onChange={this.onChange}
|
||||
ref={this.ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
ReasonInput.propTypes = {
|
||||
// redux
|
||||
setModalState: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
value: selectors.app.modalState.reasonForChange(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
setModalState: actions.app.setModalState,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ReasonInput);
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import {
|
||||
ReasonInput,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './ReasonInput';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Form: { Control: () => 'Form.Control' },
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
modalState: { reasonForChange: jest.fn(state => ({ reasonForChange: state })) },
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setModalState: jest.fn() },
|
||||
},
|
||||
}));
|
||||
describe('ReasonInput', () => {
|
||||
let el;
|
||||
let props = {
|
||||
value: 'did not answer the question',
|
||||
};
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
setModalState: jest.fn(),
|
||||
};
|
||||
});
|
||||
describe('Component', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<ReasonInput {...props} />, { disableLifecycleMethods: true });
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('displays reason for change input control', () => {
|
||||
el.instance().onChange = jest.fn().mockName('this.onChange');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('onChange', () => {
|
||||
it('calls props.setModalState event target value', () => {
|
||||
const value = 42;
|
||||
el.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
reasonForChange: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('componentDidMount', () => {
|
||||
it('focuses the input ref', () => {
|
||||
const focus = jest.fn();
|
||||
expect(el.instance().ref).toEqual({ current: null });
|
||||
el.instance().ref.current = { focus };
|
||||
el.instance().componentDidMount();
|
||||
expect(el.instance().ref.current.focus).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { to: { catchThem: 'my real test', trainThem: 'my cause!' } };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
describe('modalState', () => {
|
||||
test('value from app.modalState.reasonForChange', () => {
|
||||
expect(mapped.value).toEqual(selectors.app.modalState.reasonForChange(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('setModalState from actions.app.setModalState', () => {
|
||||
expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReasonInput component render snapshot 1`] = `
|
||||
<Form.Control
|
||||
data-testid="reason-input-control"
|
||||
name="reasonForChange"
|
||||
onChange={[MockFunction hook.onChange]}
|
||||
type="text"
|
||||
value="test-value"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
const useReasonInputData = () => {
|
||||
const ref = React.useRef();
|
||||
const { reasonForChange } = selectors.app.useModalData();
|
||||
const setModalState = actions.app.useSetModalState();
|
||||
|
||||
React.useEffect(() => {
|
||||
ref.current.focus();
|
||||
}, [ref]);
|
||||
|
||||
const onChange = (event) => {
|
||||
setModalState({ reasonForChange: event.target.value });
|
||||
};
|
||||
|
||||
return {
|
||||
value: reasonForChange,
|
||||
onChange,
|
||||
ref,
|
||||
};
|
||||
};
|
||||
|
||||
export default useReasonInputData;
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
import useReasonInputData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
useModalData: jest.fn(),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
app: {
|
||||
useSetModalState: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const modalData = { reasonForChange: 'test-reason-for-change' };
|
||||
const setModalState = jest.fn();
|
||||
selectors.app.useModalData.mockReturnValue(modalData);
|
||||
actions.app.useSetModalState.mockReturnValue(setModalState);
|
||||
|
||||
const ref = { current: { focus: jest.fn() }, useRef: true };
|
||||
React.useRef.mockReturnValue(ref);
|
||||
|
||||
let out;
|
||||
describe('useReasonInputData hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useReasonInputData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes ref', () => {
|
||||
expect(React.useRef).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||
expect(actions.app.useSetModalState).toHaveBeenCalled();
|
||||
});
|
||||
it('focuses ref on load', () => {
|
||||
const [[cb, prereqs]] = React.useEffect.mock.calls;
|
||||
expect(prereqs).toEqual([ref]);
|
||||
cb();
|
||||
expect(ref.current.focus).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards reasonForChange as value from modal data', () => {
|
||||
expect(out.value).toEqual(modalData.reasonForChange);
|
||||
});
|
||||
it('forwards ref', () => {
|
||||
expect(out.ref).toEqual(ref);
|
||||
});
|
||||
describe('onChange', () => {
|
||||
it('sets modal state with event target value', () => {
|
||||
const testValue = 'test-value';
|
||||
out.onChange({ target: { value: testValue } });
|
||||
expect(setModalState).toHaveBeenCalledWith({ reasonForChange: testValue });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import useReasonInputData from './hooks';
|
||||
|
||||
export const controlTestId = 'reason-input-control';
|
||||
|
||||
/**
|
||||
* <ReasonInput />
|
||||
* Input control for the "reason for change" field in the Edit modal.
|
||||
*/
|
||||
export const ReasonInput = () => {
|
||||
const { ref, value, onChange } = useReasonInputData();
|
||||
return (
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="reasonForChange"
|
||||
data-testid={controlTestId}
|
||||
{...{ value, onChange, ref }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ReasonInput.propTypes = {};
|
||||
|
||||
export default ReasonInput;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import useReasonInputData from './hooks';
|
||||
import ReasonInput from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
ref: 'reason-input-ref',
|
||||
onChange: jest.fn().mockName('hook.onChange'),
|
||||
value: 'test-value',
|
||||
};
|
||||
useReasonInputData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('ReasonInput component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<ReasonInput />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hook data', () => {
|
||||
expect(useReasonInputData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
const control = el.instance.findByType(Form.Control)[0];
|
||||
expect(control.props.value).toEqual(hookProps.value);
|
||||
expect(control.props.onChange).toEqual(hookProps.onChange);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import useReasonInputData from './hooks';
|
||||
import ReasonInput, { controlTestId } from '.';
|
||||
|
||||
jest.unmock('react');
|
||||
jest.unmock('@edx/paragon');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const focus = jest.fn();
|
||||
const props = {
|
||||
value: 'test-value',
|
||||
onChange: jest.fn(),
|
||||
ref: { current: { focus }, useRef: jest.fn() },
|
||||
};
|
||||
useReasonInputData.mockReturnValue(props);
|
||||
|
||||
let el;
|
||||
describe('ReasonInput ref', () => {
|
||||
it('loads ref from hook', () => {
|
||||
el = render(<ReasonInput />);
|
||||
const control = el.getByTestId(controlTestId);
|
||||
expect(control).toEqual(props.ref.current);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AdjustedGradeInput Component snapshots displays input control and "out of possible grade" label 1`] = `
|
||||
<span>
|
||||
<Control
|
||||
name="adjustedGradeValue"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
type="text"
|
||||
value={1}
|
||||
/>
|
||||
/ 5
|
||||
</span>
|
||||
`;
|
||||
@@ -1,10 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReasonInput Component snapshots displays reason for change input control 1`] = `
|
||||
<Control
|
||||
name="reasonForChange"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
type="text"
|
||||
value="did not answer the question"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverrideTable component render snapshot 1`] = `
|
||||
<DataTable
|
||||
columns="test-columns"
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"test": "data",
|
||||
},
|
||||
Object {
|
||||
"andOther": "test-data",
|
||||
},
|
||||
Object {
|
||||
"adjustedGrade": <AdjustedGradeInput />,
|
||||
"date": Object {
|
||||
"formatted": 2000-01-01T00:00:00.000Z,
|
||||
},
|
||||
"reason": <ReasonInput />,
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
@@ -1,64 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = `
|
||||
<DataTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Date"
|
||||
description="Edit Modal Override Table Date column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.dateHeader"
|
||||
/>,
|
||||
"accessor": "date",
|
||||
},
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Grader"
|
||||
description="Edit Modal Override Table Grader column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.graderHeader"
|
||||
/>,
|
||||
"accessor": "grader",
|
||||
},
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Reason"
|
||||
description="Edit Modal Override Table Reason column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.reasonHeader"
|
||||
/>,
|
||||
"accessor": "reason",
|
||||
},
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Adjusted grade"
|
||||
description="Edit Modal Override Table Adjusted grade column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader"
|
||||
/>,
|
||||
"accessor": "adjustedGrade",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"adjustedGrade": 0,
|
||||
"date": "yesterday",
|
||||
"grader": "me",
|
||||
"reason": "you ate my sandwich",
|
||||
},
|
||||
Object {
|
||||
"adjustedGrade": 20,
|
||||
"date": "today",
|
||||
"grader": "me",
|
||||
"reason": "you brought me a new sandwich",
|
||||
},
|
||||
Object {
|
||||
"adjustedGrade": <AdjustedGradeInput />,
|
||||
"date": "todaaaaaay",
|
||||
"reason": <ReasonInput />,
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
26
src/components/GradesView/EditModal/OverrideTable/hooks.js
Normal file
26
src/components/GradesView/EditModal/OverrideTable/hooks.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const useOverrideTableData = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const hide = selectors.grades.useHasOverrideErrors();
|
||||
const gradeOverrides = selectors.grades.useGradeData().gradeOverrideHistoryResults;
|
||||
const tableProps = {};
|
||||
if (!hide) {
|
||||
tableProps.columns = [
|
||||
{ Header: formatMessage(messages.dateHeader), accessor: columns.date },
|
||||
{ Header: formatMessage(messages.graderHeader), accessor: columns.grader },
|
||||
{ Header: formatMessage(messages.reasonHeader), accessor: columns.reason },
|
||||
{ Header: formatMessage(messages.adjustedGradeHeader), accessor: columns.adjustedGrade },
|
||||
];
|
||||
tableProps.data = gradeOverrides;
|
||||
}
|
||||
return { hide, ...tableProps };
|
||||
};
|
||||
|
||||
export default useOverrideTableData;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
import useOverrideTableData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
grades: {
|
||||
useHasOverrideErrors: jest.fn(),
|
||||
useGradeData: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
selectors.grades.useHasOverrideErrors.mockReturnValue(false);
|
||||
const gradeOverrides = ['some', 'override', 'data'];
|
||||
const gradeData = { gradeOverrideHistoryResults: gradeOverrides };
|
||||
selectors.grades.useGradeData.mockReturnValue(gradeData);
|
||||
|
||||
let out;
|
||||
describe('useOverrideTableData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useOverrideTableData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.grades.useHasOverrideErrors).toHaveBeenCalled();
|
||||
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('no errors', () => {
|
||||
test('hide is false', () => {
|
||||
expect(out.hide).toEqual(false);
|
||||
});
|
||||
describe('columns', () => {
|
||||
test('date column', () => {
|
||||
const { Header, accessor } = out.columns[0];
|
||||
expect(Header).toEqual(formatMessage(messages.dateHeader));
|
||||
expect(accessor).toEqual(columns.date);
|
||||
});
|
||||
test('grader column', () => {
|
||||
const { Header, accessor } = out.columns[1];
|
||||
expect(Header).toEqual(formatMessage(messages.graderHeader));
|
||||
expect(accessor).toEqual(columns.grader);
|
||||
});
|
||||
test('reason column', () => {
|
||||
const { Header, accessor } = out.columns[2];
|
||||
expect(Header).toEqual(formatMessage(messages.reasonHeader));
|
||||
expect(accessor).toEqual(columns.reason);
|
||||
});
|
||||
test('adjustedGrade column', () => {
|
||||
const { Header, accessor } = out.columns[3];
|
||||
expect(Header).toEqual(formatMessage(messages.adjustedGradeHeader));
|
||||
expect(accessor).toEqual(columns.adjustedGrade);
|
||||
});
|
||||
});
|
||||
test('data passed from grade data', () => {
|
||||
expect(out.data).toEqual(gradeOverrides);
|
||||
});
|
||||
});
|
||||
describe('with errors', () => {
|
||||
it('returns hide true and no other fields', () => {
|
||||
selectors.grades.useHasOverrideErrors.mockReturnValue(true);
|
||||
out = useOverrideTableData();
|
||||
expect(out).toEqual({ hide: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,73 +1,40 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
import selectors from 'data/selectors';
|
||||
import { formatDateForDisplay } from 'utils';
|
||||
|
||||
import messages from './messages';
|
||||
import ReasonInput from './ReasonInput';
|
||||
import AdjustedGradeInput from './AdjustedGradeInput';
|
||||
import useOverrideTableData from './hooks';
|
||||
|
||||
/**
|
||||
* <OverrideTable />
|
||||
* Table containing previous grade override entries, and an "edit" row
|
||||
* with todays date, an AdjustedGradeInput and a ReasonInput
|
||||
*/
|
||||
export const OverrideTable = ({
|
||||
hide,
|
||||
gradeOverrides,
|
||||
todaysDate,
|
||||
}) => {
|
||||
if (hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export const OverrideTable = () => {
|
||||
const { hide, columns, data } = useOverrideTableData();
|
||||
|
||||
if (hide) { return null; }
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={[
|
||||
{ Header: <FormattedMessage {...messages.dateHeader} />, accessor: columns.date },
|
||||
{ Header: <FormattedMessage {...messages.graderHeader} />, accessor: columns.grader },
|
||||
{ Header: <FormattedMessage {...messages.reasonHeader} />, accessor: columns.reason },
|
||||
{
|
||||
Header: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
accessor: columns.adjustedGrade,
|
||||
},
|
||||
]}
|
||||
columns={columns}
|
||||
data={[
|
||||
...gradeOverrides,
|
||||
...data,
|
||||
{
|
||||
adjustedGrade: <AdjustedGradeInput />,
|
||||
date: todaysDate,
|
||||
date: formatDateForDisplay(new Date()),
|
||||
reason: <ReasonInput />,
|
||||
},
|
||||
]}
|
||||
itemCount={gradeOverrides.length}
|
||||
itemCount={data.length}
|
||||
/>
|
||||
);
|
||||
};
|
||||
OverrideTable.defaultProps = {
|
||||
gradeOverrides: [],
|
||||
};
|
||||
OverrideTable.propTypes = {
|
||||
// redux
|
||||
gradeOverrides: PropTypes.arrayOf(PropTypes.shape({
|
||||
date: PropTypes.string,
|
||||
grader: PropTypes.string,
|
||||
reason: PropTypes.string,
|
||||
adjustedGrade: PropTypes.number,
|
||||
})),
|
||||
hide: PropTypes.bool.isRequired,
|
||||
todaysDate: PropTypes.string.isRequired,
|
||||
};
|
||||
OverrideTable.propTypes = {};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
hide: selectors.grades.hasOverrideErrors(state),
|
||||
gradeOverrides: selectors.grades.gradeOverrides(state),
|
||||
todaysDate: selectors.app.modalState.todaysDate(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(OverrideTable);
|
||||
export default OverrideTable;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import { formatDateForDisplay } from 'utils';
|
||||
|
||||
import AdjustedGradeInput from './AdjustedGradeInput';
|
||||
import ReasonInput from './ReasonInput';
|
||||
import useOverrideTableData from './hooks';
|
||||
import OverrideTable from '.';
|
||||
|
||||
jest.mock('utils', () => ({
|
||||
formatDateForDisplay: (date) => ({ formatted: date }),
|
||||
}));
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
|
||||
jest.mock('./ReasonInput', () => 'ReasonInput');
|
||||
|
||||
const hookProps = {
|
||||
hide: false,
|
||||
data: [
|
||||
{ test: 'data' },
|
||||
{ andOther: 'test-data' },
|
||||
],
|
||||
columns: 'test-columns',
|
||||
};
|
||||
useOverrideTableData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('OverrideTable component', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.clearAllMocks()
|
||||
.useFakeTimers('modern')
|
||||
.setSystemTime(new Date('2000-01-01').getTime());
|
||||
el = shallow(<OverrideTable />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hook data', () => {
|
||||
expect(useOverrideTableData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('null render if hide', () => {
|
||||
useOverrideTableData.mockReturnValueOnce({ ...hookProps, hide: true });
|
||||
el = shallow(<OverrideTable />);
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
const table = el.instance.findByType(DataTable)[0];
|
||||
expect(table.props.columns).toEqual(hookProps.columns);
|
||||
const data = [...table.props.data];
|
||||
const inputRow = data.pop();
|
||||
const formattedDate = formatDateForDisplay(new Date());
|
||||
expect(data).toEqual(hookProps.data);
|
||||
expect(inputRow).toMatchObject({
|
||||
adjustedGrade: <AdjustedGradeInput />,
|
||||
date: formattedDate,
|
||||
reason: <ReasonInput />,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import {
|
||||
OverrideTable,
|
||||
mapStateToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
||||
jest.mock('./ReasonInput', () => 'ReasonInput');
|
||||
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
modalState: {
|
||||
todaysDate: jest.fn(state => ({ todaysDate: state })),
|
||||
},
|
||||
},
|
||||
grades: {
|
||||
hasOverrideErrors: jest.fn(state => ({ hasOverrideErrors: state })),
|
||||
gradeOverrides: jest.fn(state => ({ gradeOverrides: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('OverrideTable', () => {
|
||||
const props = {
|
||||
gradeOverrides: [
|
||||
{
|
||||
date: 'yesterday',
|
||||
grader: 'me',
|
||||
reason: 'you ate my sandwich',
|
||||
adjustedGrade: 0,
|
||||
},
|
||||
{
|
||||
date: 'today',
|
||||
grader: 'me',
|
||||
reason: 'you brought me a new sandwich',
|
||||
adjustedGrade: 20,
|
||||
},
|
||||
],
|
||||
hide: false,
|
||||
todaysDate: 'todaaaaaay',
|
||||
};
|
||||
|
||||
describe('Component', () => {
|
||||
describe('snapshots', () => {
|
||||
it('returns null if hide is true', () => {
|
||||
expect(shallow(<OverrideTable {...props} hide />)).toEqual({});
|
||||
});
|
||||
describe('basic snapshot', () => {
|
||||
test('shows a row for each entry and one editable row', () => {
|
||||
expect(shallow(<OverrideTable {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { I: 'wanna', be: 'the', very: 'best' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
describe('modalState', () => {
|
||||
test('hide from grades.hasOverrideErrors', () => {
|
||||
expect(mapped.hide).toEqual(selectors.grades.hasOverrideErrors(testState));
|
||||
});
|
||||
test('gradeOverrides from grades.gradeOverrides', () => {
|
||||
expect(mapped.gradeOverrides).toEqual(selectors.grades.gradeOverrides(testState));
|
||||
});
|
||||
test('todaysData from app.modalState.todaysDate', () => {
|
||||
expect(mapped.todaysDate).toEqual(selectors.app.modalState.todaysDate(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,99 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
||||
exports[`ModalHeaders render snapshot 1`] = `
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment"
|
||||
description="Edit Modal Assignment header"
|
||||
id="gradebook.GradesView.EditModal.headers.assignment"
|
||||
/>
|
||||
}
|
||||
value="Qwerty"
|
||||
label="Assignment"
|
||||
value="test-assignment-name"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Student"
|
||||
description="Edit Modal Student header"
|
||||
id="gradebook.GradesView.EditModal.headers.student"
|
||||
/>
|
||||
}
|
||||
value="Uiop"
|
||||
label="Student"
|
||||
value="test-user-name"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Original Grade"
|
||||
description="Edit Modal Original Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.originalGrade"
|
||||
/>
|
||||
}
|
||||
value={20}
|
||||
label="Original Grade"
|
||||
value="test-original-grade"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Current Grade"
|
||||
description="Edit Modal Current Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.currentGrade"
|
||||
/>
|
||||
}
|
||||
value={2}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
||||
<div>
|
||||
<HistoryHeader
|
||||
id="assignment"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Assignment"
|
||||
description="Edit Modal Assignment header"
|
||||
id="gradebook.GradesView.EditModal.headers.assignment"
|
||||
/>
|
||||
}
|
||||
value="Qwerty"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="student"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Student"
|
||||
description="Edit Modal Student header"
|
||||
id="gradebook.GradesView.EditModal.headers.student"
|
||||
/>
|
||||
}
|
||||
value="Uiop"
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="original-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Original Grade"
|
||||
description="Edit Modal Original Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.originalGrade"
|
||||
/>
|
||||
}
|
||||
value={20}
|
||||
/>
|
||||
<HistoryHeader
|
||||
id="current-grade"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Current Grade"
|
||||
description="Edit Modal Current Grade header"
|
||||
id="gradebook.GradesView.EditModal.headers.currentGrade"
|
||||
/>
|
||||
}
|
||||
value={2}
|
||||
label="Current Grade"
|
||||
value="test-current-grade"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditModal component render with error snapshot 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={true}
|
||||
isFullscreenOnMobile={true}
|
||||
isOpen="test-is-open"
|
||||
onClose={[MockFunction hooks.onClose]}
|
||||
size="xl"
|
||||
title="Edit Grades"
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={true}
|
||||
variant="danger"
|
||||
>
|
||||
test-error
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
Showing most recent actions (max 5). To see more, please contact support
|
||||
</div>
|
||||
<div>
|
||||
Note: Once you save, your changes will be visible to students.
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton
|
||||
variant="tertiary"
|
||||
>
|
||||
Cancel
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
Save Grades
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
`;
|
||||
|
||||
exports[`EditModal component render without error snapshot 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={true}
|
||||
isFullscreenOnMobile={true}
|
||||
isOpen="test-is-open"
|
||||
onClose={[MockFunction hooks.onClose]}
|
||||
size="xl"
|
||||
title="Edit Grades"
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={false}
|
||||
variant="danger"
|
||||
/>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
Showing most recent actions (max 5). To see more, please contact support
|
||||
</div>
|
||||
<div>
|
||||
Note: Once you save, your changes will be visible to students.
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton
|
||||
variant="tertiary"
|
||||
>
|
||||
Cancel
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
Save Grades
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
`;
|
||||
@@ -1,125 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditModal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={true}
|
||||
isFullscreenOnMobile={true}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
size="xl"
|
||||
title="Edit Grades"
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={true}
|
||||
variant="danger"
|
||||
>
|
||||
Weve been trying to contact you regarding...
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
|
||||
description="Edit Modal visibility hint message"
|
||||
id="gradebook.GradesView.EditModal.contactSupport"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Note: Once you save, your changes will be visible to students."
|
||||
description="Edit Modal saved changes effect hint"
|
||||
id="gradebook.GradesView.EditModal.saveVisibility"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Grades"
|
||||
description="Edit Modal Save button label"
|
||||
id="gradebook.GradesView.EditModal.saveGrade"
|
||||
/>
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
`;
|
||||
|
||||
exports[`EditModal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
||||
<ModalDialog
|
||||
hasCloseButton={true}
|
||||
isFullscreenOnMobile={true}
|
||||
isOpen={false}
|
||||
onClose={[MockFunction this.closeAssignmentModal]}
|
||||
size="xl"
|
||||
title="Edit Grades"
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={false}
|
||||
variant="danger"
|
||||
>
|
||||
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
|
||||
description="Edit Modal visibility hint message"
|
||||
id="gradebook.GradesView.EditModal.contactSupport"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Note: Once you save, your changes will be visible to students."
|
||||
description="Edit Modal saved changes effect hint"
|
||||
id="gradebook.GradesView.EditModal.saveVisibility"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Edit Modal close button text"
|
||||
id="gradebook.GradesView.EditModal.closeText"
|
||||
/>
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save Grades"
|
||||
description="Edit Modal Save button label"
|
||||
id="gradebook.GradesView.EditModal.saveGrade"
|
||||
/>
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
`;
|
||||
29
src/components/GradesView/EditModal/hooks.js
Normal file
29
src/components/GradesView/EditModal/hooks.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
export const useEditModalData = () => {
|
||||
const error = selectors.grades.useGradeData().gradeOverrideHistoryError;
|
||||
const isOpen = selectors.app.useModalData().open;
|
||||
const closeModal = actions.app.useCloseModal();
|
||||
const doneViewingAssignment = actions.grades.useDoneViewingAssignment();
|
||||
const updateGrades = thunkActions.grades.useUpdateGrades();
|
||||
|
||||
const onClose = () => {
|
||||
doneViewingAssignment();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const handleAdjustedGradeClick = () => {
|
||||
updateGrades();
|
||||
doneViewingAssignment();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return {
|
||||
onClose,
|
||||
error,
|
||||
handleAdjustedGradeClick,
|
||||
isOpen,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEditModalData;
|
||||
68
src/components/GradesView/EditModal/hooks.test.js
Normal file
68
src/components/GradesView/EditModal/hooks.test.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
import useEditModalData from './hooks';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
actions: {
|
||||
app: { useCloseModal: jest.fn() },
|
||||
grades: { useDoneViewingAssignment: jest.fn() },
|
||||
},
|
||||
selectors: {
|
||||
app: { useModalData: jest.fn() },
|
||||
grades: { useGradeData: jest.fn() },
|
||||
},
|
||||
thunkActions: {
|
||||
grades: { useUpdateGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
const closeModal = jest.fn();
|
||||
const doneViewingAssignment = jest.fn();
|
||||
const updateGrades = jest.fn();
|
||||
actions.app.useCloseModal.mockReturnValue(closeModal);
|
||||
actions.grades.useDoneViewingAssignment.mockReturnValue(doneViewingAssignment);
|
||||
thunkActions.grades.useUpdateGrades.mockReturnValue(updateGrades);
|
||||
|
||||
const gradeData = { gradeOverridHistoryError: 'test-error' };
|
||||
const modalData = { open: true };
|
||||
selectors.app.useModalData.mockReturnValue(modalData);
|
||||
selectors.grades.useGradeData.mockReturnValue(gradeData);
|
||||
|
||||
let out;
|
||||
describe('useEditModalData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useEditModalData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||
expect(actions.app.useCloseModal).toHaveBeenCalled();
|
||||
expect(actions.grades.useDoneViewingAssignment).toHaveBeenCalled();
|
||||
expect(thunkActions.grades.useUpdateGrades).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards error from gradeData.gradeOverrideHistoryError', () => {
|
||||
expect(out.error).toEqual(gradeData.gradeOverrideHistoryError);
|
||||
});
|
||||
it('forwards isOpen from modalData.open', () => {
|
||||
expect(out.isOpen).toEqual(modalData.open);
|
||||
});
|
||||
describe('handleAdjustedGradeClick', () => {
|
||||
it('updates grades, calls doneViewingAssignment and closeModal', () => {
|
||||
out.handleAdjustedGradeClick();
|
||||
expect(updateGrades).toHaveBeenCalled();
|
||||
expect(doneViewingAssignment).toHaveBeenCalled();
|
||||
expect(closeModal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
test('onClose calls doneViewingAssignment and closeModal', () => {
|
||||
out.onClose();
|
||||
expect(doneViewingAssignment).toHaveBeenCalled();
|
||||
expect(closeModal).toHaveBeenCalled();
|
||||
expect(updateGrades).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,4 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -9,15 +6,12 @@ import {
|
||||
ModalDialog,
|
||||
ActionRow,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from './messages';
|
||||
import OverrideTable from './OverrideTable';
|
||||
import ModalHeaders from './ModalHeaders';
|
||||
import useEditModalData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <EditModal />
|
||||
@@ -28,87 +22,48 @@ import ModalHeaders from './ModalHeaders';
|
||||
* adjusting the grade.
|
||||
* (also provides a close button that clears the modal state)
|
||||
*/
|
||||
export class EditModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.closeAssignmentModal = this.closeAssignmentModal.bind(this);
|
||||
this.handleAdjustedGradeClick = this.handleAdjustedGradeClick.bind(this);
|
||||
}
|
||||
export const EditModal = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
onClose,
|
||||
error,
|
||||
handleAdjustedGradeClick,
|
||||
isOpen,
|
||||
} = useEditModalData();
|
||||
|
||||
closeAssignmentModal() {
|
||||
this.props.doneViewingAssignment();
|
||||
this.props.closeModal();
|
||||
}
|
||||
return (
|
||||
<ModalDialog
|
||||
title={formatMessage(messages.title)}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="xl"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert variant="danger" show={!!error} dismissible={false}>
|
||||
{error}
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>{formatMessage(messages.visibility)}</div>
|
||||
<div>{formatMessage(messages.saveVisibility)}</div>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
|
||||
handleAdjustedGradeClick() {
|
||||
this.props.updateGrades();
|
||||
this.closeAssignmentModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ModalDialog
|
||||
title={this.props.intl.formatMessage(messages.title)}
|
||||
isOpen={this.props.open}
|
||||
onClose={this.closeAssignmentModal}
|
||||
size="xl"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Body>
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<Alert
|
||||
variant="danger"
|
||||
show={!!this.props.gradeOverrideHistoryError}
|
||||
dismissible={false}
|
||||
>
|
||||
{this.props.gradeOverrideHistoryError}
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div><FormattedMessage {...messages.visibility} /></div>
|
||||
<div><FormattedMessage {...messages.saveVisibility} /></div>
|
||||
</div>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
<FormattedMessage {...messages.closeText} />
|
||||
</ModalDialog.CloseButton>
|
||||
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
|
||||
<FormattedMessage {...messages.saveGrade} />
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditModal.defaultProps = {
|
||||
gradeOverrideHistoryError: '',
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{formatMessage(messages.closeText)}
|
||||
</ModalDialog.CloseButton>
|
||||
<Button variant="primary" onClick={handleAdjustedGradeClick}>
|
||||
{formatMessage(messages.saveGrade)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
EditModal.propTypes = {
|
||||
// redux
|
||||
gradeOverrideHistoryError: PropTypes.string,
|
||||
open: PropTypes.bool.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
doneViewingAssignment: PropTypes.func.isRequired,
|
||||
updateGrades: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
gradeOverrideHistoryError: selectors.grades.gradeOverrideHistoryError(state),
|
||||
open: selectors.app.modalState.open(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
closeModal: actions.app.closeModal,
|
||||
doneViewingAssignment: actions.grades.doneViewingAssignment,
|
||||
updateGrades: thunkActions.grades.updateGrades,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(EditModal));
|
||||
export default EditModal;
|
||||
|
||||
128
src/components/GradesView/EditModal/index.test.jsx
Normal file
128
src/components/GradesView/EditModal/index.test.jsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import {
|
||||
ActionRow,
|
||||
ModalDialog,
|
||||
} from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import ModalHeaders from './ModalHeaders';
|
||||
import OverrideTable from './OverrideTable';
|
||||
import useEditModalData from './hooks';
|
||||
import EditModal from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
jest.mock('./ModalHeaders', () => 'ModalHeaders');
|
||||
jest.mock('./OverrideTable', () => 'OverrideTable');
|
||||
|
||||
const hookProps = {
|
||||
onClose: jest.fn().mockName('hooks.onClose'),
|
||||
error: 'test-error',
|
||||
handleAdjustedGradeClick: jest.fn().mockName('hooks.handleAdjustedGradeClick'),
|
||||
isOpen: 'test-is-open',
|
||||
};
|
||||
useEditModalData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('EditModal component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<EditModal />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes component hooks', () => {
|
||||
expect(useEditModalData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('modal props', () => {
|
||||
const modalProps = el.instance.findByType(ModalDialog)[0].props;
|
||||
expect(modalProps.title).toEqual(formatMessage(messages.title));
|
||||
expect(modalProps.isOpen).toEqual(hookProps.isOpen);
|
||||
expect(modalProps.onClose).toEqual(hookProps.onClose);
|
||||
});
|
||||
const loadBody = () => {
|
||||
const body = el.instance.findByType(ModalDialog)[0].children[0];
|
||||
const { children } = body.children[0];
|
||||
return { body, children };
|
||||
};
|
||||
const testBody = () => {
|
||||
test('type', () => {
|
||||
const { body } = loadBody();
|
||||
expect(body.type).toEqual('ModalDialog.Body');
|
||||
});
|
||||
test('headers row', () => {
|
||||
const { children } = loadBody();
|
||||
expect(children[0]).toMatchObject(shallow(<ModalHeaders />));
|
||||
});
|
||||
test('table row', () => {
|
||||
const { children } = loadBody();
|
||||
expect(children[2]).toMatchObject(shallow(<OverrideTable />));
|
||||
});
|
||||
test('messages', () => {
|
||||
const { children } = loadBody();
|
||||
expect(children[3].children[0].el).toEqual(formatMessage(messages.visibility));
|
||||
expect(children[4].children[0].el).toEqual(formatMessage(messages.saveVisibility));
|
||||
});
|
||||
};
|
||||
const testFooter = () => {
|
||||
let footer;
|
||||
beforeEach(() => {
|
||||
footer = el.instance.findByType(ModalDialog)[0].children;
|
||||
});
|
||||
test('type', () => {
|
||||
expect(footer[1].type).toEqual('ModalDialog.Footer');
|
||||
});
|
||||
test('contains action row', () => {
|
||||
expect(footer[1].children[0].type).toEqual('ActionRow');
|
||||
});
|
||||
test('close button', () => {
|
||||
const button = footer[1].findByType(ActionRow)[0].children[0];
|
||||
expect(button.children[0].el).toEqual(formatMessage(messages.closeText));
|
||||
expect(button.type).toEqual('ModalDialog.CloseButton');
|
||||
});
|
||||
test('adjusted grade button', () => {
|
||||
const button = footer[1].findByType(ActionRow)[0].children[1];
|
||||
expect(button.children[0].el).toEqual(formatMessage(messages.saveGrade));
|
||||
expect(button.type).toEqual('Button');
|
||||
expect(button.props.onClick).toEqual(hookProps.handleAdjustedGradeClick);
|
||||
});
|
||||
};
|
||||
describe('without error', () => {
|
||||
beforeEach(() => {
|
||||
useEditModalData.mockReturnValueOnce({ ...hookProps, error: undefined });
|
||||
el = shallow(<EditModal />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
testBody();
|
||||
testFooter();
|
||||
test('alert row', () => {
|
||||
const alert = loadBody().children[1];
|
||||
expect(alert.type).toEqual('Alert');
|
||||
expect(alert.props.show).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('with error', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
testBody();
|
||||
test('alert row', () => {
|
||||
const alert = loadBody().children[1];
|
||||
expect(alert.type).toEqual('Alert');
|
||||
expect(alert.props.show).toEqual(true);
|
||||
expect(alert.children[0].el).toEqual(hookProps.error);
|
||||
});
|
||||
testFooter();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import {
|
||||
EditModal,
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
}
|
||||
from '.';
|
||||
|
||||
jest.mock('./OverrideTable', () => 'OverrideTable');
|
||||
jest.mock('./ModalHeaders', () => 'ModalHeaders');
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { closeModal: jest.fn() },
|
||||
grades: { doneViewingAssignment: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: { updateGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
modalState: {
|
||||
open: jest.fn(state => ({ isModalOpen: state })),
|
||||
},
|
||||
},
|
||||
grades: {
|
||||
gradeOverrideHistoryError: jest.fn(state => ({ overrideHistoryError: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
describe('EditModal', () => {
|
||||
let props;
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
gradeOverrideHistoryError: 'Weve been trying to contact you regarding...',
|
||||
open: true,
|
||||
closeModal: jest.fn(),
|
||||
doneViewingAssignment: jest.fn(),
|
||||
updateGrades: jest.fn(),
|
||||
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
};
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
describe('behavior', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<EditModal {...props} />);
|
||||
});
|
||||
describe('closeAssignmentModal', () => {
|
||||
it('calls props.doneViewingAssignment and props.closeModal', () => {
|
||||
el.instance().closeAssignmentModal();
|
||||
expect(props.doneViewingAssignment).toHaveBeenCalledWith();
|
||||
expect(props.closeModal).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('handleAdjustedGradeClick', () => {
|
||||
it('calls props.updateGardes and this.closeAssignmentModal', () => {
|
||||
el.instance().closeAssignmentModal = jest.fn();
|
||||
el.instance().handleAdjustedGradeClick();
|
||||
expect(props.updateGrades).toHaveBeenCalledWith();
|
||||
expect(el.instance().closeAssignmentModal).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<EditModal {...props} />);
|
||||
el.instance().closeAssignmentModal = jest.fn().mockName('this.closeAssignmentModal');
|
||||
el.instance().handleAdjustedGradeClick = jest.fn().mockName(
|
||||
'this.handleAdjustedGradeClick',
|
||||
);
|
||||
});
|
||||
describe('gradeOverrideHistoryError is and empty and open is true', () => {
|
||||
test('modal open and StatusAlert showing', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('gradeOverrideHistoryError is empty and open is false', () => {
|
||||
test('modal closed and StatusAlert closed', () => {
|
||||
el.setProps({ open: false, gradeOverrideHistoryError: '' });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { martha: 'why did you say that name?!' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('gradeOverrideHistoryError from grades.gradeOverrideHistoryError', () => {
|
||||
expect(
|
||||
mapped.gradeOverrideHistoryError,
|
||||
).toEqual(selectors.grades.gradeOverrideHistoryError(testState));
|
||||
});
|
||||
test('open from app.modalState.open', () => {
|
||||
expect(mapped.open).toEqual(selectors.app.modalState.open(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('closeModal from actions.app.closeModal', () => {
|
||||
expect(mapDispatchToProps.closeModal).toEqual(actions.app.closeModal);
|
||||
});
|
||||
test('doneViewingAssignemtn from actions.grades.doneViewingAssignment', () => {
|
||||
expect(
|
||||
mapDispatchToProps.doneViewingAssignment,
|
||||
).toEqual(actions.grades.doneViewingAssignment);
|
||||
});
|
||||
test('updateGrades from thunkActions.grades.updateGrades', () => {
|
||||
expect(mapDispatchToProps.updateGrades).toEqual(thunkActions.grades.updateGrades);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
/**
|
||||
* FilterBadge
|
||||
@@ -16,56 +15,43 @@ import selectors from 'data/selectors';
|
||||
* @param {string} filterName - api filter name (for redux connector)
|
||||
*/
|
||||
export const FilterBadge = ({
|
||||
config: {
|
||||
filterName,
|
||||
handleClose,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
displayName,
|
||||
isDefault,
|
||||
hideValue,
|
||||
value,
|
||||
connectedFilters,
|
||||
},
|
||||
handleClose,
|
||||
}) => !isDefault && (
|
||||
<div>
|
||||
<span className="badge badge-info">
|
||||
<span>
|
||||
<FormattedMessage {...displayName} />
|
||||
} = selectors.root.useFilterBadgeConfig(filterName);
|
||||
if (isDefault) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<span className="badge badge-info">
|
||||
<span data-testid="display-name">{formatMessage(displayName)}</span>
|
||||
<span data-testid="filter-value">
|
||||
{!hideValue ? `: ${value}` : ''}
|
||||
</span>
|
||||
<Button
|
||||
className="btn-info"
|
||||
aria-label="close"
|
||||
onClick={handleClose(connectedFilters)}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</Button>
|
||||
</span>
|
||||
<span>
|
||||
{!hideValue ? `: ${value}` : ''}
|
||||
</span>
|
||||
<Button
|
||||
className="btn-info"
|
||||
aria-label="close"
|
||||
onClick={handleClose(connectedFilters)}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</Button>
|
||||
</span>
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FilterBadge.propTypes = {
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
// eslint-disable-next-line
|
||||
filterName: PropTypes.string.isRequired,
|
||||
// redux
|
||||
config: PropTypes.shape({
|
||||
connectedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||
displayName: PropTypes.shape({
|
||||
defaultMessage: PropTypes.string,
|
||||
}).isRequired,
|
||||
isDefault: PropTypes.bool.isRequired,
|
||||
hideValue: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state, ownProps) => ({
|
||||
config: selectors.root.filterBadgeConfig(state, ownProps.filterName),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(FilterBadge);
|
||||
export default FilterBadge;
|
||||
|
||||
@@ -1,107 +1,95 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { Button } from '@edx/paragon';
|
||||
import selectors from 'data/selectors';
|
||||
import { FilterBadge, mapStateToProps } from './FilterBadge';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
import FilterBadge from './FilterBadge';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
root: {
|
||||
filterBadgeConfig: jest.fn(state => ({ filterBadgeConfig: state })),
|
||||
useFilterBadgeConfig: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const handleClose = jest.fn(filters => ({ handleClose: filters }));
|
||||
const filterName = 'test-filter-name';
|
||||
|
||||
const hookProps = {
|
||||
displayName: {
|
||||
defaultMessage: 'a common name',
|
||||
},
|
||||
isDefault: false,
|
||||
hideValue: false,
|
||||
value: 'a common value',
|
||||
connectedFilters: ['some', 'filters'],
|
||||
};
|
||||
selectors.root.useFilterBadgeConfig.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('FilterBadge', () => {
|
||||
describe('component', () => {
|
||||
const config = {
|
||||
displayName: {
|
||||
defaultMessage: 'a common name',
|
||||
},
|
||||
isDefault: false,
|
||||
hideValue: false,
|
||||
value: 'a common value',
|
||||
connectedFilters: ['some', 'filters'],
|
||||
};
|
||||
const filterName = 'api.filter.name';
|
||||
let handleClose;
|
||||
let el;
|
||||
let props;
|
||||
beforeEach(() => {
|
||||
handleClose = (filters) => ({ handleClose: filters });
|
||||
props = { filterName, handleClose, config };
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
describe('with default value', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(
|
||||
<FilterBadge {...props} config={{ ...config, isDefault: true }} />,
|
||||
);
|
||||
});
|
||||
test('snapshot - empty', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('does not display', () => {
|
||||
expect(el).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('with non-default value (active)', () => {
|
||||
describe('if hideValue is true', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(
|
||||
<FilterBadge {...props} config={{ ...config, hideValue: true }} />,
|
||||
);
|
||||
});
|
||||
test('snapshot - shows displayName but not value in span', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('shows displayName but not value in span', () => {
|
||||
expect(el.find('span.badge').childAt(0).getElement()).toEqual(
|
||||
<span>
|
||||
<FormattedMessage {...config.displayName} />
|
||||
</span>,
|
||||
);
|
||||
});
|
||||
it('calls a handleClose event for connected filters on button click', () => {
|
||||
expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
|
||||
});
|
||||
});
|
||||
describe('if hideValue is false (default)', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilterBadge {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('shows displayName and value in span', () => {
|
||||
expect(el.find('span.badge').childAt(0).getElement()).toEqual(
|
||||
<span>
|
||||
<FormattedMessage {...config.displayName} />
|
||||
</span>,
|
||||
);
|
||||
expect(el.find('span.badge').childAt(1).getElement()).toEqual(
|
||||
<span>
|
||||
{`: ${config.value}`}
|
||||
</span>,
|
||||
);
|
||||
});
|
||||
it('calls a handleClose event for connected filters on button click', () => {
|
||||
expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
|
||||
});
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.root.useFilterBadgeConfig).toHaveBeenCalledWith(filterName);
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'kind', of: 'alien' };
|
||||
const filterName = 'Lilu Dallas Multipass';
|
||||
test('config loads config from root.filterBadgeConfig with ownProps.filterName', () => {
|
||||
const { config } = mapStateToProps(testState, { filterName });
|
||||
expect(config).toEqual(selectors.root.filterBadgeConfig(testState, filterName));
|
||||
describe('render', () => {
|
||||
const testDisplayName = () => {
|
||||
test('formatted display name appears on badge', () => {
|
||||
expect(el.instance.findByTestId('display-name')[0].children[0].el).toEqual(formatMessage(hookProps.displayName));
|
||||
});
|
||||
};
|
||||
const testCloseButton = () => {
|
||||
test('close button forwards close method', () => {
|
||||
expect(el.instance.findByType(Button)[0].props.onClick).toEqual(handleClose(hookProps.connectedFilters));
|
||||
});
|
||||
};
|
||||
test('empty render if isDefault', () => {
|
||||
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
|
||||
...hookProps,
|
||||
isDefault: true,
|
||||
});
|
||||
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
describe('hide Value', () => {
|
||||
beforeEach(() => {
|
||||
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
|
||||
...hookProps,
|
||||
hideValue: true,
|
||||
});
|
||||
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
|
||||
});
|
||||
testDisplayName();
|
||||
testCloseButton();
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('value is note present in the badge', () => {
|
||||
expect(el.instance.findByTestId('filter-value')[0].children).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
describe('do not hide value', () => {
|
||||
testDisplayName();
|
||||
testCloseButton();
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('value is present in the badge', () => {
|
||||
expect(el.instance.findByTestId('filter-value')[0].children[0].el).toBe(`: ${hookProps.value}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilterBadge component with default value snapshot - empty 1`] = `""`;
|
||||
|
||||
exports[`FilterBadge component with non-default value (active) if hideValue is false (default) snapshot 1`] = `
|
||||
exports[`FilterBadge render do not hide value snapshot 1`] = `
|
||||
<div>
|
||||
<span
|
||||
className="badge badge-info"
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="a common name"
|
||||
/>
|
||||
<span
|
||||
data-testid="display-name"
|
||||
>
|
||||
a common name
|
||||
</span>
|
||||
<span>
|
||||
<span
|
||||
data-testid="filter-value"
|
||||
>
|
||||
: a common value
|
||||
</span>
|
||||
<Button
|
||||
@@ -38,17 +38,19 @@ exports[`FilterBadge component with non-default value (active) if hideValue is f
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FilterBadge component with non-default value (active) if hideValue is true snapshot - shows displayName but not value in span 1`] = `
|
||||
exports[`FilterBadge render hide Value snapshot 1`] = `
|
||||
<div>
|
||||
<span
|
||||
className="badge badge-info"
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="a common name"
|
||||
/>
|
||||
<span
|
||||
data-testid="display-name"
|
||||
>
|
||||
a common name
|
||||
</span>
|
||||
<span />
|
||||
<span
|
||||
data-testid="filter-value"
|
||||
/>
|
||||
<Button
|
||||
aria-label="close"
|
||||
className="btn-info"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import FilterBadges from '.';
|
||||
import FilterBadge from './FilterBadge';
|
||||
@@ -22,14 +22,14 @@ describe('FilterBadges', () => {
|
||||
el = shallow(<FilterBadges handleClose={handleClose} />);
|
||||
});
|
||||
test('snapshot - has a filterbadge with handleClose for each filter in badgeOrder', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('has a filterbadge with handleClose for each filter in badgeOrder', () => {
|
||||
const badgeProps = el.find(FilterBadge).map(badgeEl => badgeEl.props());
|
||||
const badgeProps = el.instance.findByType(FilterBadge).map(badgeEl => badgeEl.props);
|
||||
// key prop is not rendered by react
|
||||
expect(badgeProps[0]).toEqual({ filterName: order[0], handleClose });
|
||||
expect(badgeProps[1]).toEqual({ filterName: order[1], handleClose });
|
||||
expect(badgeProps[2]).toEqual({ filterName: order[2], handleClose });
|
||||
expect(badgeProps[0]).toMatchObject({ filterName: order[0], handleClose });
|
||||
expect(badgeProps[1]).toMatchObject({ filterName: order[1], handleClose });
|
||||
expect(badgeProps[2]).toMatchObject({ filterName: order[2], handleClose });
|
||||
expect(badgeProps.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button, Icon } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from './FilterMenuToggle.messages';
|
||||
|
||||
/**
|
||||
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
|
||||
* as well as the search box for searching by username/email.
|
||||
*/
|
||||
export const FilterMenuToggle = ({ toggleFilterDrawer }) => (
|
||||
<Button
|
||||
id="edit-filters-btn"
|
||||
className="btn-primary align-self-start"
|
||||
onClick={toggleFilterDrawer}
|
||||
>
|
||||
<Icon className="fa fa-filter" /> <FormattedMessage {...messages.editFilters} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
FilterMenuToggle.propTypes = {
|
||||
// From Redux
|
||||
toggleFilterDrawer: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = () => ({});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
toggleFilterDrawer: thunkActions.app.filterMenu.toggle,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FilterMenuToggle);
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import { FilterMenuToggle, mapDispatchToProps, mapStateToProps } from './FilterMenuToggle';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
filterMenu: { toggle: jest.fn() },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FilterMenuToggle component', () => {
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const toggleFilterDrawer = jest.fn().mockName('this.props.toggleFilterDrawer');
|
||||
expect(shallow((
|
||||
<FilterMenuToggle {...{ toggleFilterDrawer }} />
|
||||
))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
test('does not connect any selectors', () => {
|
||||
expect(mapStateToProps({ test: 'state' })).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('toggleFilterDrawer from thunkActions.app.filterMenu.toggle', () => {
|
||||
expect(mapDispatchToProps.toggleFilterDrawer).toEqual(
|
||||
thunkActions.app.filterMenu.toggle,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilterMenuToggle component render snapshot 1`] = `
|
||||
<Button
|
||||
className="btn-primary align-self-start"
|
||||
id="edit-filters-btn"
|
||||
onClick={[MockFunction hooks.toggleFilterMenu]}
|
||||
>
|
||||
<Icon
|
||||
src="FilterAlt"
|
||||
/>
|
||||
|
||||
Edit Filters
|
||||
</Button>
|
||||
`;
|
||||
31
src/components/GradesView/FilterMenuToggle/index.jsx
Normal file
31
src/components/GradesView/FilterMenuToggle/index.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Icon } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FilterAlt } from '@edx/paragon/icons';
|
||||
|
||||
import { thunkActions } from 'data/redux/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
|
||||
* as well as the search box for searching by username/email.
|
||||
*/
|
||||
export const FilterMenuToggle = () => {
|
||||
const toggleFilterMenu = thunkActions.app.filterMenu.useToggleMenu();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<Button
|
||||
id="edit-filters-btn"
|
||||
className="btn-primary align-self-start"
|
||||
onClick={toggleFilterMenu}
|
||||
>
|
||||
<Icon src={FilterAlt} /> {formatMessage(messages.editFilters)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
FilterMenuToggle.propTypes = {};
|
||||
|
||||
export default FilterMenuToggle;
|
||||
47
src/components/GradesView/FilterMenuToggle/index.test.jsx
Normal file
47
src/components/GradesView/FilterMenuToggle/index.test.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { thunkActions } from 'data/redux/hooks';
|
||||
|
||||
import FilterMenuToggle from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
thunkActions: {
|
||||
app: {
|
||||
filterMenu: {
|
||||
useToggleMenu: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const toggleFilterMenu = jest.fn().mockName('hooks.toggleFilterMenu');
|
||||
thunkActions.app.filterMenu.useToggleMenu.mockReturnValue(toggleFilterMenu);
|
||||
|
||||
let el;
|
||||
describe('FilterMenuToggle component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<FilterMenuToggle />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(thunkActions.app.filterMenu.useToggleMenu).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.instance.type).toEqual('Button');
|
||||
expect(el.instance.props.onClick).toEqual(toggleFilterMenu);
|
||||
expect(el.instance.children[2].el).toContain(formatMessage(messages.editFilters));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
/**
|
||||
* <FilteredUsersLabel />
|
||||
* Simple label component displaying the filtered and total users shown
|
||||
*/
|
||||
export const FilteredUsersLabel = ({
|
||||
filteredUsersCount,
|
||||
totalUsersCount,
|
||||
}) => {
|
||||
if (!totalUsersCount) {
|
||||
return null;
|
||||
}
|
||||
const bold = (val) => (<span className="font-weight-bold">{val}</span>);
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="gradebook.GradesTab.usersVisibilityLabel'"
|
||||
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
|
||||
description="Users visibility label"
|
||||
values={{
|
||||
filteredUsers: bold(filteredUsersCount),
|
||||
totalUsers: bold(totalUsersCount),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
FilteredUsersLabel.propTypes = {
|
||||
filteredUsersCount: PropTypes.number.isRequired,
|
||||
totalUsersCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
totalUsersCount: selectors.grades.totalUsersCount(state),
|
||||
filteredUsersCount: selectors.grades.filteredUsersCount(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(FilteredUsersLabel);
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { FilteredUsersLabel, mapStateToProps } from './FilteredUsersLabel';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: {
|
||||
filteredUsersCount: state => ({ filteredUsersCount: state }),
|
||||
totalUsersCount: state => ({ totalUsersCount: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FilteredUsersLabel', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
filteredUsersCount: 23,
|
||||
totalUsersCount: 140,
|
||||
};
|
||||
it('does not render if totalUsersCount is falsey', () => {
|
||||
expect(shallow(<FilteredUsersLabel {...props} totalUsersCount={0} />)).toEqual({});
|
||||
});
|
||||
test('snapshot - displays label with number of filtered users out of total', () => {
|
||||
expect(shallow(<FilteredUsersLabel {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { a: 'nice', day: 'for', some: 'rain' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('filteredUsersCount from grades.filteredUsersCount', () => {
|
||||
expect(mapped.filteredUsersCount).toEqual(selectors.grades.filteredUsersCount(testState));
|
||||
});
|
||||
test('totalUsersCount from grades.totalUsersCount', () => {
|
||||
expect(mapped.totalUsersCount).toEqual(selectors.grades.totalUsersCount(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilteredUsersLabel component render snapshot 1`] = `
|
||||
<format-message-function
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Showing {filteredUsers} of {totalUsers} total learners",
|
||||
"description": "Users visibility label",
|
||||
"id": "gradebook.GradesTab.usersVisibilityLabel",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"filteredUsers": <BoldText
|
||||
text={100}
|
||||
/>,
|
||||
"totalUsers": <BoldText
|
||||
text={123}
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
37
src/components/GradesView/FilteredUsersLabel/index.jsx
Normal file
37
src/components/GradesView/FilteredUsersLabel/index.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const BoldText = ({ text }) => (
|
||||
<span className="font-weight-bold">{text}</span>
|
||||
);
|
||||
BoldText.propTypes = {
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* <FilteredUsersLabel />
|
||||
* Simple label component displaying the filtered and total users shown
|
||||
*/
|
||||
export const FilteredUsersLabel = () => {
|
||||
const { filteredUsersCount, totalUsersCount } = selectors.grades.useUserCounts();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
if (!totalUsersCount) {
|
||||
return null;
|
||||
}
|
||||
return formatMessage(
|
||||
messages.visibilityLabel,
|
||||
{
|
||||
filteredUsers: <BoldText text={filteredUsersCount} />,
|
||||
totalUsers: <BoldText text={totalUsersCount} />,
|
||||
},
|
||||
);
|
||||
};
|
||||
FilteredUsersLabel.propTypes = {};
|
||||
|
||||
export default FilteredUsersLabel;
|
||||
56
src/components/GradesView/FilteredUsersLabel/index.test.jsx
Normal file
56
src/components/GradesView/FilteredUsersLabel/index.test.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
import FilteredUsersLabel, { BoldText } from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
grades: {
|
||||
useUserCounts: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const userCounts = {
|
||||
filteredUsersCount: 100,
|
||||
totalUsersCount: 123,
|
||||
};
|
||||
selectors.grades.useUserCounts.mockReturnValue(userCounts);
|
||||
|
||||
let el;
|
||||
describe('FilteredUsersLabel component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<FilteredUsersLabel />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(selectors.grades.useUserCounts).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('null render if totalUsersCount is 0', () => {
|
||||
selectors.grades.useUserCounts.mockReturnValueOnce({
|
||||
...userCounts,
|
||||
totalUsersCount: 0,
|
||||
});
|
||||
expect(shallow(<FilteredUsersLabel />).isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.instance).toMatchObject(shallow(formatMessage(messages.visibilityLabel, {
|
||||
filteredUsers: <BoldText text={userCounts.filteredUsersCount} />,
|
||||
totalUsers: <BoldText text={userCounts.totalUsersCount} />,
|
||||
})));
|
||||
});
|
||||
});
|
||||
});
|
||||
11
src/components/GradesView/FilteredUsersLabel/messages.js
Normal file
11
src/components/GradesView/FilteredUsersLabel/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
visibilityLabel: {
|
||||
id: 'gradebook.GradesTab.usersVisibilityLabel',
|
||||
defaultMessage: 'Showing {filteredUsers} of {totalUsers} total learners',
|
||||
description: 'Users visibility label',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -29,16 +29,16 @@ Username.propTypes = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Fields.Email
|
||||
* Simple label field for email value.
|
||||
* @param {string} email - email for display
|
||||
* Fields.Text
|
||||
* Simple label field for text value.
|
||||
* @param {string} value - value for display
|
||||
*/
|
||||
const Email = ({ email }) => <span className="wrap-text-in-cell">{email}</span>;
|
||||
Email.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
const Text = ({ value }) => (<span className="wrap-text-in-cell">{value}</span>);
|
||||
Text.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
Email,
|
||||
Text,
|
||||
Username,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import Fields from './Fields';
|
||||
|
||||
@@ -16,14 +16,13 @@ describe('Gradebook Table Fields', () => {
|
||||
el = shallow(<Fields.Username {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('wraps external user key and username', () => {
|
||||
expect(el.find('span').childAt(0)).toMatchSnapshot();
|
||||
expect(el.find('span').childAt(0)).toMatchSnapshot();
|
||||
const content = el.find('span').childAt(0);
|
||||
expect(content.childAt(0).text()).toEqual(username);
|
||||
expect(content.childAt(1).text()).toEqual(props.userKey);
|
||||
expect(el.instance.findByType('span')[0].el).toMatchSnapshot();
|
||||
const content = el.instance.findByType('span')[0].children[0];
|
||||
expect(content.children[0].children[0].el).toEqual(username);
|
||||
expect(content.children[1].children[0].el).toEqual(props.userKey);
|
||||
});
|
||||
});
|
||||
describe('without external_user_key', () => {
|
||||
@@ -31,23 +30,23 @@ describe('Gradebook Table Fields', () => {
|
||||
el = shallow(<Fields.Username username={username} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('wraps username only', () => {
|
||||
const content = el.find('span').childAt(0);
|
||||
expect(content.childAt(0).text()).toEqual(username);
|
||||
expect(content.children()).toHaveLength(1);
|
||||
const content = el.instance.findByType('span')[0].children[0];
|
||||
expect(content.children[0].children[0].el).toEqual(username);
|
||||
expect(content.children).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Email', () => {
|
||||
const email = 'myTag@place.com';
|
||||
describe('Text', () => {
|
||||
const value = 'myTag@place.com';
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<Fields.Email email={email} />)).toMatchSnapshot();
|
||||
expect(shallow(<Fields.Text value={value} />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('wraps entry email', () => {
|
||||
expect(shallow(<Fields.Email email={email} />).text()).toEqual(email);
|
||||
test('wraps entry value', () => {
|
||||
expect(shallow(<Fields.Text value={value} />).instance.children[0].el).toEqual(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import { selectors, thunkActions } from 'data/redux/hooks';
|
||||
import transforms from 'data/redux/transforms';
|
||||
import * as module from './GradeButton';
|
||||
|
||||
const { subsectionGrade } = selectors.grades;
|
||||
export const useGradeButtonData = ({ entry, subsection }) => {
|
||||
const areGradesFrozen = selectors.assignmentTypes.useAreGradesFrozen();
|
||||
const { gradeFormat } = selectors.grades.useGradeData();
|
||||
const setModalState = thunkActions.app.useSetModalStateFromTable();
|
||||
const label = transforms.grades.subsectionGrade({ gradeFormat, subsection })();
|
||||
|
||||
const onClick = () => {
|
||||
setModalState({
|
||||
userEntry: entry,
|
||||
subsection,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
areGradesFrozen,
|
||||
label,
|
||||
onClick,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* GradeButton
|
||||
@@ -18,38 +35,24 @@ const { subsectionGrade } = selectors.grades;
|
||||
* @param {object} entry - user's grade entry
|
||||
* @param {object} subsection - user's subsection grade from subsection_breakdown
|
||||
*/
|
||||
export class GradeButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
get label() {
|
||||
return subsectionGrade[this.props.format](this.props.subsection);
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.props.setModalState({
|
||||
userEntry: this.props.entry,
|
||||
subsection: this.props.subsection,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.areGradesFrozen
|
||||
? this.label
|
||||
: (
|
||||
<Button
|
||||
variant="link"
|
||||
className="btn-header grade-button"
|
||||
onClick={this.onClick}
|
||||
>
|
||||
{this.label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const GradeButton = ({ entry, subsection }) => {
|
||||
const {
|
||||
areGradesFrozen,
|
||||
label,
|
||||
onClick,
|
||||
} = module.useGradeButtonData({ entry, subsection });
|
||||
return areGradesFrozen
|
||||
? label
|
||||
: (
|
||||
<Button
|
||||
variant="link"
|
||||
className="btn-header grade-button"
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
GradeButton.propTypes = {
|
||||
subsection: PropTypes.shape({
|
||||
attempted: PropTypes.bool,
|
||||
@@ -62,19 +65,6 @@ GradeButton.propTypes = {
|
||||
user_id: PropTypes.number,
|
||||
username: PropTypes.string,
|
||||
}).isRequired,
|
||||
// redux
|
||||
areGradesFrozen: PropTypes.bool.isRequired,
|
||||
format: PropTypes.string.isRequired,
|
||||
setModalState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
|
||||
format: selectors.grades.gradeFormat(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
setModalState: thunkActions.app.setModalStateFromTable,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GradeButton);
|
||||
export default GradeButton;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user