Compare commits
36 Commits
open-relea
...
mashal-m/b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a60955566 | ||
|
|
37966e86df | ||
|
|
03fa143fc1 | ||
|
|
075846f869 | ||
|
|
1208d27d92 | ||
|
|
e345716bd4 | ||
|
|
2121a63c83 | ||
|
|
47cab71b3c | ||
|
|
2d8af2ec00 | ||
|
|
d55abbe91e | ||
|
|
a75f365bdd | ||
|
|
bbb7e895a5 | ||
|
|
bf70fd1450 | ||
|
|
af2ece8290 | ||
|
|
620827d772 | ||
|
|
c6a4685bf5 | ||
|
|
8dd2237f9c | ||
|
|
97c58157f8 | ||
|
|
ce093efba4 | ||
|
|
799ef5b8a1 | ||
|
|
f956351cf7 | ||
|
|
7772e21c6a | ||
|
|
f07a96ce58 | ||
|
|
f64bc8d4a6 | ||
|
|
134dabb710 | ||
|
|
65c25f00b6 | ||
|
|
31748e246e | ||
|
|
650be29ef9 | ||
|
|
b713ab5748 | ||
|
|
5fe80b4a52 | ||
|
|
9e04813d06 | ||
|
|
a0e1a60d23 | ||
|
|
68c7944dd5 | ||
|
|
f4f6e5551f | ||
|
|
ee99bfdaa4 | ||
|
|
318ce349fc |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -3,4 +3,4 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence, they will
|
||||
# be requested for review when someone opens a pull request.
|
||||
* @edx/masters-devs-gta
|
||||
* @openedx/content-aurora
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -26,4 +26,4 @@ Collectively, these should be completed by reviewers of this PR:
|
||||
- [ ] I've tested the new functionality
|
||||
|
||||
|
||||
FYI: @edx/masters-devs-gta
|
||||
FYI: @openedx/content-aurora
|
||||
|
||||
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Run the workflow that adds new tickets that are either:
|
||||
# - labelled "DEPR"
|
||||
# - title starts with "[DEPR]"
|
||||
# - body starts with "Proposal Date" (this is the first template field)
|
||||
# to the org-wide DEPR project board
|
||||
|
||||
name: Add newly created DEPR issues to the DEPR project board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
routeissue:
|
||||
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
64
.github/workflows/ci.yml
vendored
Normal file
64
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: node_js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
npm: [8.5.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install npm 8.5.x
|
||||
run: npm install -g npm@${{ matrix.npm }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Unit Tests
|
||||
run: npm run test
|
||||
|
||||
- name: Validate Package Lock
|
||||
run: make validate-no-uncommitted-package-lock-changes
|
||||
|
||||
- name: Run Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run Test
|
||||
run: npm run test
|
||||
|
||||
- name: Run Build
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
uses: dawidd6/action-send-mail@v3
|
||||
with:
|
||||
server_address: email-smtp.us-east-1.amazonaws.com
|
||||
server_port: 465
|
||||
username: ${{secrets.EDX_SMTP_USERNAME}}
|
||||
password: ${{secrets.EDX_SMTP_PASSWORD}}
|
||||
subject: CI workflow failed in ${{github.repository}}
|
||||
to: masters-grades@edx.org
|
||||
from: github-actions <github-actions@edx.org>
|
||||
body: CI workflow in ${{github.repository}} failed! For details see "github.com/${{
|
||||
github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
#check package-lock file version
|
||||
|
||||
name: Lockfile Version check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
33
.github/workflows/npm-publish.yml
vendored
Normal file
33
.github/workflows/npm-publish.yml
vendored
Normal 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
|
||||
@@ -1,7 +1,6 @@
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.travis.yml
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
Makefile
|
||||
|
||||
28
.travis.yml
28
.travis.yml
@@ -1,28 +0,0 @@
|
||||
language: node_js
|
||||
node_js: 12
|
||||
notifications:
|
||||
email:
|
||||
recipients:
|
||||
- masters-grades@edx.org
|
||||
on_success: never
|
||||
on_failure: always
|
||||
webhooks: https://www.travisbuddy.com/
|
||||
on_success: never
|
||||
before_install:
|
||||
- npm install -g greenkeeper-lockfile@1.14.0
|
||||
install:
|
||||
- npm ci
|
||||
before_script: greenkeeper-lockfile-update
|
||||
after_script: greenkeeper-lockfile-upload
|
||||
script:
|
||||
- make validate-no-uncommitted-package-lock-changes
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run build
|
||||
after_success:
|
||||
- npm run travis-deploy-once "npm run semantic-release"
|
||||
- npm run coveralls
|
||||
env:
|
||||
global:
|
||||
- secure: bBLQZVw1aVUxB7GFNXGrdKeztyFrCCJusVgFcSuej9S4qmj9/jrVsEc9dEcH+BMS+b49+SvILoxzd6ZYLaRygQLzevnO1/dX596DeCKVK48PTTZRsNyafaSMCkxNKqEmRcA9hYL52xJJ5GpKo7ViWsFy8VFgUfZEJxQi8/lYbfQ1vlXRpo2LJfJh09v85roSXdQmajyGJ1Dz6elcwUX5B+BgXmIHizJXUMfFci61xTEZmgKtfeCiwFQA5pCvVMHBQhgySqT2N3eRESzRt2jAfAdcRKBYXS0rwKymdlL1ZF349Jm8xwtqm19Fwsut21181Lnn6FmccMWhQ7man3WH1xfT0ahmHNs1KJMyZcwRJd/gDfbd6iD3LB9Pt9hEQ00Qh/m7MYeahMxTEL9bp2TyILi8cTP91jeBUHCExCdv2jRrUQEnUS5vZUYRdM8CR2DLoLmNh3APndKzwgr5U8rh6RdhbQBJp97Hb/YYVrBiP2atLJAaYPY/xEQHK/YoXelQgiZ6wHBMV+tF/L0ZRn7KyVWdkbBKWfbEjRKbEJD9WD+V7HayMR81tm5CSqlrG8mTvSy2boIGiX14GV11ZEfMj5bjb6W41BW+QGqQerZvmwk/4ywe304X85PD0OBhIYPRzeLIi0Gt6lD1aOpVxgm4M03tdgYQzCPWRPq32CB+1IA=
|
||||
- secure: w1d/E+cc4+Bf017Jpp9YsKBzLSZw9sqKZGeM2tNrO6eJZbMJqfKTmfUrRw8BoLh1Z8YRkHF7RADDy3ln7XEdeAX3j9OoC3Cz0zN6iDX6TPcI461NuOIscJYb4tyFcuWm6FhgVlBAlo/BI3q+zqKwjfWuDaORpk6+haacCmvTe5V0vWhY+MYT7M+LfnKeKVzhI4magGt8jPTE21oziIFwCqCCjJc4+AmsWoWTzU0Q7Db0DZiJnLXFfXybLbkedAgJmcSgEGZCSpaZIOkX0/Lbazsz1Ky4KASfkrYT1Z5iKQ8TE3skmx1IIu+1egN8iBbdrY+NhvV24RkT+rpUvD7TBIHTrjQ5JYLe0kGjN70vG7YlKgjNSyTjkrEd7fCKpuIol3DVjBRz3tV5aCl0t/A8mIPqKyNI94MamWsExpqsxgcb9vBVno5caZvD8ZXNrGNqanB3MSoLGxZTLKif9u+AZfLnB3xtjaiJg3/BNoWaOBPlp/M6BvGIGHElwvLrAhUvl8wzrwJcQQWpmRMh0b6enr6Y7ox/mGGs7NBCT+CNKEsWeCfY4thZzgi6/GocXyqdTpXMkNSI1PDoPmi+vKafBd+7aAYbcUlJBTU6TAxyncln0tF2JF+ghTZ0v8nNzEQ9VmV4ddyoOHx6YnHvEcenWZGMROQnMCVifyDbaHpPbPI=
|
||||
@@ -1,9 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[edx-platform.frontend-app-gradebook]
|
||||
[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
|
||||
type = KEYVALUEJSON
|
||||
|
||||
|
||||
6
Makefile
6
Makefile
@@ -14,7 +14,7 @@ tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transi
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test is-es5
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
.PHONY: test
|
||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||
@@ -57,9 +57,9 @@ push_translations:
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
||||
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||
|
||||
# This target is used by Travis.
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
git diff --exit-code package-lock.json
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[](https://travis-ci.com/edx/frontend-app-gradebook) [](https://coveralls.io/github/edx/frontend-app-gradebook)
|
||||
[](https://travis-ci.com/edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Travis Configuration
|
||||
|
||||
Your project might have different build requirements - however, this project's `.travis.yml` configuration is supposed to represent a good starting point.
|
||||
|
||||
## Node JS Version
|
||||
|
||||
The minimum `Node` and `npm` versions that edX supports is `8.9.3` and `5.5.1`, respectively.
|
||||
|
||||
## Caching node_modules
|
||||
|
||||
While [the `Travis` blog](https://blog.travis-ci.com/2016-11-21-travis-ci-now-supports-yarn) recommends
|
||||
|
||||
```yaml
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
```
|
||||
|
||||
this causes issues when testing different versions of `Node` because [`node_modules` will store the compiled native modules](https://stackoverflow.com/a/42523517/5225575).
|
||||
|
||||
Caching the `~/.npm` directory avoids storing these native modules.
|
||||
|
||||
## Notifications
|
||||
|
||||
This project uses a service called [`TravisBuddy`](https://www.travisbuddy.com/), which provides Travis build context within a PR via webhooks (configured only to add feedback for build failures).
|
||||
|
||||

|
||||
|
||||
## Installing `greenkeeper-lockfile`
|
||||
|
||||
As explained in [the `Greenkeeper` documentation](https://greenkeeper.io/docs.html#greenkeeper-step-by-step), `Greenkeeper` is a service that keeps track of your project's dependencies, and will, for example, automatically open PRs with an updated `package.json` file when the latest version of a dependency is a major version ahead of the existing dependency version in your `package.json` file.
|
||||
|
||||
This automated updating is great, but `Greenkeeper` does not update your `package-lock.json` file, just your `package.json` file. This makes sense, as the only way to update the `package-lock.json` file would be to run `npm install` when building your project, using the latest `package.json`, and then committing the updated `package-lock.json` file.
|
||||
|
||||
This is essentially what you have to do manually when `Greenkeeper` opens a PR - `git checkout` the branch, `npm install` locally, `git commit` the `package-lock.json` changes, and then `git push` those changes to the `Greenkeeper` branch on `origin`. It's fun probably only the first time, and even then it gets old, fast.
|
||||
|
||||
What [`greenkeeper-lockfile`](https://github.com/greenkeeperio/greenkeeper-lockfile) does is that it automates the previous steps as part of the build process.
|
||||
|
||||
It will
|
||||
* Check that the branch is a `Greenkeeper` branch
|
||||
* Update the lockfile
|
||||
* Push a commit with the updated lockfile back to the Greenkeeper branch
|
||||
|
||||
This is why it's important to install `greenkeeper-lockfile` in the `before_install` step, and since it's used exclusively only in the Travis Build, why it's not part of the package's dependencies.
|
||||
|
||||
## Scripts
|
||||
|
||||
Most of the `script`s are self-explanatory - you probably want to fail a build if there are linting violations, or if any tests don't pass, or if it cannot compile your files.
|
||||
|
||||
However, there are a couple additional `script`s that might seem less self-explanatory.
|
||||
|
||||
### What the heck is `make validate-no-uncommitted-package-lock-changes`?
|
||||
|
||||
There are only two requirements for a good `make target` name
|
||||
|
||||
1. Definitely make it really verbose so people can't remember what it's called
|
||||
2. Definitely don't not use a double-negative
|
||||
|
||||
What `make validate-no-uncommitted-package-lock-changes` does is `git diff`s for any `package-lock.json` file changes in your project. It's important to remember that all build `script`s are executed in Travis *after* the `install` step (aka post-`npm install`).
|
||||
|
||||
This is important because `npm` uses the pinned dependencies in your `package-lock.json` file to build the `node_modules` directory. However, the dependencies defined within the `package.json` file can be modified manually, for example, to become misaligned with the dependencies defined within the `package-lock.json`. So when `npm install` executes, the `package-lock.json` file will be updated to mirror the modified `package.json` changes.
|
||||
|
||||
However, when these changes surface within a Travis build, this indicates differing dependency expectations between the committed `package.json` file and the `package-lock.json` file, which is a good reason to fail a build.
|
||||
|
||||
### What is this `npm run is-es5` check?
|
||||
|
||||
This project outputs production files to the `dist` folder. The `npm script`, `npm run is-es5`, checks the JavaScript files in the `dist` folder to make sure that they are `ES5`-compliant.
|
||||
|
||||
This check is important because `ES5` JavaScript has [greater browser compatibility](http://kangax.github.io/compat-table/es5/) than [`ES2015+`](http://kangax.github.io/compat-table/es6/) - particularly for `IE11`.
|
||||
|
||||
### `deploy` step
|
||||
|
||||
How your project deploys will probably differ between the cookie cutter and your own application.
|
||||
|
||||
For demonstrational purposes, the cookie cutter deploys to GitHub pages using [`Travis`'s GitHub pages configuration](https://docs.travis-ci.com/user/deployment/pages/).
|
||||
|
||||
Your application might deploy to an `S3` bucket or to `npm`.
|
||||
36
documentation/CI.md
Executable file
36
documentation/CI.md
Executable file
@@ -0,0 +1,36 @@
|
||||
# CI Configuration
|
||||
|
||||
Your project might have different build requirements - however, this project's `.github/ci.yml` configuration is supposed to represent a good starting point.
|
||||
|
||||
## Node JS Version
|
||||
|
||||
The minimum `Node` and `npm` versions that edX supports is `8.9.3` and `5.5.1`, respectively.
|
||||
|
||||
## Scripts
|
||||
|
||||
Most of the `script`s are self-explanatory - you probably want to fail a build if there are linting violations, or if any tests don't pass, or if it cannot compile your files.
|
||||
|
||||
However, there are a couple additional `script`s that might seem less self-explanatory.
|
||||
|
||||
### What the heck is `make validate-no-uncommitted-package-lock-changes`?
|
||||
|
||||
There are only two requirements for a good `make target` name
|
||||
|
||||
1. Definitely make it really verbose so people can't remember what it's called
|
||||
2. Definitely don't not use a double-negative
|
||||
|
||||
What `make validate-no-uncommitted-package-lock-changes` does is `git diff`s for any `package-lock.json` file changes in your project. It's important to remember that all build `script`s are executed in CI *after* the `install` step (aka post-`npm install`).
|
||||
|
||||
This is important because `npm` uses the pinned dependencies in your `package-lock.json` file to build the `node_modules` directory. However, the dependencies defined within the `package.json` file can be modified manually, for example, to become misaligned with the dependencies defined within the `package-lock.json`. So when `npm install` executes, the `package-lock.json` file will be updated to mirror the modified `package.json` changes.
|
||||
|
||||
However, when these changes surface within a CI build, this indicates differing dependency expectations between the committed `package.json` file and the `package-lock.json` file, which is a good reason to fail a build.
|
||||
|
||||
This check is important because `ES5` JavaScript has [greater browser compatibility](http://kangax.github.io/compat-table/es5/) than [`ES2015+`](http://kangax.github.io/compat-table/es6/) - particularly for `IE11`.
|
||||
|
||||
### `deploy` step
|
||||
|
||||
How your project deploys will probably differ between the cookie cutter and your own application.
|
||||
|
||||
For demonstrational purposes, the cookie cutter deploys to GitHub pages using [ GitHUb CI ].
|
||||
|
||||
Your application might deploy to an `S3` bucket or to `npm`.
|
||||
@@ -11,5 +11,6 @@ module.exports = createConfig('jest', {
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
'testUtils', // don't unit test jest mocking tools
|
||||
],
|
||||
});
|
||||
|
||||
62571
package-lock.json
generated
62571
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.4.47",
|
||||
"version": "1.5.0",
|
||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -8,8 +8,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
@@ -17,8 +15,7 @@
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"watch-tests": "jest --watch",
|
||||
"travis-deploy-once": "travis-deploy-once"
|
||||
"watch-tests": "jest --watch"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -26,12 +23,16 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
|
||||
"@edx/frontend-component-footer": "10.1.6",
|
||||
"@edx/frontend-component-header": "2.2.5",
|
||||
"@edx/frontend-platform": "1.9.5",
|
||||
"@edx/paragon": "14.16.4",
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-component-footer": "^11.1.1",
|
||||
"@edx/frontend-component-header": "^3.1.1",
|
||||
"@edx/frontend-platform": "2.3.0",
|
||||
"@edx/paragon": "19.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.11.2",
|
||||
@@ -45,13 +46,12 @@
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "4.10.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "6.13.0",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-intl": "^2.9.0",
|
||||
"react-redux": "^5.1.1",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
@@ -61,16 +61,15 @@
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"sass": "^1.49.0",
|
||||
"util": "^0.12.3",
|
||||
"whatwg-fetch": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "5.5.2",
|
||||
"@edx/frontend-build": "9.1.1",
|
||||
"axios": "0.21.1",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
"codecov": "^3.6.1",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"es-check": "^2.3.0",
|
||||
"fetch-mock": "^6.5.2",
|
||||
"husky": "2.7.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
@@ -79,7 +78,6 @@
|
||||
"react-test-renderer": "^16.10.1",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"semantic-release": "^17.2.3",
|
||||
"travis-deploy-once": "^5.0.11"
|
||||
"semantic-release": "^17.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import { bulkManagementColumns } from 'data/constants/app';
|
||||
import selectors from 'data/selectors';
|
||||
@@ -30,14 +30,13 @@ export const mapHistoryRows = ({
|
||||
export const HistoryTable = ({
|
||||
bulkManagementHistory,
|
||||
}) => (
|
||||
<>
|
||||
<Table
|
||||
data={bulkManagementHistory.map(mapHistoryRows)}
|
||||
hasFixedColumnWidths
|
||||
columns={bulkManagementColumns}
|
||||
className="table-striped"
|
||||
/>
|
||||
</>
|
||||
<DataTable
|
||||
data={bulkManagementHistory.map(mapHistoryRows)}
|
||||
hasFixedColumnWidths
|
||||
columns={bulkManagementColumns}
|
||||
className="table-striped"
|
||||
itemCount={bulkManagementHistory.length}
|
||||
/>
|
||||
);
|
||||
HistoryTable.defaultProps = {
|
||||
bulkManagementHistory: [],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { bulkManagementColumns } from 'data/constants/app';
|
||||
@@ -9,13 +9,12 @@ import { bulkManagementColumns } from 'data/constants/app';
|
||||
import ResultsSummary from './ResultsSummary';
|
||||
import { HistoryTable, mapStateToProps } from './HistoryTable';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
}));
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Table: () => 'Table',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -62,7 +61,7 @@ describe('HistoryTable', () => {
|
||||
describe('history table', () => {
|
||||
let table;
|
||||
beforeEach(() => {
|
||||
table = el.find(Table);
|
||||
table = el.find(DataTable);
|
||||
});
|
||||
describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => {
|
||||
const fieldAssertions = [
|
||||
|
||||
@@ -42,78 +42,77 @@ Array [
|
||||
`;
|
||||
|
||||
exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = `
|
||||
<Fragment>
|
||||
<Table
|
||||
className="table-striped"
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "filename",
|
||||
"label": "Gradebook",
|
||||
"width": "col-5",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "resultsSummary",
|
||||
"label": "Download Summary",
|
||||
"width": "col",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "user",
|
||||
"label": "Who",
|
||||
"width": "col-1",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "timeUploaded",
|
||||
"label": "When",
|
||||
"width": "col",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
blue.png
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="Da Bu Dee"
|
||||
rowId={12}
|
||||
text="Da ba daa"
|
||||
/>,
|
||||
"timeUploaded": "65",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Eifel
|
||||
</span>,
|
||||
},
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
allStar.jpg
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="rockstar"
|
||||
rowId={2}
|
||||
text="all that glitters is gold"
|
||||
/>,
|
||||
"timeUploaded": "2000s?",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Smashmouth
|
||||
</span>,
|
||||
},
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
/>
|
||||
</Fragment>
|
||||
<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,
|
||||
"width": "col",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
blue.png
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="Da Bu Dee"
|
||||
rowId={12}
|
||||
text="Da ba daa"
|
||||
/>,
|
||||
"timeUploaded": "65",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Eifel
|
||||
</span>,
|
||||
},
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
allStar.jpg
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="rockstar"
|
||||
rowId={2}
|
||||
text="all that glitters is gold"
|
||||
/>,
|
||||
"timeUploaded": "2000s?",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Smashmouth
|
||||
</span>,
|
||||
},
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -31,14 +31,14 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Connect(AssignmentTypeFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
<AssignmentTypeFilter
|
||||
updateQueryParams={[MockFunction this.props.updateQueryParams]}
|
||||
/>
|
||||
<Connect(AssignmentFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
<AssignmentFilter
|
||||
updateQueryParams={[MockFunction this.props.updateQueryParams]}
|
||||
/>
|
||||
<Connect(AssignmentGradeFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
<AssignmentGradeFilter
|
||||
updateQueryParams={[MockFunction this.props.updateQueryParams]}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
@@ -53,8 +53,8 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Connect(CourseGradeFilter)
|
||||
updateQueryParams={[MockFunction]}
|
||||
<CourseGradeFilter
|
||||
updateQueryParams={[MockFunction this.props.updateQueryParams]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
@@ -68,8 +68,8 @@ exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InjectIntl(ShimmedIntlComponent)
|
||||
updateQueryParams={[MockFunction]}
|
||||
<StudentGroupsFilter
|
||||
updateQueryParams={[MockFunction this.props.updateQueryParams]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
|
||||
@@ -22,6 +22,11 @@ jest.mock('@edx/paragon', () => ({
|
||||
jest.mock('@edx/paragon/icons', () => ({
|
||||
Close: 'paragon.icons.Close',
|
||||
}));
|
||||
jest.mock('./AssignmentTypeFilter', () => 'AssignmentTypeFilter');
|
||||
jest.mock('./AssignmentFilter', () => 'AssignmentFilter');
|
||||
jest.mock('./AssignmentGradeFilter', () => 'AssignmentGradeFilter');
|
||||
jest.mock('./CourseGradeFilter', () => 'CourseGradeFilter');
|
||||
jest.mock('./StudentGroupsFilter', () => 'StudentGroupsFilter');
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -50,7 +55,7 @@ describe('GradebookFilters', () => {
|
||||
closeMenu: jest.fn().mockName('this.props.closeMenu'),
|
||||
fetchGrades: jest.fn(),
|
||||
updateIncludeCourseRoleMembers: jest.fn(),
|
||||
updateQueryParams: jest.fn(),
|
||||
updateQueryParams: jest.fn().mockName('this.props.updateQueryParams'),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const messages = defineMessages({
|
||||
downloadGradesBtn: {
|
||||
id: 'gradebook.GradesView.BulkManagementControls.bulkManagementLabel',
|
||||
defaultMessage: 'Download Grades',
|
||||
description: 'Button text for bulk grades download control in GradesView',
|
||||
description: 'A labeled button that allows an admin user to download course grades all at once (in bulk).',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Form } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
/**
|
||||
* <AdjustedGradeInput />
|
||||
@@ -20,32 +21,19 @@ export class AdjustedGradeInput extends React.Component {
|
||||
}
|
||||
|
||||
onChange = ({ target }) => {
|
||||
let adjustedGradeValue;
|
||||
switch (true) {
|
||||
case target.value < 0:
|
||||
adjustedGradeValue = 0;
|
||||
break;
|
||||
case this.props.possibleGrade && target.value > this.props.possibleGrade:
|
||||
adjustedGradeValue = this.props.possibleGrade;
|
||||
break;
|
||||
default:
|
||||
adjustedGradeValue = target.value;
|
||||
}
|
||||
this.props.setModalState({ adjustedGradeValue });
|
||||
this.props.setModalState({ adjustedGradeValue: target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span>
|
||||
<Form.Control
|
||||
type="number"
|
||||
type="text"
|
||||
name="adjustedGradeValue"
|
||||
min="0"
|
||||
max={this.props.possibleGrade ? this.props.possibleGrade : ''}
|
||||
value={this.props.value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
{this.props.possibleGrade && ` / ${this.props.possibleGrade}`}
|
||||
{this.props.possibleGrade && ` ${isRtl(getLocale()) ? '\\' : '/'} ${this.props.possibleGrade}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,34 +54,9 @@ describe('AdjustedGradeInput', () => {
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('onChange', () => {
|
||||
it('calls props.setModalState event target value with correct value', () => {
|
||||
const value = 3;
|
||||
el.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
adjustedGradeValue: value,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls props.setModalState event target value with a value more then the possibleGrade value', () => {
|
||||
it('calls props.setModalState event target value', () => {
|
||||
const value = 42;
|
||||
el.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
adjustedGradeValue: props.possibleGrade,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls props.setModalState event target value with less then 0', () => {
|
||||
const value = -5;
|
||||
el.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
adjustedGradeValue: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls props.setModalState event target value without possibleGrade value', () => {
|
||||
const value = 100;
|
||||
const newEl = shallow(<AdjustedGradeInput {...props} possibleGrade={null} />);
|
||||
newEl.instance().onChange({ target: { value } });
|
||||
expect(props.setModalState).toHaveBeenCalledWith({
|
||||
adjustedGradeValue: value,
|
||||
});
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
exports[`AdjustedGradeInput Component snapshots displays input control and "out of possible grade" label 1`] = `
|
||||
<span>
|
||||
<Control
|
||||
max={5}
|
||||
min="0"
|
||||
name="adjustedGradeValue"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
type="number"
|
||||
type="text"
|
||||
value={1}
|
||||
/>
|
||||
/ 5
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = `
|
||||
<Table
|
||||
<DataTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"key": "date",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Date"
|
||||
description="Edit Modal Override Table Date column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.dateHeader"
|
||||
/>,
|
||||
"accessor": "date",
|
||||
},
|
||||
Object {
|
||||
"key": "grader",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Grader"
|
||||
description="Edit Modal Override Table Grader column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.graderHeader"
|
||||
/>,
|
||||
"accessor": "grader",
|
||||
},
|
||||
Object {
|
||||
"key": "reason",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Reason"
|
||||
description="Edit Modal Override Table Reason column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.reasonHeader"
|
||||
/>,
|
||||
"accessor": "reason",
|
||||
},
|
||||
Object {
|
||||
"key": "adjustedGrade",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Adjusted grade"
|
||||
description="Edit Modal Override Table Adjusted grade column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader"
|
||||
/>,
|
||||
"accessor": "adjustedGrade",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -59,5 +59,6 @@ exports[`OverrideTable Component snapshots basic snapshot shows a row for each e
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
@@ -27,14 +27,14 @@ export const OverrideTable = ({
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Table
|
||||
<DataTable
|
||||
columns={[
|
||||
{ label: <FormattedMessage {...messages.dateHeader} />, key: columns.date },
|
||||
{ label: <FormattedMessage {...messages.graderHeader} />, key: columns.grader },
|
||||
{ label: <FormattedMessage {...messages.reasonHeader} />, key: columns.reason },
|
||||
{ Header: <FormattedMessage {...messages.dateHeader} />, accessor: columns.date },
|
||||
{ Header: <FormattedMessage {...messages.graderHeader} />, accessor: columns.grader },
|
||||
{ Header: <FormattedMessage {...messages.reasonHeader} />, accessor: columns.reason },
|
||||
{
|
||||
label: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
key: columns.adjustedGrade,
|
||||
Header: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
accessor: columns.adjustedGrade,
|
||||
},
|
||||
]}
|
||||
data={[
|
||||
@@ -45,6 +45,7 @@ export const OverrideTable = ({
|
||||
reason: <ReasonInput />,
|
||||
},
|
||||
]}
|
||||
itemCount={gradeOverrides.length}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
mapStateToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({ Table: () => 'Table' }));
|
||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
||||
jest.mock('./ReasonInput', () => 'ReasonInput');
|
||||
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
|
||||
body={
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog="Weve been trying to contact you regarding..."
|
||||
<Alert
|
||||
dismissible={false}
|
||||
open={true}
|
||||
/>
|
||||
show={true}
|
||||
variant="danger"
|
||||
>
|
||||
Weve been trying to contact you regarding...
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
@@ -66,12 +67,13 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
|
||||
body={
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog=""
|
||||
<Alert
|
||||
dismissible={false}
|
||||
open={false}
|
||||
/>
|
||||
show={false}
|
||||
variant="danger"
|
||||
>
|
||||
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div>
|
||||
<FormattedMessage
|
||||
|
||||
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
StatusAlert,
|
||||
Alert,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -53,12 +53,13 @@ export class EditModal extends React.Component {
|
||||
body={(
|
||||
<div>
|
||||
<ModalHeaders />
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog={this.props.gradeOverrideHistoryError}
|
||||
open={!!this.props.gradeOverrideHistoryError}
|
||||
<Alert
|
||||
variant="danger"
|
||||
show={!!this.props.gradeOverrideHistoryError}
|
||||
dismissible={false}
|
||||
/>
|
||||
>
|
||||
{this.props.gradeOverrideHistoryError}
|
||||
</Alert>
|
||||
<OverrideTable />
|
||||
<div><FormattedMessage {...messages.visibility} /></div>
|
||||
<div><FormattedMessage {...messages.saveVisibility} /></div>
|
||||
|
||||
@@ -16,7 +16,7 @@ jest.mock('./ModalHeaders', () => 'ModalHeaders');
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
Modal: () => 'Modal',
|
||||
StatusAlert: () => 'StatusAlert',
|
||||
Alert: () => 'Alert',
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
|
||||
@@ -4,7 +4,7 @@ const messages = defineMessages({
|
||||
editFilters: {
|
||||
id: 'gradebook.GradesView.editFilterLabel',
|
||||
defaultMessage: 'Edit Filters',
|
||||
description: 'Button text on Grades tab to open/close the Filters tab',
|
||||
description: 'A labeled button in the Grades tab that opens/closes the Filters tab, allowing the grades to be filtered',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,48 +4,58 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot
|
||||
<div
|
||||
className="gradebook-container"
|
||||
>
|
||||
<div
|
||||
className="gbook"
|
||||
<DataTable
|
||||
RowStatusComponent={[MockFunction this.nullMethod]}
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": <UsernameLabelReplacement />,
|
||||
"accessor": "Username",
|
||||
},
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Email"
|
||||
description="Gradebook table email column header"
|
||||
id="gradebook.GradesView.table.headings.email"
|
||||
/>,
|
||||
"accessor": "Email",
|
||||
},
|
||||
Object {
|
||||
"Header": "field1",
|
||||
"accessor": "field1",
|
||||
},
|
||||
Object {
|
||||
"Header": "field2",
|
||||
"accessor": "field2",
|
||||
},
|
||||
Object {
|
||||
"Header": <TotalGradeLabelReplacement />,
|
||||
"accessor": "Total Grade (%)",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"mappedRow: 1",
|
||||
"mappedRow: 2",
|
||||
"mappedRow: 3",
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
itemCount={3}
|
||||
rowHeaderColumnKey="username"
|
||||
>
|
||||
<Table
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"key": "Username",
|
||||
"label": <UsernameLabelReplacement />,
|
||||
},
|
||||
Object {
|
||||
"key": "Email",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Email*"
|
||||
description="Gradebook table email column header"
|
||||
id="gradebook.GradesView.table.headings.email"
|
||||
/>,
|
||||
},
|
||||
Object {
|
||||
"key": "field1",
|
||||
"label": "field1",
|
||||
},
|
||||
Object {
|
||||
"key": "field2",
|
||||
"label": "field2",
|
||||
},
|
||||
Object {
|
||||
"key": "Total Grade (%)",
|
||||
"label": <TotalGradeLabelReplacement />,
|
||||
},
|
||||
]
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable
|
||||
content={
|
||||
<FormattedMessage
|
||||
defaultMessage="No results found"
|
||||
description="Gradebook table message when no learner results were found"
|
||||
id="gradebook.GradesView.table.noResultsFound"
|
||||
/>
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"mappedRow: 1",
|
||||
"mappedRow: 2",
|
||||
"mappedRow: 3",
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
rowHeaderColumnKey="username"
|
||||
/>
|
||||
</div>
|
||||
</DataTable>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3,8 +3,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage, getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { Headings } from 'data/constants/grades';
|
||||
@@ -27,6 +27,7 @@ export class GradebookTable extends React.Component {
|
||||
super(props);
|
||||
this.mapHeaders = this.mapHeaders.bind(this);
|
||||
this.mapRows = this.mapRows.bind(this);
|
||||
this.nullMethod = this.nullMethod.bind(this);
|
||||
}
|
||||
|
||||
mapHeaders(heading) {
|
||||
@@ -40,7 +41,7 @@ export class GradebookTable extends React.Component {
|
||||
} else {
|
||||
label = heading;
|
||||
}
|
||||
return { label, key: heading };
|
||||
return { Header: label, accessor: heading };
|
||||
}
|
||||
|
||||
mapRows(entry) {
|
||||
@@ -49,7 +50,7 @@ export class GradebookTable extends React.Component {
|
||||
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
|
||||
),
|
||||
[Headings.email]: (<Fields.Email email={entry.email} />),
|
||||
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}%`,
|
||||
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${isRtl(getLocale()) ? '\u200f' : ''}%`,
|
||||
};
|
||||
entry.section_breakdown.forEach(subsection => {
|
||||
dataRow[subsection.label] = (
|
||||
@@ -59,17 +60,25 @@ export class GradebookTable extends React.Component {
|
||||
return dataRow;
|
||||
}
|
||||
|
||||
nullMethod() {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="gradebook-container">
|
||||
<div className="gbook">
|
||||
<Table
|
||||
columns={this.props.headings.map(this.mapHeaders)}
|
||||
data={this.props.grades.map(this.mapRows)}
|
||||
rowHeaderColumnKey="username"
|
||||
hasFixedColumnWidths
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={this.props.headings.map(this.mapHeaders)}
|
||||
data={this.props.grades.map(this.mapRows)}
|
||||
rowHeaderColumnKey="username"
|
||||
hasFixedColumnWidths
|
||||
itemCount={this.props.grades.length}
|
||||
RowStatusComponent={this.nullMethod}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable content={<FormattedMessage {...messages.noResultsFound} />} />
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
const messages = defineMessages({
|
||||
emailHeading: {
|
||||
id: 'gradebook.GradesView.table.headings.email',
|
||||
defaultMessage: 'Email*',
|
||||
defaultMessage: 'Email',
|
||||
description: 'Gradebook table email column header',
|
||||
},
|
||||
totalGradeHeading: {
|
||||
@@ -31,6 +31,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Total Grade values are always displayed as a percentage',
|
||||
description: 'Gradebook table message that total grades are displayed in percent format',
|
||||
},
|
||||
noResultsFound: {
|
||||
id: 'gradebook.GradesView.table.noResultsFound',
|
||||
defaultMessage: 'No results found',
|
||||
description: 'Gradebook table message when no learner results were found',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
@@ -11,8 +11,12 @@ import Fields from './Fields';
|
||||
import messages from './messages';
|
||||
import { GradebookTable, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Table: () => 'Table',
|
||||
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||
DataTable: {
|
||||
Table: 'DataTable.Table',
|
||||
TableControlBar: 'DataTable.TableControlBar',
|
||||
EmptyTable: 'DataTable.EmptyTable',
|
||||
},
|
||||
}));
|
||||
jest.mock('./Fields', () => ({
|
||||
__esModule: true,
|
||||
@@ -79,40 +83,45 @@ describe('GradebookTable', () => {
|
||||
};
|
||||
test('snapshot - fields1 and 2 between email and totalGrade, mocked rows', () => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
el.instance().nullMethod = jest.fn().mockName('this.nullMethod');
|
||||
el.instance().mapRows = (entry) => `mappedRow: ${entry.percent}`;
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('null method returns null for stub component', () => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
expect(el.instance().nullMethod()).toEqual(null);
|
||||
});
|
||||
describe('table columns (mapHeaders)', () => {
|
||||
let headings;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
headings = el.find(Table).props().columns;
|
||||
headings = el.find(DataTable).props().columns;
|
||||
});
|
||||
test('username sets key and replaces label with component', () => {
|
||||
test('username sets key and replaces Header with component', () => {
|
||||
const heading = headings[0];
|
||||
expect(heading.key).toEqual(Headings.username);
|
||||
expect(heading.label.type).toEqual(LabelReplacements.UsernameLabelReplacement);
|
||||
expect(heading.accessor).toEqual(Headings.username);
|
||||
expect(heading.Header.type).toEqual(LabelReplacements.UsernameLabelReplacement);
|
||||
});
|
||||
test('email sets key and label from header', () => {
|
||||
test('email sets key and Header from header', () => {
|
||||
const heading = headings[1];
|
||||
expect(heading.key).toEqual(Headings.email);
|
||||
expect(heading.label).toEqual(<FormattedMessage {...messages.emailHeading} />);
|
||||
expect(heading.accessor).toEqual(Headings.email);
|
||||
expect(heading.Header).toEqual(<FormattedMessage {...messages.emailHeading} />);
|
||||
});
|
||||
test('subsections set key and label from header', () => {
|
||||
expect(headings[2]).toEqual({ key: fields.field1, label: fields.field1 });
|
||||
expect(headings[3]).toEqual({ key: fields.field2, label: fields.field2 });
|
||||
test('subsections set key and Header from header', () => {
|
||||
expect(headings[2]).toEqual({ accessor: fields.field1, Header: fields.field1 });
|
||||
expect(headings[3]).toEqual({ accessor: fields.field2, Header: fields.field2 });
|
||||
});
|
||||
test('totalGrade sets key and replaces label with component', () => {
|
||||
test('totalGrade sets key and replaces Header with component', () => {
|
||||
const heading = headings[4];
|
||||
expect(heading.key).toEqual(Headings.totalGrade);
|
||||
expect(heading.label.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
|
||||
expect(heading.accessor).toEqual(Headings.totalGrade);
|
||||
expect(heading.Header.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
|
||||
});
|
||||
});
|
||||
describe('table data (mapRows)', () => {
|
||||
let rows;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
rows = el.find(Table).props().data;
|
||||
rows = el.find(DataTable).props().data;
|
||||
});
|
||||
describe.each([0, 1, 2])('gradeEntry($percent)', (gradeIndex) => {
|
||||
let row;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
.import-grades-btn {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.intervention-report-description: {
|
||||
.intervention-report-description {
|
||||
margin-right: 40px;
|
||||
}
|
||||
h4.step-message-1 {
|
||||
@@ -67,104 +67,9 @@
|
||||
overflow-x: auto;
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gbook {
|
||||
width: 100%;
|
||||
|
||||
.grade-button {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.student-key {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#courseGradeTooltipIcon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.table thead tr {
|
||||
min-height: 60px;
|
||||
&:nth-child(1) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: white;
|
||||
th {
|
||||
background-color: white;
|
||||
border-bottom: 1px solid $gray_200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thead, tbody, tr, td, th {
|
||||
display: block;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.table tr th:first-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.table tr th:first-child,
|
||||
.table tr td:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1; // to float over the following children in the side-scrolling case
|
||||
background: white;
|
||||
}
|
||||
|
||||
.table tr {
|
||||
th:nth-child(1),
|
||||
td:nth-child(1),
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
width: 240px;
|
||||
}
|
||||
th:nth-last-of-type(1) {
|
||||
width: 150px;
|
||||
}
|
||||
th, td {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
.table tbody th {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.table {
|
||||
overflow-x: hidden;
|
||||
|
||||
height: 100%;
|
||||
|
||||
tbody {
|
||||
overflow-y: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
thead, tbody tr {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th {
|
||||
vertical-align: top;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-style {
|
||||
color: #0075b4;
|
||||
&:hover, &:focus {
|
||||
color: #004368;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.form-group, .pgn__form-group {
|
||||
label {
|
||||
font-weight: bold;
|
||||
|
||||
@@ -4,12 +4,12 @@ const messages = defineMessages({
|
||||
csvUploadLabel: {
|
||||
id: 'gradebook.BulkManagementHistoryView.csvUploadLabel',
|
||||
defaultMessage: 'Upload Grade CSV',
|
||||
description: 'Button in BulkManagementHistoryView Alerts',
|
||||
description: 'A labeled button to upload a CSV containing course grades.',
|
||||
},
|
||||
importGradesBtnText: {
|
||||
id: 'gradebook.GradesView.importGradesBtnText',
|
||||
defaultMessage: 'Import Grades',
|
||||
description: 'Button in BulkManagement Tab File Upload Form',
|
||||
description: 'A labeled button to import grades in the BulkManagement Tab File Upload Form',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ const messages = defineMessages({
|
||||
description: {
|
||||
id: 'gradebook.GradesView.ImportSuccessToast.description',
|
||||
defaultMessage: 'Import Successful! Grades will be updated momentarily.',
|
||||
description: 'Import Success Toast description',
|
||||
description: 'A message congratulating a successful Import of grades',
|
||||
},
|
||||
showHistoryViewBtn: {
|
||||
id: 'gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn',
|
||||
defaultMessage: 'View Activity Log',
|
||||
description: 'Button text for action that loads Bulk Management Activity Log view',
|
||||
description: 'The text on a button that loads a view of the Bulk Management Activity Log',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ const messages = defineMessages({
|
||||
title: {
|
||||
id: 'gradebook.GradesView.InterventionsReport.title',
|
||||
defaultMessage: 'Interventions Report',
|
||||
description: 'Intervention report subsection label',
|
||||
description: 'The title for the Intervention report subsection',
|
||||
},
|
||||
description: {
|
||||
id: 'gradebook.GradesView.InterventionsReport.description',
|
||||
defaultMessage: 'Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.',
|
||||
description: 'Intervention report subsection description',
|
||||
description: 'The description for the Intervention report subsection',
|
||||
},
|
||||
downloadBtn: {
|
||||
id: 'gradebook.GradesView.InterventionsReport.downloadBtn',
|
||||
defaultMessage: 'Download Interventions',
|
||||
description: 'Button text for intervention report download control in GradesView',
|
||||
description: 'The labeled button to download the Intervention report from the Grades View',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ const messages = defineMessages({
|
||||
scoreView: {
|
||||
id: 'gradebook.GradesView.scoreViewLabel',
|
||||
defaultMessage: 'Score View',
|
||||
description: 'Score format select dropdown label',
|
||||
description: 'The label for the dropdown list that allows a user to select the Score format',
|
||||
},
|
||||
absolute: {
|
||||
id: 'gradebook.GradesView.absoluteOption',
|
||||
defaultMessage: 'Absolute',
|
||||
description: 'Score format select dropdown option',
|
||||
description: 'A label within the Score Format dropdown list for the Absolute Grade Score option',
|
||||
},
|
||||
percent: {
|
||||
id: 'gradebook.GradesView.percentOption',
|
||||
defaultMessage: 'Percent',
|
||||
description: 'Score format select dropdown option',
|
||||
description: 'A label within the Score Format dropdown list for the Percent Grade Score option',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -18,13 +18,14 @@ import messages from './SearchControls.messages';
|
||||
export class SearchControls extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
this.onClear = this.onClear.bind(this);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/** Changing the search value stores the key in Gradebook. Currently unused */
|
||||
onChange(searchValue) {
|
||||
this.props.setSearchValue(searchValue);
|
||||
onBlur(e) {
|
||||
this.props.setSearchValue(e.target.value);
|
||||
}
|
||||
|
||||
onClear() {
|
||||
@@ -32,13 +33,18 @@ export class SearchControls extends React.Component {
|
||||
this.props.fetchGrades();
|
||||
}
|
||||
|
||||
onSubmit(searchValue) {
|
||||
this.props.setSearchValue(searchValue);
|
||||
this.props.fetchGrades();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<SearchField
|
||||
onSubmit={this.props.fetchGrades}
|
||||
onSubmit={this.onSubmit}
|
||||
inputLabel={<FormattedMessage {...messages.label} />}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
onClear={this.onClear}
|
||||
value={this.props.searchValue}
|
||||
/>
|
||||
|
||||
@@ -4,12 +4,12 @@ const messages = defineMessages({
|
||||
label: {
|
||||
id: 'gradebook.GradesView.search.label',
|
||||
defaultMessage: 'Search for a learner',
|
||||
description: 'Search description label',
|
||||
description: 'Text prompting a user to use this functionality to search for a learner',
|
||||
},
|
||||
hint: {
|
||||
id: 'gradebook.GradesView.search.hint',
|
||||
defaultMessage: 'Search by username, email, or student key',
|
||||
description: 'Search hint label',
|
||||
description: 'A hint explaining the ways a user can search',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ import { shallow } from 'enzyme';
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import { mapDispatchToProps, mapStateToProps, SearchControls } from './SearchControls';
|
||||
import {
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
SearchControls,
|
||||
} from './SearchControls';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Icon: 'Icon',
|
||||
@@ -15,7 +19,7 @@ jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
searchValue: jest.fn(state => ({ searchValue: state })),
|
||||
searchValue: jest.fn((state) => ({ searchValue: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -52,26 +56,45 @@ describe('SearchControls', () => {
|
||||
describe('Snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const wrapper = searchControls();
|
||||
wrapper.instance().onChange = jest.fn().mockName('onChange');
|
||||
wrapper.instance().onBlur = jest.fn().mockName('onBlur');
|
||||
wrapper.instance().onClear = jest.fn().mockName('onClear');
|
||||
wrapper.instance().onSubmit = jest.fn().mockName('onSubmit');
|
||||
expect(wrapper.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onChange', () => {
|
||||
it('saves the changed search value to Gradebook state', () => {
|
||||
const wrapper = searchControls();
|
||||
wrapper.instance().onChange('bob');
|
||||
expect(props.setSearchValue).toHaveBeenCalledWith('bob');
|
||||
describe('Behavior', () => {
|
||||
describe('onBlur', () => {
|
||||
it('saves the search value to Gradebook state but do not fetch grade', () => {
|
||||
const wrapper = searchControls();
|
||||
const event = {
|
||||
target: {
|
||||
value: 'bob',
|
||||
},
|
||||
};
|
||||
wrapper.instance().onBlur(event);
|
||||
expect(props.setSearchValue).toHaveBeenCalledWith('bob');
|
||||
expect(props.fetchGrades).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onChange', () => {
|
||||
it('sets search value to empty string and calls fetchGrades', () => {
|
||||
const wrapper = searchControls();
|
||||
wrapper.instance().onClear();
|
||||
expect(props.setSearchValue).toHaveBeenCalledWith('');
|
||||
expect(props.fetchGrades).toHaveBeenCalled();
|
||||
describe('onClear', () => {
|
||||
it('sets search value to empty string and calls fetchGrades', () => {
|
||||
const wrapper = searchControls();
|
||||
wrapper.instance().onClear();
|
||||
expect(props.setSearchValue).toHaveBeenCalledWith('');
|
||||
expect(props.fetchGrades).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSubmit', () => {
|
||||
it('sets search value to input and calls fetchGrades', () => {
|
||||
const wrapper = searchControls();
|
||||
|
||||
wrapper.instance().onSubmit('John');
|
||||
expect(props.setSearchValue).toHaveBeenCalledWith('John');
|
||||
expect(props.fetchGrades).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { StatusAlert } from '@edx/paragon';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
@@ -40,18 +40,20 @@ export class StatusAlerts extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<StatusAlert
|
||||
alertType="success"
|
||||
dialog={<FormattedMessage {...messages.editSuccessAlert} />}
|
||||
<Alert
|
||||
variant="success"
|
||||
onClose={this.props.handleCloseSuccessBanner}
|
||||
open={this.props.showSuccessBanner}
|
||||
/>
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog={this.courseGradeFilterAlertDialogText}
|
||||
show={this.props.showSuccessBanner}
|
||||
>
|
||||
<FormattedMessage {...messages.editSuccessAlert} />
|
||||
</Alert>
|
||||
<Alert
|
||||
variant="danger"
|
||||
dismissible={false}
|
||||
open={this.isCourseGradeFilterAlertOpen}
|
||||
/>
|
||||
show={this.isCourseGradeFilterAlertOpen}
|
||||
>
|
||||
{this.courseGradeFilterAlertDialogText}
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,17 +4,17 @@ const messages = defineMessages({
|
||||
editSuccessAlert: {
|
||||
id: 'gradebook.GradesView.editSuccessAlert',
|
||||
defaultMessage: 'The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.',
|
||||
description: 'Alert text for successful edit action',
|
||||
description: 'An alert text for successfully editing a grade',
|
||||
},
|
||||
maxGradeInvalid: {
|
||||
id: 'gradebook.GradesView.maxCourseGradeInvalid',
|
||||
defaultMessage: 'Maximum course grade must be between 0 and 100',
|
||||
description: 'Alert text for invalid maximum course grade',
|
||||
description: 'An alert text for selecting a maximum course grade greater than 100',
|
||||
},
|
||||
minGradeInvalid: {
|
||||
id: 'gradebook.GradesView.minCourseGradeInvalid',
|
||||
defaultMessage: 'Minimum course grade must be between 0 and 100',
|
||||
description: 'Alert text for invalid minimum course grade',
|
||||
description: 'An alert text for selecting a minimum course grade less than 0',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from './StatusAlerts';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
StatusAlert: 'StatusAlert',
|
||||
Alert: 'Alert',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
|
||||
@@ -12,7 +12,7 @@ exports[`FilterMenuToggle component snapshots basic snapshot 1`] = `
|
||||
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit Filters"
|
||||
description="Button text on Grades tab to open/close the Filters tab"
|
||||
description="A labeled button in the Grades tab that opens/closes the Filters tab, allowing the grades to be filtered"
|
||||
id="gradebook.GradesView.editFilterLabel"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -19,7 +19,7 @@ exports[`ImportGradesButton component snapshot snapshot - loads export form w/ a
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Grade CSV"
|
||||
description="Button in BulkManagementHistoryView Alerts"
|
||||
description="A labeled button to upload a CSV containing course grades."
|
||||
id="gradebook.BulkManagementHistoryView.csvUploadLabel"
|
||||
/>
|
||||
}
|
||||
@@ -35,7 +35,7 @@ exports[`ImportGradesButton component snapshot snapshot - loads export form w/ a
|
||||
label={
|
||||
Object {
|
||||
"defaultMessage": "Import Grades",
|
||||
"description": "Button in BulkManagement Tab File Upload Form",
|
||||
"description": "A labeled button to import grades in the BulkManagement Tab File Upload Form",
|
||||
"id": "gradebook.GradesView.importGradesBtnText",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ exports[`InterventionsReport component snapshots snapshot 1`] = `
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Interventions Report"
|
||||
description="Intervention report subsection label"
|
||||
description="The title for the Intervention report subsection"
|
||||
id="gradebook.GradesView.InterventionsReport.title"
|
||||
/>
|
||||
</h4>
|
||||
@@ -19,7 +19,7 @@ exports[`InterventionsReport component snapshots snapshot 1`] = `
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits."
|
||||
description="Intervention report subsection description"
|
||||
description="The description for the Intervention report subsection"
|
||||
id="gradebook.GradesView.InterventionsReport.description"
|
||||
/>
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@ exports[`InterventionsReport component snapshots snapshot 1`] = `
|
||||
label={
|
||||
Object {
|
||||
"defaultMessage": "Download Interventions",
|
||||
"description": "Button text for intervention report download control in GradesView",
|
||||
"description": "The labeled button to download the Intervention report from the Grades View",
|
||||
"id": "gradebook.GradesView.InterventionsReport.downloadBtn",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ exports[`ScoreViewInput component snapshot - select box with percent and absolut
|
||||
<FormLabel>
|
||||
<FormattedMessage
|
||||
defaultMessage="Score View"
|
||||
description="Score format select dropdown label"
|
||||
description="The label for the dropdown list that allows a user to select the Score format"
|
||||
id="gradebook.GradesView.scoreViewLabel"
|
||||
/>
|
||||
:
|
||||
|
||||
@@ -6,13 +6,13 @@ exports[`SearchControls Component Snapshots basic snapshot 1`] = `
|
||||
inputLabel={
|
||||
<FormattedMessage
|
||||
defaultMessage="Search for a learner"
|
||||
description="Search description label"
|
||||
description="Text prompting a user to use this functionality to search for a learner"
|
||||
id="gradebook.GradesView.search.label"
|
||||
/>
|
||||
}
|
||||
onChange={[MockFunction onChange]}
|
||||
onBlur={[MockFunction onBlur]}
|
||||
onClear={[MockFunction onClear]}
|
||||
onSubmit={[MockFunction fetchGrades]}
|
||||
onSubmit={[MockFunction onSubmit]}
|
||||
value="alice"
|
||||
/>
|
||||
<small
|
||||
@@ -20,7 +20,7 @@ exports[`SearchControls Component Snapshots basic snapshot 1`] = `
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Search by username, email, or student key"
|
||||
description="Search hint label"
|
||||
description="A hint explaining the ways a user can search"
|
||||
id="gradebook.GradesView.search.hint"
|
||||
/>
|
||||
</small>
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
|
||||
exports[`StatusAlerts snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<StatusAlert
|
||||
alertType="success"
|
||||
dialog={
|
||||
<FormattedMessage
|
||||
defaultMessage="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
|
||||
description="Alert text for successful edit action"
|
||||
id="gradebook.GradesView.editSuccessAlert"
|
||||
/>
|
||||
}
|
||||
<Alert
|
||||
onClose={[MockFunction handleCloseSuccessBanner]}
|
||||
open={true}
|
||||
/>
|
||||
<StatusAlert
|
||||
alertType="danger"
|
||||
dialog="the quiCk brown does somEthing or other"
|
||||
show={true}
|
||||
variant="success"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
|
||||
description="An alert text for successfully editing a grade"
|
||||
id="gradebook.GradesView.editSuccessAlert"
|
||||
/>
|
||||
</Alert>
|
||||
<Alert
|
||||
dismissible={false}
|
||||
open={false}
|
||||
/>
|
||||
show={false}
|
||||
variant="danger"
|
||||
>
|
||||
the quiCk brown does somEthing or other
|
||||
</Alert>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
@@ -32,26 +32,26 @@ export const localFilterKeys = StrictDict({
|
||||
*/
|
||||
export const bulkManagementColumns = [
|
||||
{
|
||||
key: 'filename',
|
||||
label: 'Gradebook',
|
||||
accessor: 'filename',
|
||||
Header: 'Gradebook',
|
||||
columnSortable: false,
|
||||
width: 'col-5',
|
||||
},
|
||||
{
|
||||
key: 'resultsSummary',
|
||||
label: 'Download Summary',
|
||||
accessor: 'resultsSummary',
|
||||
Header: 'Download Summary',
|
||||
columnSortable: false,
|
||||
width: 'col',
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: 'Who',
|
||||
accessor: 'user',
|
||||
Header: 'Who',
|
||||
columnSortable: false,
|
||||
width: 'col-1',
|
||||
},
|
||||
{
|
||||
key: 'timeUploaded',
|
||||
label: 'When',
|
||||
accessor: 'timeUploaded',
|
||||
Header: 'When',
|
||||
columnSortable: false,
|
||||
width: 'col',
|
||||
},
|
||||
|
||||
@@ -4,37 +4,37 @@ const messages = defineMessages({
|
||||
assignment: {
|
||||
id: 'gradebook.GradesTab.FilterBadges.assignment',
|
||||
defaultMessage: 'Assignment',
|
||||
description: 'Assignment FilterBadge label',
|
||||
description: 'A label describing the notification under the "Edit Filters" button that shows by which of the course\'s assignments the view is being filtered.',
|
||||
},
|
||||
assignmentGrade: {
|
||||
id: 'gradebook.GradesTab.FilterBadges.assignmentGrade',
|
||||
defaultMessage: 'Assignment Grade',
|
||||
description: 'Assignment Grade FilterBadge label',
|
||||
description: 'A label describing the notification under the "Edit Filters" button that shows that the view is being filtered to include assignment grades within the alloted range.',
|
||||
},
|
||||
assignmentType: {
|
||||
id: 'gradebook.GradesTab.FilterBadges.assignmentType',
|
||||
defaultMessage: 'Assignment Type',
|
||||
description: 'Assignment Type FilterBadge label',
|
||||
description: 'A label describing the notification under the "Edit Filters" button that shows by which of the course\'s assignment types the view is being filtered.',
|
||||
},
|
||||
cohort: {
|
||||
id: 'gradebook.GradesTab.FilterBadges.cohort',
|
||||
defaultMessage: 'Cohort',
|
||||
description: 'Cohort FilterBadge label',
|
||||
description: 'A label describing the notification under the "Edit Filters" button that shows by which of the course\'s cohorts the view is being filtered.',
|
||||
},
|
||||
courseGrade: {
|
||||
id: 'gradebook.GradesTab.FilterBadges.courseGrade',
|
||||
defaultMessage: 'Course Grade',
|
||||
description: 'Course Grade FilterBadge label',
|
||||
description: 'A label describing the notification under the "Edit Filters" button that shows that the view is being filtered to include course grades within the alloted range.',
|
||||
},
|
||||
includeCourseRoleMembers: {
|
||||
id: 'gradebook.GradesTab.FilterBadges.includeCourseRoleMembers',
|
||||
defaultMessage: 'Include Course Team Members',
|
||||
description: 'Include Course Team Members FilterBadge label',
|
||||
description: 'A label describing the notification under the "Edit Filters" button that shows that the view is being filtered to include course team members.',
|
||||
},
|
||||
track: {
|
||||
id: 'gradebook.GradesTab.FilterBadges.track',
|
||||
defaultMessage: 'Track',
|
||||
description: 'Track FilterBadge label',
|
||||
description: 'A label describing the notification under the "Edit Filters" button that shows by which of the course\'s tracks the view is being filtered.',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('app reducer', () => {
|
||||
const mockDate = new Date(8675309);
|
||||
let dateSpy;
|
||||
beforeEach(() => {
|
||||
dateSpy = jest.spyOn(global, 'Date').mockReturnValue(mockDate);
|
||||
dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
||||
});
|
||||
afterEach(() => {
|
||||
dateSpy.mockRestore();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { StrictDict } from 'utils';
|
||||
|
||||
import { Headings, GradeFormats } from 'data/constants/grades';
|
||||
import { formatDateForDisplay } from 'data/actions/utils';
|
||||
import { getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||
import simpleSelectorFactory from '../utils';
|
||||
import * as module from './grades';
|
||||
|
||||
@@ -156,7 +157,7 @@ export const subsectionGrade = StrictDict({
|
||||
[GradeFormats.absolute]: (subsection) => {
|
||||
const earned = module.roundGrade(subsection.score_earned);
|
||||
const possible = module.roundGrade(subsection.score_possible);
|
||||
return subsection.attempted ? `${earned}/${possible}` : `${earned}`;
|
||||
return subsection.attempted ? `${earned}${isRtl(getLocale()) ? '\\' : '/'}${possible}` : `${earned}`;
|
||||
},
|
||||
/**
|
||||
* subsectionGrade.percent(subsection)
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
|
||||
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
|
||||
"gradebook.GradesView.editFilterLabel": "Edit Filters",
|
||||
"gradebook.GradesView.table.headings.email": "Email*",
|
||||
"gradebook.GradesView.table.headings.email": "Email",
|
||||
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
|
||||
"gradebook.GradesView.table.headings.username": "Username",
|
||||
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
|
||||
@@ -70,4 +70,4 @@
|
||||
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
|
||||
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
|
||||
"gradebook.GradesTab.FilterBadges.track": "Track"
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
|
||||
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
|
||||
"gradebook.GradesView.editFilterLabel": "Edit Filters",
|
||||
"gradebook.GradesView.table.headings.email": "Email*",
|
||||
"gradebook.GradesView.table.headings.email": "Email",
|
||||
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
|
||||
"gradebook.GradesView.table.headings.username": "Username",
|
||||
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
|
||||
@@ -70,4 +70,4 @@
|
||||
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
|
||||
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
|
||||
"gradebook.GradesTab.FilterBadges.track": "Track"
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Motif",
|
||||
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
|
||||
"gradebook.GradesView.editFilterLabel": "Editer les filtres",
|
||||
"gradebook.GradesView.table.headings.email": "Email*",
|
||||
"gradebook.GradesView.table.headings.email": "Email",
|
||||
"gradebook.GradesView.table.headings.totalGrade": "Note totale (%)",
|
||||
"gradebook.GradesView.table.headings.username": "Nom d’utilisateur",
|
||||
"gradebook.GradesView.table.labels.studentKey": "Clé d'étudiant",
|
||||
@@ -70,4 +70,4 @@
|
||||
"gradebook.GradesTab.FilterBadges.courseGrade": "Note du cours",
|
||||
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
|
||||
"gradebook.GradesTab.FilterBadges.track": "Track"
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
|
||||
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
|
||||
"gradebook.GradesView.editFilterLabel": "Edit Filters",
|
||||
"gradebook.GradesView.table.headings.email": "Email*",
|
||||
"gradebook.GradesView.table.headings.email": "Email",
|
||||
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
|
||||
"gradebook.GradesView.table.headings.username": "Username",
|
||||
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
|
||||
@@ -70,4 +70,4 @@
|
||||
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
|
||||
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
|
||||
"gradebook.GradesTab.FilterBadges.track": "Track"
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
"gradebook.GradesView.EditModal.Overrides.reasonHeader": "Reason",
|
||||
"gradebook.GradesTab.usersVisibilityLabel'": "Showing {filteredUsers} of {totalUsers} total learners",
|
||||
"gradebook.GradesView.editFilterLabel": "Edit Filters",
|
||||
"gradebook.GradesView.table.headings.email": "Email*",
|
||||
"gradebook.GradesView.table.headings.email": "Email",
|
||||
"gradebook.GradesView.table.headings.totalGrade": "Total Grade (%)",
|
||||
"gradebook.GradesView.table.headings.username": "Username",
|
||||
"gradebook.GradesView.table.labels.studentKey": "Student Key*",
|
||||
@@ -70,4 +70,4 @@
|
||||
"gradebook.GradesTab.FilterBadges.courseGrade": "Course Grade",
|
||||
"gradebook.GradesTab.FilterBadges.includeCourseRoleMembers": "Include Course Team Members",
|
||||
"gradebook.GradesTab.FilterBadges.track": "Track"
|
||||
}
|
||||
}
|
||||
187
src/testUtils.js
Normal file
187
src/testUtils.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import react from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
/**
|
||||
* Mocked formatMessage provided by react-intl
|
||||
*/
|
||||
export const formatMessage = (msg, values) => {
|
||||
let message = msg.defaultMessage;
|
||||
if (values === undefined) {
|
||||
return message;
|
||||
}
|
||||
Object.keys(values).forEach((key) => {
|
||||
// eslint-disable-next-line
|
||||
message = message.replace(`{${key}}`, values[key]);
|
||||
});
|
||||
return message;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock a single component, or a nested component so that its children render nicely
|
||||
* in snapshots.
|
||||
* @param {string} name - parent component name
|
||||
* @param {obj} contents - object of child components with intended component
|
||||
* render name.
|
||||
* @return {func} - mock component with nested children.
|
||||
*
|
||||
* usage:
|
||||
* mockNestedComponent('Card', { Body: 'Card.Body', Form: { Control: { Feedback: 'Form.Control.Feedback' }}... });
|
||||
* mockNestedComponent('IconButton', 'IconButton');
|
||||
*/
|
||||
export const mockNestedComponent = (name, contents) => {
|
||||
if (typeof contents !== 'object') {
|
||||
return contents;
|
||||
}
|
||||
const fn = () => name;
|
||||
Object.defineProperty(fn, 'name', { value: name });
|
||||
Object.keys(contents).forEach((nestedName) => {
|
||||
const value = contents[nestedName];
|
||||
fn[nestedName] = typeof value !== 'object'
|
||||
? value
|
||||
: mockNestedComponent(`${name}.${nestedName}`, value);
|
||||
});
|
||||
return fn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock a module of components. nested components will be rendered nicely in snapshots.
|
||||
* @param {obj} mapping - component module mock config.
|
||||
* @return {obj} - module of flat and nested components that will render nicely in snapshots.
|
||||
* usage:
|
||||
* mockNestedComponents({
|
||||
* Card: { Body: 'Card.Body' },
|
||||
* IconButton: 'IconButton',
|
||||
* })
|
||||
*/
|
||||
export const mockNestedComponents = (mapping) => Object.entries(mapping).reduce(
|
||||
(obj, [name, value]) => ({
|
||||
...obj,
|
||||
[name]: mockNestedComponent(name, value),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
/**
|
||||
* Mock utility for working with useState in a hooks module.
|
||||
* Expects/requires an object containing the state object in order to ensure
|
||||
* the mock behavior works appropriately.
|
||||
*
|
||||
* Expected format:
|
||||
* hooks = { state: { <key>: (val) => React.createRef(val), ... } }
|
||||
*
|
||||
* Returns a utility for mocking useState and providing access to specific state values
|
||||
* and setState methods, as well as allowing per-test configuration of useState value returns.
|
||||
*
|
||||
* Example usage:
|
||||
* // hooks.js
|
||||
* import * as module from './hooks';
|
||||
* const state = {
|
||||
* isOpen: (val) => React.useState(val),
|
||||
* hasDoors: (val) => React.useState(val),
|
||||
* selected: (val) => React.useState(val),
|
||||
* };
|
||||
* ...
|
||||
* export const exampleHook = () => {
|
||||
* const [isOpen, setIsOpen] = module.state.isOpen(false);
|
||||
* if (!isOpen) { return null; }
|
||||
* return { isOpen, setIsOpen };
|
||||
* }
|
||||
* ...
|
||||
*
|
||||
* // hooks.test.js
|
||||
* import * as hooks from './hooks';
|
||||
* const state = new MockUseState(hooks)
|
||||
* ...
|
||||
* describe('state hooks', () => {
|
||||
* state.testGetter(state.keys.isOpen);
|
||||
* state.testGetter(state.keys.hasDoors);
|
||||
* state.testGetter(state.keys.selected);
|
||||
* });
|
||||
* describe('exampleHook', () => {
|
||||
* beforeEach(() => { state.mock(); });
|
||||
* it('returns null if isOpen is default value', () => {
|
||||
* expect(hooks.exampleHook()).toEqual(null);
|
||||
* });
|
||||
* it('returns isOpen and setIsOpen if isOpen is not null', () => {
|
||||
* state.mockVal(state.keys.isOpen, true);
|
||||
* expect(hooks.exampleHook()).toEqual({
|
||||
* isOpen: true,
|
||||
* setIsOpen: state.setState[state.keys.isOpen],
|
||||
* });
|
||||
* });
|
||||
* afterEach(() => { state.restore(); });
|
||||
* });
|
||||
*
|
||||
* @param {obj} hooks - hooks module containing a 'state' object
|
||||
*/
|
||||
export class MockUseState {
|
||||
constructor(hooks) {
|
||||
this.hooks = hooks;
|
||||
this.oldState = null;
|
||||
this.setState = {};
|
||||
this.stateVals = {};
|
||||
|
||||
this.mock = this.mock.bind(this);
|
||||
this.restore = this.restore.bind(this);
|
||||
this.mockVal = this.mockVal.bind(this);
|
||||
this.testGetter = this.testGetter.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {object} - StrictDict of state object keys
|
||||
*/
|
||||
get keys() {
|
||||
return StrictDict(Object.keys(this.hooks.state).reduce(
|
||||
(obj, key) => ({ ...obj, [key]: key }),
|
||||
{},
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the hook module's state object with a mocked version, initialized to default values.
|
||||
*/
|
||||
mock() {
|
||||
this.oldState = this.hooks.state;
|
||||
Object.keys(this.keys).forEach(key => {
|
||||
this.hooks.state[key] = jest.fn(val => {
|
||||
this.stateVals[key] = val;
|
||||
return [val, this.setState[key]];
|
||||
});
|
||||
});
|
||||
this.setState = Object.keys(this.keys).reduce(
|
||||
(obj, key) => ({
|
||||
...obj,
|
||||
[key]: jest.fn(val => {
|
||||
this.hooks.state[key] = val;
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the hook module's state object to the actual code.
|
||||
*/
|
||||
restore() {
|
||||
this.hooks.state = this.oldState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock the state getter associated with a single key to return a specific value one time.
|
||||
* @param {string} key - state key (from this.keys)
|
||||
* @param {any} val - new value to be returned by the useState call.
|
||||
*/
|
||||
mockVal(key, val) {
|
||||
this.hooks.state[key].mockReturnValueOnce([val, this.setState[key]]);
|
||||
}
|
||||
|
||||
testGetter(key) {
|
||||
test(`${key} state getter should return useState passthrough`, () => {
|
||||
const testValue = 'some value';
|
||||
const useState = (val) => ({ useState: val });
|
||||
jest.spyOn(react, 'useState').mockImplementationOnce(useState);
|
||||
expect(this.hooks.state[key](testValue)).toEqual(useState(testValue));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ const strictGet = (target, name) => {
|
||||
return target;
|
||||
}
|
||||
|
||||
if (name === '$$typeof') {
|
||||
return typeof target;
|
||||
}
|
||||
|
||||
if (name in target || name === '_reactFragment') {
|
||||
return target[name];
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ describe('StrictDict', () => {
|
||||
it('allows entry listing', () => {
|
||||
expect(Object.entries(dict)).toEqual(Object.entries(rawDict));
|
||||
});
|
||||
it('allows $$typeof access', () => {
|
||||
expect(dict.$$typeof).toEqual(typeof rawDict);
|
||||
});
|
||||
describe('missing key', () => {
|
||||
it('logs error with target, name, and error stack', () => {
|
||||
// eslint-ignore-next-line no-unused-vars
|
||||
|
||||
Reference in New Issue
Block a user