Compare commits

..

2 Commits

Author SHA1 Message Date
Ben Warzeski
096ec432fe refactor: bulk management controls component 2023-03-31 15:03:40 -04:00
Ben Warzeski
18648eb8c6 refactor: gradebook header component refactor 2023-03-31 14:18:12 -04:00
259 changed files with 50954 additions and 15744 deletions

2
.env
View File

@@ -10,7 +10,6 @@ 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=''
@@ -33,4 +32,3 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
APP_ID=''
MFE_CONFIG_API_URL=''
DISPLAY_FEEDBACK_WIDGET='true'

View File

@@ -7,11 +7,11 @@ LOGOUT_URL='http://localhost:18000/login'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
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,4 +39,3 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
APP_ID=''
MFE_CONFIG_API_URL=''
DISPLAY_FEEDBACK_WIDGET='false'

View File

@@ -3,4 +3,3 @@ dist/
node_modules/
src/postcss.config.js
src/segment.js
src/lightning.js

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
const config = createConfig('eslint', {
rules: {

View File

@@ -1,7 +0,0 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

33
.github/renovate.json vendored
View File

@@ -1,33 +0,0 @@
{
"extends": [
"config:base",
"schedule:weekly",
":automergeLinters",
":automergeMinor",
":automergeTesters",
":enableVulnerabilityAlerts",
":rebaseStalePrs",
":semanticCommits",
":updateNotScheduled"
],
"packageRules": [
{
"matchDepTypes": [
"devDependencies"
],
"matchUpdateTypes": [
"lockFileMaintenance",
"minor",
"patch",
"pin"
],
"automerge": true
},
{
"matchPackagePatterns": ["@edx", "@openedx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
],
"timezone": "America/New_York"
}

View File

@@ -10,16 +10,23 @@ on:
jobs:
test:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
matrix:
node: [16]
npm: [8.5.x]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Setup Nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version-file: '.nvmrc'
node-version: ${{ matrix.node }}
- name: Install npm 8.5.x
run: npm install -g npm@${{ matrix.npm }}
- name: Install dependencies
run: npm ci
@@ -40,14 +47,11 @@ jobs:
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
uses: codecov/codecov-action@v2
- name: Send failure notification
if: ${{ failure() }}
uses: dawidd6/action-send-mail@v4
uses: dawidd6/action-send-mail@v3
with:
server_address: email-smtp.us-east-1.amazonaws.com
server_port: 465

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master

33
.github/workflows/npm-publish.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Release CI
on:
push:
tags:
- '*'
jobs:
release:
name: Release
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: npm ci
- name: Create Build
run: npm run build
- name: Release Package
env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
run: npm semantic-release

2
.gitignore vendored
View File

@@ -23,5 +23,3 @@ temp/babel-plugin-react-intl
### transifex ###
src/i18n/transifex_input.json
temp
src/i18n/messages/

1
.nvmrc
View File

@@ -1 +0,0 @@
20

27
.releaserc Normal file
View File

@@ -0,0 +1,27 @@
{
"branch": "master",
"tagFormat": "v${version}",
"verifyConditions": [
"@semantic-release/npm",
{
"path": "@semantic-release/github",
"assets": {
"path": "dist/*"
}
}
],
"analyzeCommits": "@semantic-release/commit-analyzer",
"generateNotes": "@semantic-release/release-notes-generator",
"prepare": "@semantic-release/npm",
"publish": [
"@semantic-release/npm",
{
"path": "@semantic-release/github",
"assets": {
"path": "dist/*"
}
}
],
"success": [],
"fail": []
}

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-gradebook]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -1,14 +1,17 @@
npm-install-%: ## install specified % npm package
npm ci $* --save-dev
npm install $* --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
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-formatjs
transifex_temp = ./temp/babel-plugin-react-intl
NPM_TESTS=build i18n_extract lint test is-es5
@@ -35,20 +38,25 @@ i18n.concat:
extract_translations: | requirements i18n.extract i18n.concat
# Despite the name, we actually need this target to detect changes in the incoming translated message files as well.
detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Experimental: OEP-58 Pulls translations using atlas
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
# Pulls translations from Transifex.
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
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
# This target is used by CI.
validate-no-uncommitted-package-lock-changes:

136
README.md
View File

@@ -1,5 +1,3 @@
# frontend-app-gradebook
[![Build Status](https://api.travis-ci.com/edx/frontend-app-gradebook.svg?branch=master)](https://travis-ci.com/edx/frontend-app-gradebook)
[![Codecov](https://img.shields.io/codecov/c/gh/openedx/frontend-app-gradebook)](https://app.codecov.io/gh/openedx/frontend-app-gradebook)
[![npm_version](https://img.shields.io/npm/v/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)
@@ -7,7 +5,7 @@
[![license](https://img.shields.io/npm/l/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
# Purpose
# Gradebook
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.
@@ -18,8 +16,8 @@ Jump to:
For existing documentation see:
- Basic Usage: [Review Learner Grades (read-the-docs)](https://docs.openedx.org/en/latest/educators/how-tos/data/view_learner_grades.html)
- Bulk Grade Management: [Override Learner Subsection Scores in Bulk (read-the-docs)](https://docs.openedx.org/en/latest/educators/how-tos/data/manage_learner_grades.html#override-learner-subsection-scores-in-bulk)
- Basic Usage: [Review Learner Grades (read-the-docs)](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/student_progress/course_grades.html#review-learner-grades-on-the-instructor-dashboard)
- Bulk Grade Management: [Override Learner Subsection Scores in Bulk (read-the-docs)](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/student_progress/course_grades.html#override-learner-subsection-scores-in-bulk)
## Should I use Gradebook in my course?
@@ -49,8 +47,7 @@ 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.
## Getting Started
## Quickstart
### Installation
@@ -59,66 +56,47 @@ 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. Use the version of Node specified in ``.nvmrc``
3. Stop the Tutor devstack, if it's running:
``tutor dev stop``
4. Next, we need to tell Tutor that we're going to be running this repo in development mode, and it should be excluded from the mfe container that otherwise runs every MFE. Run this:
``tutor mounts add /path/to/frontend-app-gradebook``
5. Start Tutor in development mode. This command will start the LMS and Studio,
and other required MFEs like ``authn`` and ``account``, but will not start the
Gradebook MFE, which we're going to run on the host instead of in a container
managed by Tutor. Run:
``tutor dev start lms cms mfe``
## Startup
1. Install npm dependencies:
``cd frontend-app-gradebook && npm install``
2. Start the dev server:
``npm run dev``
## Running the UI Standalone
To install the project please refer to the [`MFE Development on Tutor`](https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development) instructions.
To install the project please refer to the [`edX Developer Stack`](https://github.com/openedx/devstack) instructions.
When not mounted, gradebook will run in the shared MFE container at http://apps.local.openedx.io/gradebook/course-v1:edX+DemoX+Demo_Course.
The web application runs on port **1994**, so when you go to `http://localhost:1994/course-v1:edX+DemoX+Demo_Course` you should see the UI (assuming you have such a Demo Course in your devstack). Note that you always have to provide a course id to actually see a gradebook.
When mounted in the tutor ``gradebook`` container, or when running a local (host) webpack dev server, the web application runs on port **1994**, so when you go to `http://apps.local.openedx.io:1994/gradebook/course-v1:edX+DemoX+Demo_Course` you should see the UI (assuming you have such a Demo Course in your devstack). Note that you always have to provide a course id to actually see a gradebook.
(Note: This may not work in Tutor; these instructions are for the deprecated Devstack) You can see the log messages for the docker container by executing `make gradebook-logs` in the `devstack` directory.
If you don't, you can see the log messages for the docker container by executing `make gradebook-logs` in the `devstack` directory.
Note that starting the container executes the `npm run start` script which will hot-reload JavaScript and Sass files changes, so you should (:crossed_fingers:) not need to do anything (other than wait) when making changes.
## Plugins
This MFE can be customized using [Frontend Plugin Framework](https://github.com/openedx/frontend-plugin-framework).
## Configuring for local use in edx-platform
The parts of this MFE that can be customized in that manner are documented [here](/src/plugin-slots).
Assuming you've got the UI running at `http://localhost:1994`, you can configure the LMS in edx-platform
to point to your local gradebook from the instructor dashboard by putting this setting in `lms/env/private.py`:
```
WRITABLE_GRADEBOOK_URL = 'http://localhost:1994'
```
There are also several edx-platform waffle and feature flags you'll have to enable from the Django admin:
1. Grades > Persistent grades enabled flag. Add this flag if it doesn't exist,
check the ``enabled`` and ``enabled for all courses`` boxes.
2. Waffle > Switches. Add the ``grades.assume_zero_grade_if_absent`` switch and make it active.
3. Waffle_utils > Waffle flag course overrides. Activate waffle flags for courses where you want to enable Gradebook functionality:
- Enable Gradebook by adding the ``grades.writable_gradebook`` add checking the ``enabled`` box.
- Enable Bulk Grade Management by adding the ``grades.bulk_management`` flag and checking the ``enabled`` box.
Alternatively, you could add these as regular waffle flags to enable the functionality for all courses.
**NOTE:** IF the above flags are not configured correctly, the gradebook may appear to work, but will return bogus
numbers for grades. If your gradebook isn't accepting your changes, or the changes aren't resulting in sane,
recalculated grade values, verify you've set all flags correctly.
## Running tests
Run:
``nvm use``
``npm ci``
``npm test``
1. Assuming that you're operating in the context of the edX devstack,
run `gradebook-shell` from your devstack directory. This will start a bash shell inside your
running gradebook container.
2. Run `make test` (which executes `npm run test`). This will run all of the gradebook tests.
## Directory Structure
@@ -141,49 +119,3 @@ Run:
## 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](https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html) for details.
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](https://openedx.org/slack), then join our
[community Slack workspace](https://openedx.slack.com/) Because this is a
frontend repository, the best place to discuss it would be in the
[#wg-frontend channel](https://openedx.slack.com/archives/C04BM6YC7A6).
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](https://openedx.org/community/connect) page.
The Open edX Code of Conduct
============================
All community members are expected to follow the [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.

View File

@@ -1,3 +1,3 @@
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('babel');

View File

@@ -1,14 +0,0 @@
# This file records information about this repo. Its use is described in OEP-55:
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: "frontend-app-gradebook"
description: "The frontend (MFE) for Open edX Gradebook"
annotations:
openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec:
owner: user:farhaanbukhsh
type: 'website'
lifecycle: 'experimental'

View File

@@ -4,15 +4,15 @@ Instructions for setting up environments and data for testing Gradebook.
## Set up a course with graded content
A course with graded content is the first prerequisite to testing. Use an existing course (e.g. the DemoX Demonstration Course in Devstack) or see [Building and Running an edX Course > Developing Your Course](https://docs.openedx.org/en/latest/educators/quickstarts/build_a_course.html) for notes on how to develop a course from scratch.
A course with graded content is the first prerequisite to testing. Use an existing course (e.g. the DemoX Demonstration Course in Devstack) or see [Building and Running an edX Course > Developing Your Course](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/developing_course/index.html) for notes on how to develop a course from scratch.
Notably, the course needs a grading policy and subsections with scoreable content.
After creating subsections with content, they need to be configured with an "Assignment Type" to be included in grading.
Suggested resources:
- [Establishing a Grading Policy For Your Course](https://docs.openedx.org/en/latest/educators/how-tos/data/manage_learner_grades.html#review-how-grading-is-configured-for-your-course)
- [Adding Exercises and Tools](https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_problems_exercises_tools.html)
- [Set the Assignment Type and Due Date for a Subsection](https://docs.openedx.org/en/latest/educators/how-tos/course_development/set_subsection_problem_date.html#set-the-assignment-type-and-due-date-for-a-subsection)
- [Establishing a Grading Policy For Your Course](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/grading/index.html)
- [Adding Exercises and Tools](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/grading/index.html)
- [Set the Assignment Type and Due Date for a Subsection](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/developing_course/course_subsections.html#set-the-assignment-type-and-due-date-for-a-subsection)
## Enable Gradebook for course
@@ -35,7 +35,7 @@ Bulk Management is an added feature to allow modifying grades in bulk via CSV up
## Create a Master's track for testing Master's-only features
[source - note: possibly outdated, edx.org-specific](https://openedx.atlassian.net/wiki/spaces/MS/pages/1453818012/Add+a+learner+into+a+master+s+track)
[source](https://openedx.atlassian.net/wiki/spaces/MS/pages/1453818012/Add+a+learner+into+a+master+s+track)
Add a Master's track in your course:
- As an admin user, go to Django Admin (`{lms-url}/admin`) > Course Modes and add a new course mode

View File

@@ -1,10 +1,13 @@
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', {
setupFilesAfterEnv: [
'<rootDir>/src/setupTest.js',
],
modulePaths: ['<rootDir>/src/'],
snapshotSerializers: [
'enzyme-to-json/serializer',
],
coveragePathIgnorePatterns: [
'src/segment.js',
'src/postcss.config.js',

9
openedx.yaml Normal file
View File

@@ -0,0 +1,9 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
tags:
- frontend-app
- masters
oeps:
oep-2: true # Repository metadata
openedx-release: {ref: master}

56340
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.6.2",
"version": "1.6.1",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": {
"type": "git",
@@ -9,13 +9,12 @@
"scripts": {
"build": "fedx-scripts webpack",
"is-es5": "es-check es5 ./dist/*.js",
"i18n_extract": "fedx-scripts formatjs extract",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"prepush": "npm run lint",
"semantic-release": "semantic-release",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/gradebook/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
"watch-tests": "jest --watch"
},
@@ -29,34 +28,34 @@
"extends @edx/browserslist-config"
],
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.4.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@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/reactifex": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.5",
"@openedx/frontend-plugin-framework": "^1.6.0",
"@openedx/paragon": "^22.16.0",
"@redux-beacon/segment": "^1.0.0",
"@reduxjs/toolkit": "^1.5.1",
"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.8.1",
"prop-types": "15.7.2",
"query-string": "6.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "^6.1.0",
"react-redux": "^7.2.9",
"react-router": "6.15.0",
"react-router-dom": "6.15.0",
"react-intl": "^2.9.0",
"react-redux": "^7.1.1",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-router-redux": "^5.0.0-alpha.9",
"redux": "4.0.5",
"redux-beacon": "^2.1.0",
@@ -69,15 +68,20 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/react": "^16.2.0",
"@edx/frontend-build": "^12.4.15",
"@testing-library/react": "^12.1.0",
"axios": "0.21.2",
"axios-mock-adapter": "^1.17.0",
"enzyme-adapter-react-16": "^1.14.0",
"es-check": "^2.3.0",
"fetch-mock": "^12.2.0",
"fetch-mock": "^6.5.2",
"husky": "2.7.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest": "29.3.1",
"react-dev-utils": "^12.0.1",
"react-test-renderer": "^18.3.1",
"react-test-renderer": "^16.10.1",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.3"
"redux-mock-store": "^1.5.3",
"semantic-release": "^19.0.3"
}
}

View File

@@ -1,11 +1,12 @@
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { AppProvider } from '@edx/frontend-platform/react';
import { FooterSlot } from '@edx/frontend-component-footer';
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';
@@ -14,18 +15,21 @@ import Head from './head/Head';
const App = () => (
<AppProvider store={store}>
<Head />
<div>
<Header />
<main>
<Routes>
<Route
path="/:courseId"
element={<GradebookPage />}
/>
</Routes>
</main>
<FooterSlot />
</div>
<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>
</AppProvider>
);

View File

@@ -1,7 +1,7 @@
// frontend-app-*/src/index.scss
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
$fa-font-path: "~font-awesome/fonts";

View File

@@ -1,63 +1,80 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { Route } from 'react-router-dom';
import { BrowserRouter as Router, Route, Switch } 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 from './App';
import Head from './head/Head';
jest.mock('react-router-dom', () => ({
BrowserRouter: () => 'BrowserRouter',
Route: () => 'Route',
Routes: () => 'Routes',
Switch: () => 'Switch',
}));
jest.mock('@edx/frontend-platform/react', () => ({
AppProvider: () => 'AppProvider',
}));
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' }));
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 secondChild;
let router;
describe('App router component', () => {
test('snapshot', () => {
expect(shallow(<App />).snapshot).toMatchSnapshot();
expect(shallow(<App />)).toMatchSnapshot();
});
describe('component', () => {
beforeEach(() => {
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
el = shallow(<App />);
secondChild = el.instance.children;
router = el.childAt(1);
});
describe('AppProvider', () => {
test('AppProvider is the parent component, passed the redux store props', () => {
expect(el.instance.type).toBe('AppProvider');
expect(el.instance.props.store).toEqual(store);
expect(el.type()).toBe(AppProvider);
expect(el.props().store).toEqual(store);
});
});
describe('Head', () => {
test('first child of AppProvider', () => {
expect(el.instance.children[0].type).toBe('Head');
expect(el.childAt(0).type()).toBe(Head);
});
});
describe('Router', () => {
test('second child of AppProvider', () => {
expect(secondChild[1].type).toBe('div');
expect(router.type()).toBe(Router);
});
test('Header is above/outside-of the routing', () => {
expect(secondChild[1].children[0].type).toBe('Header');
expect(secondChild[1].children[1].type).toBe('main');
expect(router.childAt(0).childAt(0).type()).toBe(Header);
expect(router.childAt(0).childAt(1).type()).toBe('main');
});
test('Routing - GradebookPage is only route', () => {
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);
expect(router.find('main')).toEqual(shallow(
<main>
<Switch>
<Route exact path={routePath} component={GradebookPage} />
</Switch>
</main>,
));
});
});
test('Footer logo drawn from env variable', () => {
expect(router.find(Footer).props().logo).toEqual(logo);
});
});
});

View File

@@ -5,17 +5,20 @@ exports[`App router component snapshot 1`] = `
store="testStore"
>
<Head />
<div>
<Header />
<main>
<Routes>
<Route
element={<GradebookPage />}
path="/:courseId"
/>
</Routes>
</main>
<FooterSlot />
</div>
<BrowserRouter>
<div>
<Header />
<main>
<Switch>
<Route
component="GradebookPage"
exact={true}
path="/:courseId"
/>
</Switch>
</main>
<Footer />
</div>
</BrowserRouter>
</AppProvider>
`;

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { connect } from 'react-redux';
import { Alert } from '@openedx/paragon';
import { Alert } from '@edx/paragon';
import selectors from 'data/selectors';
import messages from './messages';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Alert } from '@openedx/paragon';
import { shallow } from 'enzyme';
import { Alert } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
@@ -12,7 +12,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('@openedx/paragon', () => ({
jest.mock('@edx/paragon', () => ({
Alert: () => 'Alert',
}));
jest.mock('data/selectors', () => ({
@@ -35,17 +35,17 @@ describe('BulkManagementAlerts', () => {
el = shallow(<BulkManagementAlerts />);
});
test('snapshot - bulkImportError closed, success closed', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('closed danger alert', () => {
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');
expect(el.childAt(0).is(Alert)).toEqual(true);
expect(el.childAt(0).props().show).toEqual(false);
expect(el.childAt(0).props().variant).toEqual('danger');
});
test('closed success alert', () => {
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');
expect(el.childAt(1).is(Alert)).toEqual(true);
expect(el.childAt(1).props().show).toEqual(false);
expect(el.childAt(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.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('open danger alert with bulkImportError content', () => {
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);
expect(el.childAt(0).is(Alert)).toEqual(true);
expect(el.childAt(0).children().text()).toEqual(errorMessage);
expect(el.childAt(0).props().show).toEqual(true);
});
test('open success alert with messages.successDialog content', () => {
expect(el.instance.children[1].type).toBe('Alert');
expect(el.shallowWrapper.props.children[1].props.children).toEqual(
expect(el.childAt(1).is(Alert)).toEqual(true);
expect(el.childAt(1).children().getElement()).toEqual(
<FormattedMessage {...messages.successDialog} />,
);
expect(el.instance.children[1].props.show).toEqual(true);
expect(el.childAt(1).props().show).toEqual(true);
});
});
});

View File

@@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { DataTable } from '@openedx/paragon';
import { DataTable } from '@edx/paragon';
import { bulkManagementColumns } from 'data/constants/app';
import selectors from 'data/selectors';

View File

@@ -1,7 +1,7 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { DataTable } from '@openedx/paragon';
import { shallow } from 'enzyme';
import { DataTable } from '@edx/paragon';
import selectors from 'data/selectors';
import { bulkManagementColumns } from 'data/constants/app';
@@ -9,7 +9,7 @@ import { bulkManagementColumns } from 'data/constants/app';
import ResultsSummary from './ResultsSummary';
import { HistoryTable, mapStateToProps } from './HistoryTable';
jest.mock('@openedx/paragon', () => ({ DataTable: () => 'DataTable' }));
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
@@ -56,12 +56,12 @@ describe('HistoryTable', () => {
el = shallow(<HistoryTable {...props} />);
});
test('snapshot - loads formatted table', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
describe('history table', () => {
let table;
beforeEach(() => {
table = el.instance.findByType(DataTable);
table = el.find(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[0].props.data).toMatchSnapshot();
expect(table.props().data).toMatchSnapshot();
});
test(fieldAssertions.join(', '), () => {
const rows = table[0].props.data;
const rows = table.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[0].props.columns).toEqual(bulkManagementColumns);
expect(table.props().columns).toEqual(bulkManagementColumns);
});
});
});

View File

@@ -2,8 +2,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Hyperlink, Icon } from '@openedx/paragon';
import { Download } from '@openedx/paragon/icons';
import { Hyperlink, Icon } from '@edx/paragon';
import { Download } from '@edx/paragon/icons';
import lms from 'data/services/lms';

View File

@@ -1,16 +1,17 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { Download } from '@openedx/paragon/icons';
import { Icon } from '@edx/paragon';
import { Download } from '@edx/paragon/icons';
import lms from 'data/services/lms';
import ResultsSummary from './ResultsSummary';
jest.mock('@openedx/paragon', () => ({
jest.mock('@edx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
Icon: () => 'Icon',
}));
jest.mock('@openedx/paragon/icons', () => ({
jest.mock('@edx/paragon/icons', () => ({
Download: 'DownloadIcon',
}));
jest.mock('data/services/lms', () => ({
@@ -34,19 +35,19 @@ describe('ResultsSummary component', () => {
el = shallow(<ResultsSummary {...props} />);
});
test(`snapshot - ${assertions.join(', ')}`, () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('Hyperlink has target="_blank" and rel="noopener noreferrer"', () => {
expect(el.instance.props.target).toEqual('_blank');
expect(el.instance.props.rel).toEqual('noopener noreferrer');
expect(el.props().target).toEqual('_blank');
expect(el.props().rel).toEqual('noopener noreferrer');
});
test('Hyperlink has href to bulkGradesUrl', () => {
expect(el.instance.props.href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
expect(el.props().href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
});
test('displays Download Icon and 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);
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);
});
});

View File

@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistoryTable component snapshot history table data (from bulkManagementHistory.map(this.formatHistoryRow) snapshot: maps resultsSummay to ResultsSummary, wraps filename and user, forwards the rest 1`] = `
[
{
Array [
Object {
"filename": <span
className="wrap-text-in-cell"
>
@@ -20,7 +20,7 @@ exports[`HistoryTable component snapshot history table data (from bulkManagement
Eifel
</span>,
},
{
Object {
"filename": <span
className="wrap-text-in-cell"
>
@@ -45,26 +45,26 @@ exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] =
<DataTable
className="table-striped"
columns={
[
{
Array [
Object {
"Header": "Gradebook",
"accessor": "filename",
"columnSortable": false,
"width": "col-5",
},
{
Object {
"Header": "Download Summary",
"accessor": "resultsSummary",
"columnSortable": false,
"width": "col",
},
{
Object {
"Header": "Who",
"accessor": "user",
"columnSortable": false,
"width": "col-1",
},
{
Object {
"Header": "When",
"accessor": "timeUploaded",
"columnSortable": false,
@@ -73,8 +73,8 @@ exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] =
]
}
data={
[
{
Array [
Object {
"filename": <span
className="wrap-text-in-cell"
>
@@ -92,7 +92,7 @@ exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] =
Eifel
</span>,
},
{
Object {
"filename": <span
className="wrap-text-in-cell"
>

View File

@@ -4,8 +4,8 @@ exports[`ResultsSummary component snapshot - safe hyperlink with bulkGradesUrl w
<Hyperlink
destination="www.edx.org"
href={
{
"url": {
Object {
"url": Object {
"rowId": 42,
},
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { BulkManagementHistoryView } from '.';
@@ -24,23 +24,20 @@ describe('BulkManagementHistoryView', () => {
'<HistoryTable />',
];
test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('heading - h4 loaded from messages', () => {
const heading = el.instance.findByType('h4')[0];
const expectedHeading = shallow(
const heading = el.find('h4');
expect(heading.getElement()).toEqual((
<h4>
<FormattedMessage {...messages.heading} />
</h4>,
);
expect(heading.el.type).toEqual(expectedHeading.type);
expect(heading.el.props).toEqual(expectedHeading.props);
</h4>
));
});
test('heading, then alerts, then upload form, then table', () => {
expect(el.instance.children[0].type).toEqual('h4');
expect(el.instance.children[2].type).toEqual(BulkManagementAlerts);
expect(el.instance.children[3].type).toEqual(HistoryTable);
expect(el.childAt(0).is('h4')).toEqual(true);
expect(el.childAt(2).is(BulkManagementAlerts)).toEqual(true);
expect(el.childAt(3).is(HistoryTable)).toEqual(true);
});
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Hyperlink } from '@openedx/paragon';
import { Hyperlink } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
/**

View File

@@ -1,11 +1,11 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { getConfig } from '@edx/frontend-platform';
import Header from '.';
jest.mock('@openedx/paragon', () => ({
jest.mock('@edx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
}));
jest.mock('@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 />).snapshot).toMatchSnapshot();
expect(shallow(<Header />)).toMatchSnapshot();
});
});

View File

@@ -10,7 +10,7 @@ exports[`AssignmentFilter component render snapshot 1`] = `
label="Assignment"
onChange={[MockFunction]}
options={
[
Array [
<option
value=""
>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { useIntl } from '@edx/frontend-platform/i18n';
import SelectGroup from '../SelectGroup';
@@ -38,10 +38,10 @@ describe('AssignmentFilter component', () => {
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('filter options', () => {
const { options } = el.instance.findByType(SelectGroup)[0].props;
const { options } = el.find(SelectGroup).props();
expect(options.length).toEqual(5);
const testOption = assignmentFilterOptions[0];
const optionProps = options[1].props;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Button } from '@edx/paragon';
import useAssignmentGradeFilterData from './hooks';
import messages from '../messages';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Button } from '@edx/paragon';
import PercentGroup from '../PercentGroup';
import useAssignmentGradeFilterData from './hooks';
@@ -37,20 +37,20 @@ describe('AssignmentFilter component', () => {
describe('render', () => {
describe('with selected assignment', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
it('renders a PercentGroup for both Max and Min filters', () => {
let { props } = el.instance.findByType(PercentGroup)[0];
let props = el.find(PercentGroup).at(0).props();
expect(props.value).toEqual(hookData.assignmentGradeMin);
expect(props.disabled).toEqual(false);
expect(props.onChange).toEqual(hookData.handleSetMin);
props = el.instance.findByType(PercentGroup)[1].props;
props = el.find(PercentGroup).at(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.instance.findByType(Button)[0];
const props = el.find(Button).props();
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.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
it('disables controls', () => {
let { props } = el.instance.findByType(PercentGroup)[0];
let props = el.find(PercentGroup).at(0).props();
expect(props.disabled).toEqual(true);
props = el.instance.findByType(PercentGroup)[1].props;
props = el.find(PercentGroup).at(1).props();
expect(props.disabled).toEqual(true);
});
});

View File

@@ -10,7 +10,7 @@ exports[`AssignmentFilterType component render snapshot 1`] = `
label="Assignment Types"
onChange={[MockFunction]}
options={
[
Array [
<option
value=""
>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { useIntl } from '@edx/frontend-platform/i18n';
import SelectGroup from '../SelectGroup';
@@ -34,10 +34,10 @@ describe('AssignmentFilterType component', () => {
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('filter options', () => {
const { options } = el.instance.findByType(SelectGroup)[0].props;
const { options } = el.find(SelectGroup).props();
expect(options.length).toEqual(5);
const optionProps = options[1].props;
expect(optionProps.value).toEqual(assignmentTypes[0]);

View File

@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import { Button } from '@openedx/paragon';
import { Button } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../messages';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Button } from '@edx/paragon';
import PercentGroup from '../PercentGroup';
import useCourseGradeFilterData from './hooks';
@@ -42,18 +42,18 @@ describe('CourseFilter component', () => {
describe('render', () => {
describe('with selected assignment', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
it('renders a PercentGroup for both Max and Min filters', () => {
let { props } = el.instance.findByType(PercentGroup)[0];
let props = el.find(PercentGroup).at(0).props();
expect(props.value).toEqual(hookData.min.value);
expect(props.onChange).toEqual(hookData.min.onChange);
props = el.instance.findByType(PercentGroup)[1].props;
props = el.find(PercentGroup).at(1).props();
expect(props.value).toEqual(hookData.max.value);
expect(props.onChange).toEqual(hookData.max.onChange);
});
it('renders a submit button', () => {
const { props } = el.instance.findByType(Button)[0];
const props = el.find(Button).props();
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.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
it('disables submit', () => {
const { props } = el.instance.findByType(Button)[0];
const props = el.find(Button).props();
expect(props.disabled).toEqual(true);
});
});

View File

@@ -2,7 +2,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form } from '@openedx/paragon';
import { Form } from '@edx/paragon';
const PercentGroup = ({
id,

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import PercentGroup from './PercentGroup';
@@ -22,11 +22,11 @@ describe('PercentGroup', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<PercentGroup {...props} />);
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('disabled', () => {
const el = shallow(<PercentGroup {...props} disabled />);
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
});
});

View File

@@ -2,7 +2,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form } from '@openedx/paragon';
import { Form } from '@edx/paragon';
const SelectGroup = ({
id,

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import SelectGroup from './SelectGroup';
@@ -27,11 +27,11 @@ describe('SelectGroup', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<SelectGroup {...props} />);
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('disabled', () => {
const el = shallow(<SelectGroup {...props} disabled />);
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
});
});

View File

@@ -7,7 +7,7 @@ exports[`StudentGroupsFilter component render snapshot 1`] = `
label="Tracks"
onChange={[MockFunction]}
options={
[
Array [
<option
value="Track-All"
>
@@ -43,7 +43,7 @@ exports[`StudentGroupsFilter component render snapshot 1`] = `
label="Cohorts"
onChange={[MockFunction]}
options={
[
Array [
<option
value="Cohort-All"
>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { useIntl } from '@edx/frontend-platform/i18n';
import SelectGroup from '../SelectGroup';
@@ -48,14 +48,14 @@ describe('StudentGroupsFilter component', () => {
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('track options', () => {
const {
options,
onChange,
value,
} = el.instance.findByType(SelectGroup)[0].props;
} = el.find(SelectGroup).at(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.instance.findByType(SelectGroup)[1].props;
} = el.find(SelectGroup).at(1).props();
expect(value).toEqual(props.cohorts.value);
expect(disabled).toEqual(false);
expect(onChange).toEqual(props.cohorts.handleChange);

View File

@@ -16,7 +16,7 @@ exports[`GradebookFilters render snapshot 1`] = `
className="p-1"
iconAs="Icon"
onClick={[MockFunction hook.closeMenu]}
src="Close"
src={[Function]}
/>
</div>
<Collapsible

View File

@@ -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.filterMenu.useCloseMenu();
const closeMenu = thunkActions.app.useCloseFilterMenu();
const fetchGrades = thunkActions.grades.useFetchGrades();
const handleIncludeTeamMembersChange = ({ target: { checked } }) => {

View File

@@ -9,9 +9,7 @@ jest.mock('data/redux/hooks', () => ({
filters: { useIncludeCourseRoleMembers: jest.fn() },
},
thunkActions: {
app: {
filterMenu: { useCloseMenu: jest.fn() },
},
app: { useCloseFilterMenu: jest.fn() },
grades: { useFetchGrades: jest.fn() },
},
}));
@@ -20,7 +18,7 @@ selectors.filters.useIncludeCourseRoleMembers.mockReturnValue(true);
const updateIncludeCourseRoleMembers = jest.fn();
actions.filters.useUpdateIncludeCourseRoleMembers.mockReturnValue(updateIncludeCourseRoleMembers);
const closeFilterMenu = jest.fn();
thunkActions.app.filterMenu.useCloseMenu.mockReturnValue(closeFilterMenu);
thunkActions.app.useCloseFilterMenu.mockReturnValue(closeFilterMenu);
const fetchGrades = jest.fn();
thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades);
@@ -36,7 +34,7 @@ describe('GradebookFiltersData component hooks', () => {
it('initializes hooks', () => {
expect(actions.filters.useUpdateIncludeCourseRoleMembers).toHaveBeenCalledWith();
expect(selectors.filters.useIncludeCourseRoleMembers).toHaveBeenCalledWith();
expect(thunkActions.app.filterMenu.useCloseMenu).toHaveBeenCalledWith();
expect(thunkActions.app.useCloseFilterMenu).toHaveBeenCalledWith();
expect(thunkActions.grades.useFetchGrades).toHaveBeenCalledWith();
});
});

View File

@@ -6,8 +6,8 @@ import {
Icon,
IconButton,
Form,
} from '@openedx/paragon';
import { Close } from '@openedx/paragon/icons';
} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Collapsible } from '@openedx/paragon';
import { Collapsible } from '@edx/paragon';
import { formatMessage } from 'testUtils';
@@ -49,10 +49,10 @@ describe('GradebookFilters', () => {
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('Assignment filters', () => {
expect(el.instance.findByType(Collapsible)[0].children[0]).toMatchObject(shallow(
expect(el.find(Collapsible).at(0).children()).toMatchObject(shallow(
<div>
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
<AssignmentFilter updateQueryParams={updateQueryParams} />
@@ -61,22 +61,22 @@ describe('GradebookFilters', () => {
));
});
test('CourseGrade filters', () => {
expect(el.instance.findByType(Collapsible)[1].children[0]).toMatchObject(shallow(
expect(el.find(Collapsible).at(1).children()).toMatchObject(shallow(
<CourseGradeFilter updateQueryParams={updateQueryParams} />,
));
});
test('StudentGroups filters', () => {
expect(el.instance.findByType(Collapsible)[2].children[0]).toMatchObject(shallow(
expect(el.find(Collapsible).at(2).children()).toMatchObject(shallow(
<StudentGroupsFilter updateQueryParams={updateQueryParams} />,
));
});
test('includeCourseTeamMembers', () => {
const checkbox = el.instance.findByType(Collapsible)[3].children[0];
expect(checkbox.props).toEqual({
const checkbox = el.find(Collapsible).at(3).children();
expect(checkbox.props()).toEqual({
checked: true,
onChange: hookProps.includeCourseTeamMembers.handleChange,
children: formatMessage(messages.includeCourseTeamMembers),
});
expect(checkbox.children[0].el).toEqual(formatMessage(messages.includeCourseTeamMembers));
});
});
});

View File

@@ -21,9 +21,7 @@ exports[`GradebookHeader component render default view shapshot 1`] = `
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
<h2>
test-course-id
</h2>
</div>
@@ -51,9 +49,7 @@ exports[`GradebookHeader component render frozen grades snapshot: show frozen wa
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
<h2>
test-course-id
</h2>
</div>
@@ -87,9 +83,7 @@ exports[`GradebookHeader component render show bulk management snapshot: show to
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
<h2>
test-course-id
</h2>
<Button
@@ -123,9 +117,7 @@ exports[`GradebookHeader component render user cannot view gradebook snapshot: s
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
<h2>
test-course-id
</h2>
</div>

View File

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

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Button } from '@edx/paragon';
import { instructorDashboardUrl } from 'data/services/lms/urls';
import useGradebookHeaderData from './hooks';
@@ -26,7 +26,7 @@ export const GradebookHeader = () => {
</a>
<h1>{formatMessage(messages.gradebook)}</h1>
<div className="subtitle-row d-flex justify-content-between align-items-center">
<h2 className="text-break">{courseId}</h2>
<h2>{courseId}</h2>
{showBulkManagement && (
<Button variant="tertiary" onClick={handleToggleViewClick}>
{formatMessage(toggleViewMessage)}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Button } from '@edx/paragon';
import { formatMessage } from 'testUtils';
import { instructorDashboardUrl } from 'data/services/lms/urls';
@@ -40,7 +40,7 @@ describe('GradebookHeader component', () => {
describe('render', () => {
describe('default view', () => {
test('shapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
});
describe('show bulk management', () => {
@@ -49,10 +49,10 @@ describe('GradebookHeader component', () => {
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(el).toMatchSnapshot();
const { onClick, children } = el.find(Button).props();
expect(onClick).toEqual(hookProps.handleToggleViewClick);
expect(el.instance.findByType(Button)[0].children[0].el).toEqual(formatMessage(hookProps.toggleViewMessage));
expect(children).toEqual(formatMessage(hookProps.toggleViewMessage));
});
});
describe('frozen grades', () => {
@@ -61,7 +61,7 @@ describe('GradebookHeader component', () => {
el = shallow(<GradebookHeader />);
});
test('snapshot: show frozen warning', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
});
describe('user cannot view gradebook', () => {
@@ -70,7 +70,7 @@ describe('GradebookHeader component', () => {
el = shallow(<GradebookHeader />);
});
test('snapshot: show unauthorized warning', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
});
});

View File

@@ -6,7 +6,7 @@ exports[`BulkManagementControls render snapshot - show - network and import butt
>
<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",

View File

@@ -3,7 +3,7 @@ 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 downloadBulkGradesReport = actions.grades.downloadReport.useBulkGrades();
const handleClickExportGrades = () => {
downloadBulkGradesReport();

View File

@@ -5,7 +5,7 @@ import useBulkManagementControlsData from './hooks';
jest.mock('data/redux/hooks', () => ({
actions: {
grades: {
useDownloadBulkGradesReport: jest.fn(),
downloadReport: { useBulkGrades: jest.fn() },
},
},
selectors: {
@@ -17,7 +17,7 @@ jest.mock('data/redux/hooks', () => ({
}));
const downloadBulkGrades = jest.fn();
actions.grades.useDownloadBulkGradesReport.mockReturnValue(downloadBulkGrades);
actions.grades.downloadReport.useBulkGrades.mockReturnValue(downloadBulkGrades);
const gradeExportUrl = 'test-grade-export-url';
selectors.root.useGradeExportUrl.mockReturnValue(gradeExportUrl);
selectors.root.useShowBulkManagement.mockReturnValue(true);
@@ -47,7 +47,7 @@ describe('useBulkManagementControlsData', () => {
it('initializes redux hooks', () => {
expect(selectors.root.useGradeExportUrl).toHaveBeenCalledWith();
expect(selectors.root.useShowBulkManagement).toHaveBeenCalledWith();
expect(actions.grades.useDownloadBulkGradesReport).toHaveBeenCalledWith();
expect(actions.grades.downloadReport.useBulkGrades).toHaveBeenCalledWith();
});
});
describe('output', () => {

View File

@@ -18,8 +18,7 @@ export const BulkManagementControls = () => {
handleClickExportGrades,
} = useBulkManagementControlsData();
if (!show) { return null; }
return (
return show && (
<div className="d-flex">
<NetworkButton
label={messages.downloadGradesBtn}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import useBulkManagementControlsData from './hooks';
import BulkManagementControls from '.';
@@ -22,7 +22,7 @@ describe('BulkManagementControls', () => {
});
describe('render', () => {
test('snapshot - show - network and import buttons', () => {
expect(shallow(<BulkManagementControls />).snapshot).toMatchSnapshot();
expect(shallow(<BulkManagementControls />)).toMatchSnapshot();
});
test('snapshot - empty if show is not truthy', () => {
useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false });

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import HistoryHeader from './HistoryHeader';
@@ -11,7 +11,7 @@ describe('HistoryHeader', () => {
};
describe('Component', () => {
test('snapshot', () => {
expect(shallow(<HistoryHeader {...props} />).snapshot).toMatchSnapshot();
expect(shallow(<HistoryHeader {...props} />)).toMatchSnapshot();
});
});
});

View File

@@ -1,53 +1,68 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import { selectors } from 'data/redux/hooks';
import selectors from 'data/selectors';
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 = () => {
const { assignmentName, updateUserName } = selectors.app.useModalData();
const { gradeOverrideCurrentEarnedGradedOverride, gradeOriginalEarnedGraded } = 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={gradeOriginalEarnedGraded}
/>
<HistoryHeader
id={HistoryKeys.currentGrade}
label={formatMessage(messages.currentGradeHeader)}
value={gradeOverrideCurrentEarnedGradedOverride}
/>
</div>
);
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 default ModalHeaders;
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);

View File

@@ -1,84 +1,93 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { useIntl } from '@edx/frontend-platform/i18n';
import { selectors } from 'data/redux/hooks';
import selectors from 'data/selectors';
import { formatMessage } from 'testUtils';
import HistoryHeader from './HistoryHeader';
import ModalHeaders, { HistoryKeys } from './ModalHeaders';
import messages from './messages';
import {
ModalHeaders,
mapStateToProps,
} from './ModalHeaders';
jest.mock('./HistoryHeader', () => 'HistoryHeader');
jest.mock('data/redux/hooks', () => ({
selectors: {
app: { useModalData: jest.fn() },
grades: { useGradeData: jest.fn() },
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 })),
},
},
}));
const modalData = {
assignmentName: 'test-assignment-name',
updateUserName: 'test-user-name',
};
selectors.app.useModalData.mockReturnValue(modalData);
const gradeData = {
gradeOverrideCurrentEarnedGradedOverride: 'test-current-grade',
gradeOriginalEarnedGraded: '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();
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();
});
});
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
describe('mapStateToProps', () => {
const testState = { he: 'lives in a', pineapple: 'under the sea' };
let mapped;
beforeEach(() => {
mapped = mapStateToProps(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('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('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.gradeOverrideCurrentEarnedGradedOverride', () => {
expect(mapped.currentGrade).toEqual(
selectors.grades.gradeOverrideCurrentEarnedGradedOverride(testState),
);
});
});
test('originalGrade header', () => {
const headerProps = el.instance.findByType(HistoryHeader)[2].props;
expect(headerProps).toMatchObject({
id: HistoryKeys.originalGrade,
label: formatMessage(messages.originalGradeHeader),
value: gradeData.gradeOriginalEarnedGraded,
});
});
test('currentGrade header', () => {
const headerProps = el.instance.findByType(HistoryHeader)[3].props;
expect(headerProps).toMatchObject({
id: HistoryKeys.currentGrade,
label: formatMessage(messages.currentGradeHeader),
value: gradeData.gradeOverrideCurrentEarnedGradedOverride,
describe('originalGrade', () => {
test('from grades.gradeOriginalEarnedGrades', () => {
expect(mapped.originalGrade).toEqual(
selectors.grades.gradeOriginalEarnedGraded(testState),
);
});
});
});

View File

@@ -0,0 +1,65 @@
/* 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);

View File

@@ -0,0 +1,92 @@
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);
});
});
});

View File

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

View File

@@ -1,21 +0,0 @@
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;

View File

@@ -1,67 +0,0 @@
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 });
});
});
});
});

View File

@@ -1,33 +0,0 @@
import React from 'react';
import { Form } from '@openedx/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;

View File

@@ -1,38 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Form } from '@openedx/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);
});
});
});

View File

@@ -0,0 +1,55 @@
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);

View File

@@ -0,0 +1,90 @@
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);
});
});
});

View File

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

View File

@@ -1,25 +0,0 @@
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;

View File

@@ -1,63 +0,0 @@
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 });
});
});
});
});

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { Form } from '@openedx/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;

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Form } from '@openedx/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);
});
});
});

View File

@@ -1,26 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import useReasonInputData from './hooks';
import ReasonInput, { controlTestId } from '.';
jest.unmock('react');
jest.unmock('@openedx/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);
});
});

View File

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

View File

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

View File

@@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`OverrideTable component render snapshot 1`] = `
<DataTable
columns="test-columns"
data={
[
{
"test": "data",
},
{
"andOther": "test-data",
},
{
"adjustedGrade": <AdjustedGradeInput />,
"date": {
"formatted": 2000-01-01T00:00:00.000Z,
},
"reason": <ReasonInput />,
},
]
}
itemCount={3}
/>
`;

View File

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

View File

@@ -1,26 +0,0 @@
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;

View File

@@ -1,78 +0,0 @@
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 });
});
});
});
});

View File

@@ -1,42 +1,73 @@
/* 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 '@openedx/paragon';
import { DataTable } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { formatDateForDisplay } from 'utils';
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
import selectors from 'data/selectors';
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 = () => {
const { hide, columns, data } = useOverrideTableData();
if (hide) { return null; }
const tableData = [
...data,
{
adjustedGrade: <AdjustedGradeInput />,
date: formatDateForDisplay(new Date()),
reason: <ReasonInput />,
},
];
export const OverrideTable = ({
hide,
gradeOverrides,
todaysDate,
}) => {
if (hide) {
return null;
}
return (
<DataTable
columns={columns}
data={tableData}
itemCount={tableData.length}
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,
},
]}
data={[
...gradeOverrides,
{
adjustedGrade: <AdjustedGradeInput />,
date: todaysDate,
reason: <ReasonInput />,
},
]}
itemCount={gradeOverrides.length}
/>
);
};
OverrideTable.propTypes = {};
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,
};
export default OverrideTable;
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);

View File

@@ -1,65 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { DataTable } from '@openedx/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 />,
});
});
});
});

View File

@@ -0,0 +1,81 @@
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));
});
});
});
});

View File

@@ -1,26 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ModalHeaders render snapshot 1`] = `
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
<div>
<HistoryHeader
id="assignment"
label="Assignment"
value="test-assignment-name"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Edit Modal Assignment header"
id="gradebook.GradesView.EditModal.headers.assignment"
/>
}
value="Qwerty"
/>
<HistoryHeader
id="student"
label="Student"
value="test-user-name"
label={
<FormattedMessage
defaultMessage="Student"
description="Edit Modal Student header"
id="gradebook.GradesView.EditModal.headers.student"
/>
}
value="Uiop"
/>
<HistoryHeader
id="original-grade"
label="Original Grade"
value="test-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="Current Grade"
value="test-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}
/>
</div>
`;

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
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;

View File

@@ -1,68 +0,0 @@
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();
});
});
});

View File

@@ -1,17 +1,23 @@
/* 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,
Alert,
ModalDialog,
ActionRow,
} from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
} from '@edx/paragon';
import { FormattedMessage, injectIntl, intlShape } 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 />
@@ -22,48 +28,87 @@ import messages from './messages';
* adjusting the grade.
* (also provides a close button that clears the modal state)
*/
export const EditModal = () => {
const { formatMessage } = useIntl();
const {
onClose,
error,
handleAdjustedGradeClick,
isOpen,
} = useEditModalData();
export class EditModal extends React.Component {
constructor(props) {
super(props);
this.closeAssignmentModal = this.closeAssignmentModal.bind(this);
this.handleAdjustedGradeClick = this.handleAdjustedGradeClick.bind(this);
}
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>
closeAssignmentModal() {
this.props.doneViewingAssignment();
this.props.closeModal();
}
<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>
);
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: '',
};
export default EditModal;
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));

View File

@@ -1,128 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import {
ActionRow,
ModalDialog,
} from '@openedx/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();
});
});
});

View File

@@ -0,0 +1,130 @@
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);
});
});
});

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