Compare commits
2 Commits
dependabot
...
bw/compone
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
096ec432fe | ||
|
|
18648eb8c6 |
4
.env
4
.env
@@ -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,6 +32,3 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
DISPLAY_FEEDBACK_WIDGET='true'
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -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,6 +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'
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -3,4 +3,3 @@ dist/
|
||||
node_modules/
|
||||
src/postcss.config.js
|
||||
src/segment.js
|
||||
src/lightning.js
|
||||
|
||||
@@ -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: {
|
||||
|
||||
6
.github/CODEOWNERS
vendored
Normal file
6
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Code owners for frontend-app-gradebook, editable gradebook micro-frontend (MFE)
|
||||
|
||||
# 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.
|
||||
* @openedx/content-aurora
|
||||
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -1,7 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Adding new check for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
33
.github/renovate.json
vendored
33
.github/renovate.json
vendored
@@ -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"
|
||||
}
|
||||
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -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@v6
|
||||
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@v6
|
||||
uses: dawidd6/action-send-mail@v3
|
||||
with:
|
||||
server_address: email-smtp.us-east-1.amazonaws.com
|
||||
server_port: 465
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
uses: openedx/.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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,5 +23,3 @@ temp/babel-plugin-react-intl
|
||||
### transifex ###
|
||||
src/i18n/transifex_input.json
|
||||
temp
|
||||
|
||||
src/i18n/messages/
|
||||
27
.releaserc
Normal file
27
.releaserc
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"branch": "master",
|
||||
"tagFormat": "v${version}",
|
||||
"verifyConditions": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"analyzeCommits": "@semantic-release/commit-analyzer",
|
||||
"generateNotes": "@semantic-release/release-notes-generator",
|
||||
"prepare": "@semantic-release/npm",
|
||||
"publish": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"success": [],
|
||||
"fail": []
|
||||
}
|
||||
9
.tx/config
Normal file
9
.tx/config
Normal 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
|
||||
|
||||
38
Makefile
38
Makefile
@@ -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
136
README.md
@@ -1,5 +1,3 @@
|
||||
# frontend-app-gradebook
|
||||
|
||||
[](https://travis-ci.com/edx/frontend-app-gradebook)
|
||||
[](https://app.codecov.io/gh/openedx/frontend-app-gradebook)
|
||||
[](@edx/frontend-app-gradebook)
|
||||
@@ -7,7 +5,7 @@
|
||||
[](@edx/frontend-app-gradebook)
|
||||
[](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.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('babel');
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
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',
|
||||
'testUtils', // don't unit test jest mocking tools
|
||||
'testUtilsExtra', // don't unit test jest mocking tools
|
||||
],
|
||||
});
|
||||
|
||||
9
openedx.yaml
Normal file
9
openedx.yaml
Normal 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}
|
||||
57838
package-lock.json
generated
57838
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
53
package.json
53
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.6.3",
|
||||
"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,32 +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.6.1",
|
||||
"@edx/frontend-platform": "^8.3.7",
|
||||
"@edx/openedx-atlas": "^0.6.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": "^23.4.5",
|
||||
"@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",
|
||||
@@ -67,16 +68,20 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@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",
|
||||
"redux-mock-store": "^1.5.3"
|
||||
"react-test-renderer": "^16.10.1",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"semantic-release": "^19.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
32
src/App.jsx
32
src/App.jsx
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// frontend-app-*/src/index.scss
|
||||
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints.css" as paragonCustomMediaBreakpoints;
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
|
||||
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
|
||||
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
|
||||
117
src/App.test.jsx
117
src/App.test.jsx
@@ -1,63 +1,80 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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', () => ({
|
||||
Routes: ({ children }) => children,
|
||||
Route: ({ element }) => element,
|
||||
BrowserRouter: () => 'BrowserRouter',
|
||||
Route: () => 'Route',
|
||||
Switch: () => 'Switch',
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppProvider: ({ children }) => children,
|
||||
AppProvider: () => 'AppProvider',
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-component-header', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>Header</div>,
|
||||
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('@edx/frontend-component-footer', () => ({
|
||||
FooterSlot: () => <div>Footer</div>,
|
||||
}));
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
let router;
|
||||
|
||||
jest.mock('./head/Head', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>Head</div>,
|
||||
}));
|
||||
|
||||
jest.mock('containers/GradebookPage', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>Gradebook</div>,
|
||||
}));
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(() => {
|
||||
render(<App />);
|
||||
describe('App router component', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<App />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders Head component', () => {
|
||||
const head = screen.getByText('Head');
|
||||
expect(head).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Header component', () => {
|
||||
const header = screen.getByText('Header');
|
||||
expect(header).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Footer component', () => {
|
||||
const footer = screen.getByText('Footer');
|
||||
expect(footer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders main content wrapper', () => {
|
||||
const main = screen.getByRole('main');
|
||||
expect(main).toBeInTheDocument();
|
||||
const gradebook = screen.getByText('Gradebook');
|
||||
expect(gradebook).toBeInTheDocument();
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.childAt(1);
|
||||
});
|
||||
describe('AppProvider', () => {
|
||||
test('AppProvider is the parent component, passed the redux store props', () => {
|
||||
expect(el.type()).toBe(AppProvider);
|
||||
expect(el.props().store).toEqual(store);
|
||||
});
|
||||
});
|
||||
describe('Head', () => {
|
||||
test('first child of AppProvider', () => {
|
||||
expect(el.childAt(0).type()).toBe(Head);
|
||||
});
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('second child of AppProvider', () => {
|
||||
expect(router.type()).toBe(Router);
|
||||
});
|
||||
test('Header is above/outside-of the routing', () => {
|
||||
expect(router.childAt(0).childAt(0).type()).toBe(Header);
|
||||
expect(router.childAt(0).childAt(1).type()).toBe('main');
|
||||
});
|
||||
test('Routing - GradebookPage is only route', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
24
src/__snapshots__/App.test.jsx.snap
Normal file
24
src/__snapshots__/App.test.jsx.snap
Normal file
@@ -0,0 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot 1`] = `
|
||||
<AppProvider
|
||||
store="testStore"
|
||||
>
|
||||
<Head />
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
component="GradebookPage"
|
||||
exact={true}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</AppProvider>
|
||||
`;
|
||||
@@ -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';
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
import { BulkManagementAlerts, mapStateToProps } from './BulkManagementAlerts';
|
||||
import { renderWithIntl, screen } from '../../testUtilsExtra';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
}));
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Alert: () => 'Alert',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -19,19 +29,47 @@ const errorMessage = 'Oh noooooo';
|
||||
|
||||
describe('BulkManagementAlerts', () => {
|
||||
describe('component', () => {
|
||||
describe('states of the warnings', () => {
|
||||
test('no alert shown', () => {
|
||||
renderWithIntl(<BulkManagementAlerts bulkImportError="" uploadSuccess={false} />);
|
||||
expect(document.querySelectorAll('.alert').length).toEqual(0);
|
||||
let el;
|
||||
describe('no errer, no upload success', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<BulkManagementAlerts />);
|
||||
});
|
||||
test('Just success alert shown', () => {
|
||||
renderWithIntl(<BulkManagementAlerts bulkImportError="" uploadSuccess />);
|
||||
expect(document.querySelectorAll('.alert-success').length).toEqual(1);
|
||||
test('snapshot - bulkImportError closed, success closed', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('Just error alert shown', () => {
|
||||
renderWithIntl(<BulkManagementAlerts bulkImportError={errorMessage} uploadSuccess={false} />);
|
||||
expect(document.querySelectorAll('.alert-danger').length).toEqual(1);
|
||||
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||
test('closed danger alert', () => {
|
||||
expect(el.childAt(0).is(Alert)).toEqual(true);
|
||||
expect(el.childAt(0).props().show).toEqual(false);
|
||||
expect(el.childAt(0).props().variant).toEqual('danger');
|
||||
});
|
||||
test('closed success alert', () => {
|
||||
expect(el.childAt(1).is(Alert)).toEqual(true);
|
||||
expect(el.childAt(1).props().show).toEqual(false);
|
||||
expect(el.childAt(1).props().variant).toEqual('success');
|
||||
});
|
||||
});
|
||||
describe('no errer, no upload success', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<BulkManagementAlerts uploadSuccess bulkImportError={errorMessage} />);
|
||||
});
|
||||
const assertions = [
|
||||
'danger alert open with bulkImportError',
|
||||
'success alert open with messages.successDialog',
|
||||
];
|
||||
test(`snapshot - ${assertions.join(', ')}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('open danger alert with bulkImportError content', () => {
|
||||
expect(el.childAt(0).is(Alert)).toEqual(true);
|
||||
expect(el.childAt(0).children().text()).toEqual(errorMessage);
|
||||
expect(el.childAt(0).props().show).toEqual(true);
|
||||
});
|
||||
test('open success alert with messages.successDialog content', () => {
|
||||
expect(el.childAt(1).is(Alert)).toEqual(true);
|
||||
expect(el.childAt(1).children().getElement()).toEqual(
|
||||
<FormattedMessage {...messages.successDialog} />,
|
||||
);
|
||||
expect(el.childAt(1).props().show).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,187 +1,108 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { render, screen, initializeMocks } from 'testUtilsExtra';
|
||||
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';
|
||||
|
||||
import { HistoryTable, mapHistoryRows, mapStateToProps } from './HistoryTable';
|
||||
import ResultsSummary from './ResultsSummary';
|
||||
import { HistoryTable, mapStateToProps } from './HistoryTable';
|
||||
|
||||
initializeMocks();
|
||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
||||
|
||||
jest.mock('@openedx/paragon', () => ({
|
||||
...jest.requireActual('@openedx/paragon'),
|
||||
DataTable: jest.fn(() => <div data-testid="data-table">DataTable</div>),
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
defineMessages: m => m,
|
||||
FormattedMessage: () => 'FormattedMessage',
|
||||
}));
|
||||
jest.mock('./ResultsSummary', () => jest.fn(() => <div data-testid="results-summary">ResultsSummary</div>));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: {
|
||||
bulkManagementHistoryEntries: jest.fn(),
|
||||
bulkManagementHistoryEntries: jest.fn(state => ({ historyEntries: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('./ResultsSummary', () => 'ResultsSummary');
|
||||
|
||||
describe('HistoryTable', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockBulkManagementHistory = [
|
||||
{
|
||||
originalFilename: 'test-file-1.csv',
|
||||
user: 'test-user-1',
|
||||
timeUploaded: '2025-01-01T10:00:00Z',
|
||||
resultsSummary: {
|
||||
rowId: 1,
|
||||
text: 'Download results 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
originalFilename: 'test-file-2.csv',
|
||||
user: 'test-user-2',
|
||||
timeUploaded: '2025-01-02T10:00:00Z',
|
||||
resultsSummary: {
|
||||
rowId: 2,
|
||||
text: 'Download results 2',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('mapHistoryRows', () => {
|
||||
const mockRow = {
|
||||
resultsSummary: {
|
||||
rowId: 1,
|
||||
text: 'Download results',
|
||||
},
|
||||
originalFilename: 'test-file.csv',
|
||||
user: 'test-user',
|
||||
timeUploaded: '2025-01-01T10:00:00Z',
|
||||
};
|
||||
|
||||
it('transforms row data correctly', () => {
|
||||
const result = mapHistoryRows(mockRow);
|
||||
|
||||
expect(result).toHaveProperty('resultsSummary');
|
||||
expect(result).toHaveProperty('filename');
|
||||
expect(result).toHaveProperty('user');
|
||||
expect(result).toHaveProperty('timeUploaded');
|
||||
expect(result.timeUploaded).toBe('2025-01-01T10:00:00Z');
|
||||
});
|
||||
|
||||
it('wraps filename in span with correct class', () => {
|
||||
const result = mapHistoryRows(mockRow);
|
||||
render(<div>{result.filename}</div>);
|
||||
|
||||
const filenameSpan = screen.getByText('test-file.csv');
|
||||
expect(filenameSpan).toBeInTheDocument();
|
||||
expect(filenameSpan).toHaveClass('wrap-text-in-cell');
|
||||
});
|
||||
|
||||
it('wraps user in span with correct class', () => {
|
||||
const result = mapHistoryRows(mockRow);
|
||||
render(<div>{result.user}</div>);
|
||||
|
||||
const userSpan = screen.getByText('test-user');
|
||||
expect(userSpan).toBeInTheDocument();
|
||||
expect(userSpan).toHaveClass('wrap-text-in-cell');
|
||||
});
|
||||
|
||||
it('renders ResultsSummary component with correct props', () => {
|
||||
const result = mapHistoryRows(mockRow);
|
||||
render(<div>{result.resultsSummary}</div>);
|
||||
|
||||
expect(ResultsSummary).toHaveBeenCalledWith(mockRow.resultsSummary, {});
|
||||
expect(screen.getByTestId('results-summary')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
it('renders DataTable with empty data when no history provided', () => {
|
||||
render(<HistoryTable />);
|
||||
|
||||
expect(DataTable).toHaveBeenCalledWith(
|
||||
{
|
||||
data: [],
|
||||
hasFixedColumnWidths: true,
|
||||
columns: bulkManagementColumns,
|
||||
className: 'table-striped',
|
||||
itemCount: 0,
|
||||
},
|
||||
{},
|
||||
);
|
||||
expect(screen.getByTestId('data-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders DataTable with mapped history data', () => {
|
||||
render(
|
||||
<HistoryTable bulkManagementHistory={mockBulkManagementHistory} />,
|
||||
);
|
||||
|
||||
expect(DataTable).toHaveBeenCalledWith(
|
||||
{
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
filename: expect.any(Object),
|
||||
user: expect.any(Object),
|
||||
resultsSummary: expect.any(Object),
|
||||
timeUploaded: '2025-01-01T10:00:00Z',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
filename: expect.any(Object),
|
||||
user: expect.any(Object),
|
||||
resultsSummary: expect.any(Object),
|
||||
timeUploaded: '2025-01-02T10:00:00Z',
|
||||
}),
|
||||
]),
|
||||
hasFixedColumnWidths: true,
|
||||
columns: bulkManagementColumns,
|
||||
className: 'table-striped',
|
||||
itemCount: 2,
|
||||
},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it('passes correct props to DataTable', () => {
|
||||
render(
|
||||
<HistoryTable bulkManagementHistory={mockBulkManagementHistory} />,
|
||||
);
|
||||
|
||||
const dataTableCall = DataTable.mock.calls[0][0];
|
||||
expect(dataTableCall.hasFixedColumnWidths).toBe(true);
|
||||
expect(dataTableCall.columns).toBe(bulkManagementColumns);
|
||||
expect(dataTableCall.className).toBe('table-striped');
|
||||
expect(dataTableCall.itemCount).toBe(mockBulkManagementHistory.length);
|
||||
const entry1 = {
|
||||
originalFilename: 'blue.png',
|
||||
user: 'Eifel',
|
||||
timeUploaded: '65',
|
||||
resultsSummary: {
|
||||
rowId: 12,
|
||||
courseId: 'Da Bu Dee',
|
||||
text: 'Da ba daa',
|
||||
},
|
||||
};
|
||||
const entry2 = {
|
||||
originalFilename: 'allStar.jpg',
|
||||
user: 'Smashmouth',
|
||||
timeUploaded: '2000s?',
|
||||
resultsSummary: {
|
||||
courseId: 'rockstar',
|
||||
rowId: 2,
|
||||
text: 'all that glitters is gold',
|
||||
},
|
||||
};
|
||||
const props = {
|
||||
bulkManagementHistory: [entry1, entry2],
|
||||
};
|
||||
let el;
|
||||
describe('snapshot', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<HistoryTable {...props} />);
|
||||
});
|
||||
test('snapshot - loads formatted table', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
describe('history table', () => {
|
||||
let table;
|
||||
beforeEach(() => {
|
||||
table = el.find(DataTable);
|
||||
});
|
||||
describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => {
|
||||
const fieldAssertions = [
|
||||
'maps resultsSummay to ResultsSummary',
|
||||
'wraps filename and user',
|
||||
'forwards the rest',
|
||||
];
|
||||
test(`snapshot: ${fieldAssertions.join(', ')}`, () => {
|
||||
expect(table.props().data).toMatchSnapshot();
|
||||
});
|
||||
test(fieldAssertions.join(', '), () => {
|
||||
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(
|
||||
rows[0].filename,
|
||||
).toEqual(<span className="wrap-text-in-cell">{entry1.originalFilename}</span>);
|
||||
expect(rows[1].resultsSummary).toEqual(<ResultsSummary {...entry2.resultsSummary} />);
|
||||
expect(rows[1].user).toEqual(<span className="wrap-text-in-cell">{entry2.user}</span>);
|
||||
expect(
|
||||
rows[1].filename,
|
||||
).toEqual(<span className="wrap-text-in-cell">{entry2.originalFilename}</span>);
|
||||
});
|
||||
});
|
||||
test('columns from bulkManagementColumns', () => {
|
||||
expect(table.props().columns).toEqual(bulkManagementColumns);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const mockState = { test: 'state' };
|
||||
const mockHistoryEntries = [
|
||||
{ originalFilename: 'file1.csv', user: 'user1' },
|
||||
{ originalFilename: 'file2.csv', user: 'user2' },
|
||||
];
|
||||
|
||||
const testState = { a: 'simple', test: 'state' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
selectors.grades.bulkManagementHistoryEntries.mockReturnValue(
|
||||
mockHistoryEntries,
|
||||
);
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
|
||||
it('maps bulkManagementHistory from selector', () => {
|
||||
const result = mapStateToProps(mockState);
|
||||
|
||||
test('bulkManagementHistory from grades.bulkManagementHistoryEntries', () => {
|
||||
expect(
|
||||
selectors.grades.bulkManagementHistoryEntries,
|
||||
).toHaveBeenCalledWith(mockState);
|
||||
expect(result.bulkManagementHistory).toBe(mockHistoryEntries);
|
||||
mapped.bulkManagementHistory,
|
||||
).toEqual(selectors.grades.bulkManagementHistoryEntries(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -19,8 +19,10 @@ const ResultsSummary = ({
|
||||
text,
|
||||
}) => (
|
||||
<Hyperlink
|
||||
destination={lms.urls.bulkGradesUrlByRow(rowId)}
|
||||
href={lms.urls.bulkGradesUrlByRow(rowId)}
|
||||
destination="www.edx.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<Icon src={Download} className="d-inline-block" />
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { Download } from '@edx/paragon/icons';
|
||||
|
||||
import lms from 'data/services/lms';
|
||||
import { renderWithIntl, screen } from '../../testUtilsExtra';
|
||||
import ResultsSummary from './ResultsSummary';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Hyperlink: () => 'Hyperlink',
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('@edx/paragon/icons', () => ({
|
||||
Download: 'DownloadIcon',
|
||||
}));
|
||||
jest.mock('data/services/lms', () => ({
|
||||
urls: {
|
||||
bulkGradesUrlByRow: jest.fn((rowId) => (`www.edx.org/${rowId}`)),
|
||||
bulkGradesUrlByRow: jest.fn((rowId) => ({ url: { rowId } })),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -15,21 +25,29 @@ describe('ResultsSummary component', () => {
|
||||
rowId: 42,
|
||||
text: 'texty',
|
||||
};
|
||||
let link;
|
||||
let el;
|
||||
const assertions = [
|
||||
'safe hyperlink with bulkGradesUrl with course and row id',
|
||||
'download icon',
|
||||
'results text',
|
||||
];
|
||||
beforeEach(() => {
|
||||
renderWithIntl(<ResultsSummary {...props} />);
|
||||
link = screen.getByRole('link', { name: props.text });
|
||||
el = shallow(<ResultsSummary {...props} />);
|
||||
});
|
||||
test(`snapshot - ${assertions.join(', ')}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('Hyperlink has target="_blank" and rel="noopener noreferrer"', () => {
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
expect(el.props().target).toEqual('_blank');
|
||||
expect(el.props().rel).toEqual('noopener noreferrer');
|
||||
});
|
||||
test('Hyperlink has href to bulkGradesUrl', () => {
|
||||
expect(link).toHaveAttribute('href', lms.urls.bulkGradesUrlByRow(props.rowId));
|
||||
expect(el.props().href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
|
||||
});
|
||||
test('displays Download Icon and text', () => {
|
||||
expect(link).toHaveTextContent(props.text);
|
||||
const icon = screen.getByRole('img', { hidden: true });
|
||||
expect(icon).toBeInTheDocument();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BulkManagementAlerts component no errer, no upload success snapshot - bulkImportError closed, success closed 1`] = `
|
||||
<Fragment>
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={false}
|
||||
variant="danger"
|
||||
/>
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={false}
|
||||
variant="success"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="CSV processing. File uploads may take several minutes to complete."
|
||||
description="Success Dialog message in BulkManagement Tab File Upload Form"
|
||||
id="gradebook.BulkManagementHistoryView.successDialog"
|
||||
/>
|
||||
</Alert>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`BulkManagementAlerts component no errer, no upload success snapshot - danger alert open with bulkImportError, success alert open with messages.successDialog 1`] = `
|
||||
<Fragment>
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={true}
|
||||
variant="danger"
|
||||
>
|
||||
Oh noooooo
|
||||
</Alert>
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={true}
|
||||
variant="success"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="CSV processing. File uploads may take several minutes to complete."
|
||||
description="Success Dialog message in BulkManagement Tab File Upload Form"
|
||||
id="gradebook.BulkManagementHistoryView.successDialog"
|
||||
/>
|
||||
</Alert>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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"
|
||||
>
|
||||
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>,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
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,
|
||||
"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}
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResultsSummary component snapshot - safe hyperlink with bulkGradesUrl with course and row id, download icon, results text 1`] = `
|
||||
<Hyperlink
|
||||
destination="www.edx.org"
|
||||
href={
|
||||
Object {
|
||||
"url": Object {
|
||||
"rowId": 42,
|
||||
},
|
||||
}
|
||||
}
|
||||
rel="noopener noreferrer"
|
||||
showLaunchIcon={false}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block"
|
||||
src="DownloadIcon"
|
||||
/>
|
||||
texty
|
||||
</Hyperlink>
|
||||
`;
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BulkManagementHistoryView component snapshot snapshot - loads heading from messages.BulkManagementHistoryView.heading, <BulkManagementAlerts />, <HistoryTable /> 1`] = `
|
||||
<div
|
||||
className="bulk-management-history-view"
|
||||
>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
defaultMessage="Bulk Management History"
|
||||
description="Heading text for BulkManagement History Tab"
|
||||
id="gradebook.BulkManagementHistoryView.heading"
|
||||
/>
|
||||
</h4>
|
||||
<p
|
||||
className="help-text"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override."
|
||||
description="Bulk Management History View help text"
|
||||
id="gradebook.BulkManagementHistoryView"
|
||||
/>
|
||||
</p>
|
||||
<BulkManagementAlerts />
|
||||
<HistoryTable />
|
||||
</div>
|
||||
`;
|
||||
@@ -1,24 +1,43 @@
|
||||
import { render, initializeMocks, screen } from 'testUtilsExtra';
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { BulkManagementHistoryView } from '.';
|
||||
import BulkManagementAlerts from './BulkManagementAlerts';
|
||||
import HistoryTable from './HistoryTable';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('./BulkManagementAlerts', () => jest.fn(() => <div>BulkManagementAlerts</div>));
|
||||
jest.mock('./HistoryTable', () => jest.fn(() => <div>HistoryTable</div>));
|
||||
|
||||
initializeMocks();
|
||||
jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts');
|
||||
jest.mock('./HistoryTable', () => 'HistoryTable');
|
||||
|
||||
describe('BulkManagementHistoryView', () => {
|
||||
describe('component', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
render(<BulkManagementHistoryView />);
|
||||
el = shallow(<BulkManagementHistoryView />);
|
||||
});
|
||||
describe('render alerts and heading', () => {
|
||||
it('heading - h4 loaded from messages', () => {
|
||||
expect(screen.getByText(messages.heading.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.helpText.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText('BulkManagementAlerts')).toBeInTheDocument();
|
||||
expect(screen.getByText('HistoryTable')).toBeInTheDocument();
|
||||
describe('snapshot', () => {
|
||||
const snapshotSegments = [
|
||||
'heading from messages.BulkManagementHistoryView.heading',
|
||||
'<BulkManagementAlerts />',
|
||||
'<HistoryTable />',
|
||||
];
|
||||
test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('heading - h4 loaded from messages', () => {
|
||||
const heading = el.find('h4');
|
||||
expect(heading.getElement()).toEqual((
|
||||
<h4>
|
||||
<FormattedMessage {...messages.heading} />
|
||||
</h4>
|
||||
));
|
||||
});
|
||||
test('heading, then alerts, then upload form, then table', () => {
|
||||
expect(el.childAt(0).is('h4')).toEqual(true);
|
||||
expect(el.childAt(2).is(BulkManagementAlerts)).toEqual(true);
|
||||
expect(el.childAt(3).is(HistoryTable)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ const messages = defineMessages({
|
||||
},
|
||||
helpText: {
|
||||
id: 'gradebook.BulkManagementHistoryView',
|
||||
defaultMessage: 'Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override.',
|
||||
defaultMessage: 'Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override.',
|
||||
description: 'Bulk Management History View help text',
|
||||
},
|
||||
successDialog: {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import Header from '.';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform'),
|
||||
getConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Header', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('has edx link with logo url', () => {
|
||||
const url = 'www.ourLogo.url';
|
||||
const baseUrl = 'www.lms.url';
|
||||
getConfig.mockReturnValue({ LOGO_URL: url, LMS_BASE_URL: baseUrl });
|
||||
|
||||
render(
|
||||
<IntlProvider messages={{}} locale="en">
|
||||
<Header />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const link = screen.getByRole('link');
|
||||
const logo = screen.getByAltText('edX logo');
|
||||
|
||||
expect(link).toHaveAttribute('href', `${baseUrl}/dashboard`);
|
||||
expect(logo).toHaveAttribute('src', url);
|
||||
});
|
||||
});
|
||||
23
src/components/EdxHeader/__snapshots__/test.jsx.snap
Normal file
23
src/components/EdxHeader/__snapshots__/test.jsx.snap
Normal file
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header snapshot - has edx link with logo url 1`] = `
|
||||
<div
|
||||
className="mb-3"
|
||||
>
|
||||
<header
|
||||
className="d-flex justify-content-center align-items-center p-3 border-bottom-blue"
|
||||
>
|
||||
<Hyperlink
|
||||
destination="undefined/dashboard"
|
||||
>
|
||||
<img
|
||||
alt="edX logo"
|
||||
height="30"
|
||||
src="www.ourLogo.url"
|
||||
width="60"
|
||||
/>
|
||||
</Hyperlink>
|
||||
<div />
|
||||
</header>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
/**
|
||||
|
||||
21
src/components/EdxHeader/test.jsx
Normal file
21
src/components/EdxHeader/test.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import Header from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Hyperlink: () => 'Hyperlink',
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Header', () => {
|
||||
test('snapshot - has edx link with logo url', () => {
|
||||
const url = 'www.ourLogo.url';
|
||||
getConfig.mockReturnValue({ LOGO_URL: url });
|
||||
expect(shallow(<Header />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AssignmentFilter component render snapshot 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="assignment"
|
||||
label="Assignment"
|
||||
onChange={[MockFunction]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
All
|
||||
</option>,
|
||||
<option
|
||||
value="label1"
|
||||
>
|
||||
label1
|
||||
:
|
||||
sLabel1
|
||||
</option>,
|
||||
<option
|
||||
value="label2"
|
||||
>
|
||||
label2
|
||||
:
|
||||
sLabel2
|
||||
</option>,
|
||||
<option
|
||||
value="label3"
|
||||
>
|
||||
label3
|
||||
:
|
||||
sLabel3
|
||||
</option>,
|
||||
<option
|
||||
value="label4"
|
||||
>
|
||||
label4
|
||||
:
|
||||
sLabel4
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="test-label"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,9 +1,12 @@
|
||||
import React from 'react';
|
||||
import { render, screen, initializeMocks } from 'testUtilsExtra';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
import useAssignmentFilterData from './hooks';
|
||||
import AssignmentFilter from '.';
|
||||
|
||||
jest.mock('../SelectGroup', () => 'SelectGroup');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const handleChange = jest.fn();
|
||||
@@ -22,16 +25,29 @@ useAssignmentFilterData.mockReturnValue({
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let el;
|
||||
describe('AssignmentFilter component', () => {
|
||||
beforeAll(() => {
|
||||
initializeMocks();
|
||||
render(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useAssignmentFilterData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('filter options', () => {
|
||||
expect(screen.getByRole('combobox', { name: 'Assignment' })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('option')).toHaveLength(assignmentFilterOptions.length + 1); // +1 for the default option
|
||||
expect(screen.getAllByRole('option')[assignmentFilterOptions.length]).toHaveTextContent(assignmentFilterOptions[assignmentFilterOptions.length - 1].label);
|
||||
const { options } = el.find(SelectGroup).props();
|
||||
expect(options.length).toEqual(5);
|
||||
const testOption = assignmentFilterOptions[0];
|
||||
const optionProps = options[1].props;
|
||||
expect(optionProps.value).toEqual(testOption.label);
|
||||
expect(optionProps.children.join(''))
|
||||
.toEqual(`${testOption.label}: ${testOption.subsectionLabel}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AssignmentFilter component render with selected assignment snapshot 1`] = `
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
disabled={false}
|
||||
id="assignmentGradeMin"
|
||||
label="Min Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={23}
|
||||
/>
|
||||
<PercentGroup
|
||||
disabled={false}
|
||||
id="assignmentGradeMax"
|
||||
label="Max Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={300}
|
||||
/>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<Button
|
||||
disabled={false}
|
||||
name="assignmentGradeMinMax"
|
||||
type="submit"
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AssignmentFilter component render without selected assignment snapshot 1`] = `
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
disabled={true}
|
||||
id="assignmentGradeMin"
|
||||
label="Min Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={23}
|
||||
/>
|
||||
<PercentGroup
|
||||
disabled={true}
|
||||
id="assignmentGradeMax"
|
||||
label="Max Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={300}
|
||||
/>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<Button
|
||||
disabled={true}
|
||||
name="assignmentGradeMinMax"
|
||||
type="submit"
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -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';
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import PercentGroup from '../PercentGroup';
|
||||
import useAssignmentGradeFilterData from './hooks';
|
||||
import AssignmentFilter from '.';
|
||||
import { renderWithIntl } from '../../../testUtilsExtra';
|
||||
|
||||
jest.mock('../PercentGroup', () => 'PercentGroup');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const hookData = {
|
||||
handleSubmit: jest.fn(),
|
||||
handleChange: jest.fn(),
|
||||
handleSetMax: jest.fn(),
|
||||
handleSetMin: jest.fn(),
|
||||
selectedAssignment: 'test-assignment',
|
||||
@@ -20,39 +22,37 @@ useAssignmentGradeFilterData.mockReturnValue(hookData);
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let el;
|
||||
describe('AssignmentFilter component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
expect(useAssignmentGradeFilterData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('with selected assignment', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('renders a PercentGroup for both Max and Min filters', async () => {
|
||||
const user = userEvent.setup();
|
||||
const minGradeInput = screen.getByRole('spinbutton', { name: /Min Grade/i });
|
||||
const maxGradeInput = screen.getByRole('spinbutton', { name: /Max Grade/i });
|
||||
expect(minGradeInput).toBeInTheDocument();
|
||||
expect(maxGradeInput).toBeInTheDocument();
|
||||
expect(minGradeInput).toBeEnabled();
|
||||
expect(maxGradeInput).toBeEnabled();
|
||||
await user.type(minGradeInput, '25');
|
||||
expect(hookData.handleSetMin).toHaveBeenCalled();
|
||||
await user.type(maxGradeInput, '50');
|
||||
expect(hookData.handleSetMax).toHaveBeenCalled();
|
||||
it('renders a PercentGroup for both Max and Min filters', () => {
|
||||
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.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', async () => {
|
||||
const user = userEvent.setup();
|
||||
const submitButton = screen.getByRole('button', { name: /Apply/ });
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
expect(submitButton).not.toHaveAttribute('disabled');
|
||||
await user.click(submitButton);
|
||||
expect(hookData.handleSubmit).toHaveBeenCalled();
|
||||
it('renders a submit button', () => {
|
||||
const props = el.find(Button).props();
|
||||
expect(props.disabled).toEqual(false);
|
||||
expect(props.onClick).toEqual(hookData.handleSubmit);
|
||||
});
|
||||
});
|
||||
describe('without selected assignment', () => {
|
||||
@@ -61,13 +61,16 @@ describe('AssignmentFilter component', () => {
|
||||
...hookData,
|
||||
selectedAssignment: null,
|
||||
});
|
||||
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('disables controls', () => {
|
||||
const minGrade = screen.getByRole('spinbutton', { name: /Min Grade/ });
|
||||
const maxGrade = screen.getByRole('spinbutton', { name: /Max Grade/ });
|
||||
expect(minGrade).toHaveAttribute('disabled');
|
||||
expect(maxGrade).toHaveAttribute('disabled');
|
||||
let props = el.find(PercentGroup).at(0).props();
|
||||
expect(props.disabled).toEqual(true);
|
||||
props = el.find(PercentGroup).at(1).props();
|
||||
expect(props.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AssignmentFilterType component render snapshot 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<SelectGroup
|
||||
disabled={true}
|
||||
id="assignment-types"
|
||||
label="Assignment Types"
|
||||
onChange={[MockFunction]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
All
|
||||
</option>,
|
||||
<option
|
||||
value="test-type"
|
||||
>
|
||||
test-type
|
||||
</option>,
|
||||
<option
|
||||
value="type1"
|
||||
>
|
||||
type1
|
||||
</option>,
|
||||
<option
|
||||
value="type2"
|
||||
>
|
||||
type2
|
||||
</option>,
|
||||
<option
|
||||
value="type3"
|
||||
>
|
||||
type3
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="test-type"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
import useAssignmentFilterTypeData from './hooks';
|
||||
import AssignmentFilterType from '.';
|
||||
import { renderWithIntl } from '../../../testUtilsExtra';
|
||||
|
||||
jest.mock('../SelectGroup', () => 'SelectGroup');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const handleChange = jest.fn();
|
||||
@@ -19,15 +21,27 @@ useAssignmentFilterTypeData.mockReturnValue({
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let el;
|
||||
describe('AssignmentFilterType component', () => {
|
||||
beforeAll(() => {
|
||||
renderWithIntl(<AssignmentFilterType updateQueryParams={updateQueryParams} />);
|
||||
el = shallow(<AssignmentFilterType updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useAssignmentFilterTypeData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('filter options', () => {
|
||||
const options = screen.getAllByRole('option');
|
||||
expect(options.length).toEqual(5); // 4 types + "All Types"
|
||||
expect(options[1]).toHaveTextContent(testType);
|
||||
const { options } = el.find(SelectGroup).props();
|
||||
expect(options.length).toEqual(5);
|
||||
const optionProps = options[1].props;
|
||||
expect(optionProps.value).toEqual(assignmentTypes[0]);
|
||||
expect(optionProps.children).toEqual(testType);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseFilter component render if disabled snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
id="minimum-grade"
|
||||
label="Min Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={23}
|
||||
/>
|
||||
<PercentGroup
|
||||
id="maximum-grade"
|
||||
label="Max Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={300}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<Button
|
||||
disabled={true}
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseFilter component render with selected assignment snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="grade-filter-inputs"
|
||||
>
|
||||
<PercentGroup
|
||||
id="minimum-grade"
|
||||
label="Min Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={23}
|
||||
/>
|
||||
<PercentGroup
|
||||
id="maximum-grade"
|
||||
label="Max Grade"
|
||||
onChange={[MockFunction]}
|
||||
value={300}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="grade-filter-action"
|
||||
>
|
||||
<Button
|
||||
disabled={false}
|
||||
variant="outline-secondary"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -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';
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import PercentGroup from '../PercentGroup';
|
||||
import useCourseGradeFilterData from './hooks';
|
||||
import CourseFilter from '.';
|
||||
import { renderWithIntl } from '../../../testUtilsExtra';
|
||||
|
||||
jest.mock('../PercentGroup', () => 'PercentGroup');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const hookData = {
|
||||
@@ -24,37 +27,48 @@ useCourseGradeFilterData.mockReturnValue(hookData);
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let el;
|
||||
describe('CourseFilter component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useCourseGradeFilterData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('with selected assignment', () => {
|
||||
beforeEach(() => {
|
||||
renderWithIntl(<CourseFilter updateQueryParams={updateQueryParams} />);
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a PercentGroup for both Max and Min filters', () => {
|
||||
expect(screen.getByRole('spinbutton', { name: 'Min Grade' })).toHaveValue(hookData.min.value);
|
||||
expect(screen.getByRole('spinbutton', { name: 'Max Grade' })).toHaveValue(hookData.max.value);
|
||||
let props = el.find(PercentGroup).at(0).props();
|
||||
expect(props.value).toEqual(hookData.min.value);
|
||||
expect(props.onChange).toEqual(hookData.min.onChange);
|
||||
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', () => {
|
||||
expect(screen.getByRole('button', { name: 'Apply' })).toBeInTheDocument();
|
||||
// Expect it to be enabled
|
||||
expect(screen.getByRole('button', { name: 'Apply' })).not.toBeDisabled();
|
||||
const props = el.find(Button).props();
|
||||
expect(props.disabled).toEqual(false);
|
||||
expect(props.onClick).toEqual(hookData.handleApplyClick);
|
||||
});
|
||||
});
|
||||
describe('if disabled', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useCourseGradeFilterData.mockReturnValueOnce({ ...hookData, isDisabled: true });
|
||||
renderWithIntl(<CourseFilter updateQueryParams={updateQueryParams} />);
|
||||
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('disables submit', () => {
|
||||
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
|
||||
const props = el.find(Button).props();
|
||||
expect(props.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { render, screen, initializeMocks } from 'testUtilsExtra';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import PercentGroup from './PercentGroup';
|
||||
|
||||
@@ -11,7 +12,6 @@ describe('PercentGroup', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
initializeMocks();
|
||||
props = {
|
||||
...props,
|
||||
onChange: jest.fn().mockName('props.onChange'),
|
||||
@@ -19,17 +19,15 @@ describe('PercentGroup', () => {
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
test('is displayed', () => {
|
||||
render(<PercentGroup {...props} />);
|
||||
expect(screen.getByRole('spinbutton', { name: 'Group Label' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Group Label')).toBeVisible();
|
||||
expect(screen.getByText('%')).toBeVisible();
|
||||
});
|
||||
test('disabled', () => {
|
||||
render(<PercentGroup {...props} disabled />);
|
||||
expect(screen.getByRole('spinbutton', { name: 'Group Label' })).toBeDisabled();
|
||||
expect(screen.getByText('Group Label')).toBeVisible();
|
||||
expect(screen.getByText('%')).toBeVisible();
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const el = shallow(<PercentGroup {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('disabled', () => {
|
||||
const el = shallow(<PercentGroup {...props} disabled />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import SelectGroup from './SelectGroup';
|
||||
|
||||
@@ -24,14 +24,15 @@ describe('SelectGroup', () => {
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
test('rendered with all options and label', () => {
|
||||
render(<SelectGroup {...props} />);
|
||||
expect(screen.getAllByRole('option')).toHaveLength(props.options.length);
|
||||
expect(screen.getByLabelText(props.label)).toBeInTheDocument();
|
||||
});
|
||||
test('disabled', () => {
|
||||
render(<SelectGroup {...props} disabled />);
|
||||
expect(screen.getByRole('combobox')).toBeDisabled();
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const el = shallow(<SelectGroup {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('disabled', () => {
|
||||
const el = shallow(<SelectGroup {...props} disabled />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StudentGroupsFilter component render snapshot 1`] = `
|
||||
<Fragment>
|
||||
<SelectGroup
|
||||
id="Tracks"
|
||||
label="Tracks"
|
||||
onChange={[MockFunction]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value="Track-All"
|
||||
>
|
||||
Track-All
|
||||
</option>,
|
||||
<option
|
||||
value="v1"
|
||||
>
|
||||
n1
|
||||
</option>,
|
||||
<option
|
||||
value="v2"
|
||||
>
|
||||
n2
|
||||
</option>,
|
||||
<option
|
||||
value="v3"
|
||||
>
|
||||
n3
|
||||
</option>,
|
||||
<option
|
||||
value="v4"
|
||||
>
|
||||
n4
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="test-track"
|
||||
/>
|
||||
<SelectGroup
|
||||
disabled={false}
|
||||
id="Cohorts"
|
||||
label="Cohorts"
|
||||
onChange={[MockFunction]}
|
||||
options={
|
||||
Array [
|
||||
<option
|
||||
value="Cohort-All"
|
||||
>
|
||||
Cohort-All
|
||||
</option>,
|
||||
<option
|
||||
value="v1"
|
||||
>
|
||||
n1
|
||||
</option>,
|
||||
<option
|
||||
value="v2"
|
||||
>
|
||||
n2
|
||||
</option>,
|
||||
<option
|
||||
value="v3"
|
||||
>
|
||||
n3
|
||||
</option>,
|
||||
]
|
||||
}
|
||||
value="test-cohort"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,164 +1,84 @@
|
||||
import React from 'react';
|
||||
import { render, screen, initializeMocks } from 'testUtilsExtra';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SelectGroup from '../SelectGroup';
|
||||
import { StudentGroupsFilter } from './index';
|
||||
import useStudentGroupsFilterData from './hooks';
|
||||
import StudentGroupsFilter from '.';
|
||||
|
||||
jest.mock('../SelectGroup', () => jest.fn(() => <div data-testid="select-group">SelectGroup</div>));
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
jest.mock('../SelectGroup', () => 'SelectGroup');
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
initializeMocks();
|
||||
|
||||
describe('StudentGroupsFilter', () => {
|
||||
const mockUpdateQueryParams = jest.fn();
|
||||
|
||||
const mockTracksData = {
|
||||
value: 'test-track-value',
|
||||
const props = {
|
||||
cohorts: {
|
||||
value: 'test-cohort',
|
||||
entries: [
|
||||
{ value: 'track1', name: 'Track 1' },
|
||||
{ value: 'track2', name: 'Track 2' },
|
||||
],
|
||||
handleChange: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCohortsData = {
|
||||
value: 'test-cohort-value',
|
||||
entries: [
|
||||
{ value: 'cohort1', name: 'Cohort 1' },
|
||||
{ value: 'cohort2', name: 'Cohort 2' },
|
||||
{ value: 'v1', name: 'n1' },
|
||||
{ value: 'v2', name: 'n2' },
|
||||
{ value: 'v3', name: 'n3' },
|
||||
],
|
||||
handleChange: jest.fn(),
|
||||
isDisabled: false,
|
||||
};
|
||||
},
|
||||
tracks: {
|
||||
value: 'test-track',
|
||||
entries: [
|
||||
{ value: 'v1', name: 'n1' },
|
||||
{ value: 'v2', name: 'n2' },
|
||||
{ value: 'v3', name: 'n3' },
|
||||
{ value: 'v4', name: 'n4' },
|
||||
],
|
||||
handleChange: jest.fn(),
|
||||
},
|
||||
};
|
||||
useStudentGroupsFilterData.mockReturnValue(props);
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
let el;
|
||||
describe('StudentGroupsFilter component', () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
useStudentGroupsFilterData.mockReturnValue({
|
||||
tracks: mockTracksData,
|
||||
cohorts: mockCohortsData,
|
||||
el = shallow(<StudentGroupsFilter updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls useStudentGroupsFilterData hook with updateQueryParams', () => {
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({
|
||||
updateQueryParams: mockUpdateQueryParams,
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders two SelectGroup components', () => {
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
expect(SelectGroup).toHaveBeenCalledTimes(2);
|
||||
expect(screen.getAllByTestId('select-group')).toHaveLength(2);
|
||||
});
|
||||
|
||||
describe('tracks SelectGroup', () => {
|
||||
it('renders tracks SelectGroup with correct props', () => {
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
const tracksCall = SelectGroup.mock.calls[0][0];
|
||||
expect(tracksCall.id).toBe('Tracks');
|
||||
expect(tracksCall.value).toBe(mockTracksData.value);
|
||||
expect(tracksCall.onChange).toBe(mockTracksData.handleChange);
|
||||
test('track options', () => {
|
||||
const {
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
} = el.find(SelectGroup).at(0).props();
|
||||
expect(value).toEqual(props.tracks.value);
|
||||
expect(onChange).toEqual(props.tracks.handleChange);
|
||||
expect(options.length).toEqual(5);
|
||||
const testEntry = props.tracks.entries[0];
|
||||
const optionProps = options[1].props;
|
||||
expect(optionProps.value).toEqual(testEntry.value);
|
||||
expect(optionProps.children).toEqual(testEntry.name);
|
||||
});
|
||||
|
||||
it('includes trackAll option in tracks SelectGroup', () => {
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
const tracksCall = SelectGroup.mock.calls[0][0];
|
||||
const { options } = tracksCall;
|
||||
|
||||
expect(options).toHaveLength(3);
|
||||
expect(options[0].props.value).toBeDefined();
|
||||
expect(options[0].props.children).toBeDefined();
|
||||
});
|
||||
|
||||
it('includes track entries in tracks SelectGroup options', () => {
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
const tracksCall = SelectGroup.mock.calls[0][0];
|
||||
const { options } = tracksCall;
|
||||
|
||||
expect(options[1].props.value).toBe('track1');
|
||||
expect(options[1].props.children).toBe('Track 1');
|
||||
expect(options[2].props.value).toBe('track2');
|
||||
expect(options[2].props.children).toBe('Track 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cohorts SelectGroup', () => {
|
||||
it('renders cohorts SelectGroup with correct props', () => {
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
const cohortsCall = SelectGroup.mock.calls[1][0];
|
||||
expect(cohortsCall.id).toBe('Cohorts');
|
||||
expect(cohortsCall.value).toBe(mockCohortsData.value);
|
||||
expect(cohortsCall.onChange).toBe(mockCohortsData.handleChange);
|
||||
expect(cohortsCall.disabled).toBe(mockCohortsData.isDisabled);
|
||||
});
|
||||
|
||||
it('includes cohortAll option in cohorts SelectGroup', () => {
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
const cohortsCall = SelectGroup.mock.calls[1][0];
|
||||
const { options } = cohortsCall;
|
||||
|
||||
expect(options).toHaveLength(3);
|
||||
expect(options[0].props.value).toBeDefined();
|
||||
expect(options[0].props.children).toBeDefined();
|
||||
});
|
||||
|
||||
it('includes cohort entries in cohorts SelectGroup options', () => {
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
const cohortsCall = SelectGroup.mock.calls[1][0];
|
||||
const { options } = cohortsCall;
|
||||
|
||||
expect(options[1].props.value).toBe('cohort1');
|
||||
expect(options[1].props.children).toBe('Cohort 1');
|
||||
expect(options[2].props.value).toBe('cohort2');
|
||||
expect(options[2].props.children).toBe('Cohort 2');
|
||||
});
|
||||
|
||||
it('passes disabled state to cohorts SelectGroup', () => {
|
||||
useStudentGroupsFilterData.mockReturnValue({
|
||||
tracks: mockTracksData,
|
||||
cohorts: { ...mockCohortsData, isDisabled: true },
|
||||
});
|
||||
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
const cohortsCall = SelectGroup.mock.calls[1][0];
|
||||
expect(cohortsCall.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with empty entries', () => {
|
||||
it('handles empty tracks entries', () => {
|
||||
useStudentGroupsFilterData.mockReturnValue({
|
||||
tracks: { ...mockTracksData, entries: [] },
|
||||
cohorts: mockCohortsData,
|
||||
});
|
||||
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
const tracksCall = SelectGroup.mock.calls[0][0];
|
||||
expect(tracksCall.options).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('handles empty cohorts entries', () => {
|
||||
useStudentGroupsFilterData.mockReturnValue({
|
||||
tracks: mockTracksData,
|
||||
cohorts: { ...mockCohortsData, entries: [] },
|
||||
});
|
||||
|
||||
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
|
||||
|
||||
const cohortsCall = SelectGroup.mock.calls[1][0];
|
||||
expect(cohortsCall.options).toHaveLength(1);
|
||||
test('cohort options', () => {
|
||||
const {
|
||||
options,
|
||||
onChange,
|
||||
disabled,
|
||||
value,
|
||||
} = el.find(SelectGroup).at(1).props();
|
||||
expect(value).toEqual(props.cohorts.value);
|
||||
expect(disabled).toEqual(false);
|
||||
expect(onChange).toEqual(props.cohorts.handleChange);
|
||||
expect(options.length).toEqual(4);
|
||||
const testEntry = props.cohorts.entries[0];
|
||||
const optionProps = options[1].props;
|
||||
expect(optionProps.value).toEqual(testEntry.value);
|
||||
expect(optionProps.children).toEqual(testEntry.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PercentGroup Component snapshots basic snapshot 1`] = `
|
||||
<div
|
||||
className="percent-group"
|
||||
>
|
||||
<Form.Group
|
||||
controlId="group id"
|
||||
>
|
||||
<Form.Label>
|
||||
Group Label
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
disabled={false}
|
||||
max={100}
|
||||
min={0}
|
||||
onChange={[MockFunction props.onChange]}
|
||||
step={1}
|
||||
type="number"
|
||||
value="group VALUE"
|
||||
/>
|
||||
</Form.Group>
|
||||
<span
|
||||
className="input-percent-label"
|
||||
>
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PercentGroup Component snapshots disabled 1`] = `
|
||||
<div
|
||||
className="percent-group"
|
||||
>
|
||||
<Form.Group
|
||||
controlId="group id"
|
||||
>
|
||||
<Form.Label>
|
||||
Group Label
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
disabled={true}
|
||||
max={100}
|
||||
min={0}
|
||||
onChange={[MockFunction props.onChange]}
|
||||
step={1}
|
||||
type="number"
|
||||
value="group VALUE"
|
||||
/>
|
||||
</Form.Group>
|
||||
<span
|
||||
className="input-percent-label"
|
||||
>
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,79 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectGroup Component snapshots basic snapshot 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<Form.Group
|
||||
controlId="group id"
|
||||
>
|
||||
<Form.Label>
|
||||
Group Label
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
disabled={false}
|
||||
onChange={[MockFunction props.onChange]}
|
||||
value="group VALUE"
|
||||
>
|
||||
<option
|
||||
key="opt1"
|
||||
value="opt1"
|
||||
>
|
||||
Option 1
|
||||
</option>
|
||||
<option
|
||||
key="opt2"
|
||||
value="opt2"
|
||||
>
|
||||
Option 2
|
||||
</option>
|
||||
<option
|
||||
key="opt3"
|
||||
value="opt3"
|
||||
>
|
||||
Option 3
|
||||
</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SelectGroup Component snapshots disabled 1`] = `
|
||||
<div
|
||||
className="student-filters"
|
||||
>
|
||||
<Form.Group
|
||||
controlId="group id"
|
||||
>
|
||||
<Form.Label>
|
||||
Group Label
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
disabled={true}
|
||||
onChange={[MockFunction props.onChange]}
|
||||
value="group VALUE"
|
||||
>
|
||||
<option
|
||||
key="opt1"
|
||||
value="opt1"
|
||||
>
|
||||
Option 1
|
||||
</option>
|
||||
<option
|
||||
key="opt2"
|
||||
value="opt2"
|
||||
>
|
||||
Option 2
|
||||
</option>
|
||||
<option
|
||||
key="opt3"
|
||||
value="opt3"
|
||||
>
|
||||
Option 3
|
||||
</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,70 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookFilters render snapshot 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="filter-sidebar-header"
|
||||
>
|
||||
<h2>
|
||||
<Icon
|
||||
className="fa fa-filter"
|
||||
/>
|
||||
</h2>
|
||||
<IconButton
|
||||
alt="Close Filters"
|
||||
aria-label="Close Filters"
|
||||
className="p-1"
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction hook.closeMenu]}
|
||||
src={[Function]}
|
||||
/>
|
||||
</div>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Assignments"
|
||||
>
|
||||
<div>
|
||||
<AssignmentTypeFilter
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
<AssignmentFilter
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
<AssignmentGradeFilter
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Overall Grade"
|
||||
>
|
||||
<CourseGradeFilter
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Student Groups"
|
||||
>
|
||||
<StudentGroupsFilter
|
||||
updateQueryParams={[MockFunction]}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
className="filter-group mb-3"
|
||||
defaultOpen={true}
|
||||
title="Include Course Team Members"
|
||||
>
|
||||
<Form.Checkbox
|
||||
checked={true}
|
||||
onChange={[MockFunction hook.handleChange]}
|
||||
>
|
||||
Include Course Team Members
|
||||
</Form.Checkbox>
|
||||
</Collapsible>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -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 } }) => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,30 +1,82 @@
|
||||
import React from 'react';
|
||||
import { render, screen, initializeMocks } from 'testUtilsExtra';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import AssignmentTypeFilter from './AssignmentTypeFilter';
|
||||
import AssignmentFilter from './AssignmentFilter';
|
||||
import AssignmentGradeFilter from './AssignmentGradeFilter';
|
||||
import CourseGradeFilter from './CourseGradeFilter';
|
||||
import StudentGroupsFilter from './StudentGroupsFilter';
|
||||
import messages from './messages';
|
||||
|
||||
import useGradebookFiltersData from './hooks';
|
||||
import GradebookFilters from '.';
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
jest.mock('./AssignmentTypeFilter', () => 'AssignmentTypeFilter');
|
||||
jest.mock('./AssignmentFilter', () => 'AssignmentFilter');
|
||||
jest.mock('./AssignmentGradeFilter', () => 'AssignmentGradeFilter');
|
||||
jest.mock('./CourseGradeFilter', () => 'CourseGradeFilter');
|
||||
jest.mock('./StudentGroupsFilter', () => 'StudentGroupsFilter');
|
||||
|
||||
initializeMocks();
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
|
||||
const hookProps = {
|
||||
closeMenu: jest.fn().mockName('hook.closeMenu'),
|
||||
includeCourseTeamMembers: {
|
||||
value: true,
|
||||
handleChange: jest.fn().mockName('hook.handleChange'),
|
||||
},
|
||||
};
|
||||
useGradebookFiltersData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
describe('GradebookFilters', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
render(<GradebookFilters updateQueryParams={updateQueryParams} />);
|
||||
el = shallow(<GradebookFilters updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('All filters render together', () => {
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useGradebookFiltersData).toHaveBeenCalledWith({ updateQueryParams });
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('Assignment filters', () => {
|
||||
expect(screen.getByRole('combobox', { name: 'Assignment Types' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox', { name: 'Assignment' })).toBeInTheDocument();
|
||||
expect(el.find(Collapsible).at(0).children()).toMatchObject(shallow(
|
||||
<div>
|
||||
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
|
||||
<AssignmentFilter updateQueryParams={updateQueryParams} />
|
||||
<AssignmentGradeFilter updateQueryParams={updateQueryParams} />
|
||||
</div>,
|
||||
));
|
||||
});
|
||||
test('CourseGrade filters', () => {
|
||||
expect(screen.getByRole('button', { name: 'Overall Grade' })).toBeInTheDocument();
|
||||
expect(el.find(Collapsible).at(1).children()).toMatchObject(shallow(
|
||||
<CourseGradeFilter updateQueryParams={updateQueryParams} />,
|
||||
));
|
||||
});
|
||||
test('StudentGroups filters', () => {
|
||||
expect(screen.getByRole('button', { name: 'Student Groups' })).toBeInTheDocument();
|
||||
expect(el.find(Collapsible).at(2).children()).toMatchObject(shallow(
|
||||
<StudentGroupsFilter updateQueryParams={updateQueryParams} />,
|
||||
));
|
||||
});
|
||||
test('includeCourseTeamMembers', () => {
|
||||
expect(screen.getByRole('button', { name: 'Include Course Team Members' })).toBeInTheDocument();
|
||||
const checkbox = el.find(Collapsible).at(3).children();
|
||||
expect(checkbox.props()).toEqual({
|
||||
checked: true,
|
||||
onChange: hookProps.includeCourseTeamMembers.handleChange,
|
||||
children: formatMessage(messages.includeCourseTeamMembers),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
131
src/components/GradebookHeader/__snapshots__/index.test.jsx.snap
Normal file
131
src/components/GradebookHeader/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,131 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradebookHeader component render default view shapshot 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component render frozen grades snapshot: show frozen warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component render show bulk management snapshot: show toggle view message button with handleToggleViewClick method 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction hooks.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
toggle-view-message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component render user cannot view gradebook snapshot: show unauthorized warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="test-dashboard-url"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<h1>
|
||||
Gradebook
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
You are not authorized to view the gradebook for this course.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
261
src/components/GradebookHeader/__snapshots__/test.jsx.snap
Normal file
261
src/components/GradebookHeader/__snapshots__/test.jsx.snap
Normal 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"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots grades frozen, can view. grades frozen warning but no unauthorized warning 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
|
||||
description="Warning message in Gradebook Header for frozen messages"
|
||||
id="gradebook.GradebookHeader.frozenWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots grades frozen, cannot view unauthorized warning, and grades frozen warning. 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
|
||||
description="Warning message in Gradebook Header for frozen messages"
|
||||
id="gradebook.GradebookHeader.frozenWarning"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots show bulk management, active view is bulkManagementHistory view toggle view button to grades 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Return to Gradebook"
|
||||
description="Button text for button navigating to Grades view."
|
||||
id="gradebook.GradebookHeader.toGradesView"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GradebookHeader component snapshots show bulk management, active view is grades view toggle view button to activity log 1`] = `
|
||||
<div
|
||||
className="gradebook-header"
|
||||
>
|
||||
<a
|
||||
className="mb-3"
|
||||
href="http://localhost:18000/courses/fakeID/instructor"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<<
|
||||
</span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to Dashboard"
|
||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
||||
id="gradebook.GradebookHeader.backButton"
|
||||
/>
|
||||
</a>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Gradebook"
|
||||
description="Top-level app title in Gradebook Header component"
|
||||
id="gradebook.GradebookHeader.appLabel"
|
||||
/>
|
||||
</h1>
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
fakeID
|
||||
</h2>
|
||||
<Button
|
||||
onClick={[MockFunction this.handleToggleViewClick]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View Bulk Management History"
|
||||
description="Button text for button navigating to Bulk Managment Activity Log"
|
||||
id="gradebook.GradebookHeader.toActivityLogButton"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -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)}
|
||||
|
||||
@@ -1,300 +1,77 @@
|
||||
import React from 'react';
|
||||
import { render, screen, initializeMocks } from 'testUtilsExtra';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { instructorDashboardUrl } from 'data/services/lms/urls';
|
||||
|
||||
import { GradebookHeader } from './index';
|
||||
import useGradebookHeaderData from './hooks';
|
||||
import messages from './messages';
|
||||
import GradebookHeader from '.';
|
||||
|
||||
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
instructorDashboardUrl: jest.fn(),
|
||||
}));
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
initializeMocks();
|
||||
instructorDashboardUrl.mockReturnValue('test-dashboard-url');
|
||||
|
||||
describe('GradebookHeader', () => {
|
||||
const mockHandleToggleViewClick = jest.fn();
|
||||
const hookProps = {
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'test-course-id',
|
||||
handleToggleViewClick: jest.fn().mockName('hooks.handleToggleViewClick'),
|
||||
showBulkManagement: false,
|
||||
toggleViewMessage: { defaultMessage: 'toggle-view-message' },
|
||||
};
|
||||
useGradebookHeaderData.mockReturnValue(hookProps);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
instructorDashboardUrl.mockReturnValue('https://example.com/dashboard');
|
||||
let el;
|
||||
describe('GradebookHeader component', () => {
|
||||
beforeAll(() => {
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
|
||||
describe('basic rendering', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValue({
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'course-v1:TestU+CS101+2024',
|
||||
handleToggleViewClick: mockHandleToggleViewClick,
|
||||
showBulkManagement: false,
|
||||
toggleViewMessage: messages.toActivityLog,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the main header container', () => {
|
||||
render(<GradebookHeader />);
|
||||
const header = screen.getByText('Gradebook').closest('.gradebook-header');
|
||||
expect(header).toHaveClass('gradebook-header');
|
||||
});
|
||||
|
||||
it('renders back to dashboard link', () => {
|
||||
render(<GradebookHeader />);
|
||||
const dashboardLink = screen.getByRole('link');
|
||||
expect(dashboardLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://example.com/dashboard',
|
||||
);
|
||||
expect(dashboardLink).toHaveClass('mb-3');
|
||||
expect(dashboardLink).toHaveTextContent('Back to Dashboard');
|
||||
});
|
||||
|
||||
it('renders gradebook title', () => {
|
||||
render(<GradebookHeader />);
|
||||
const title = screen.getByRole('heading', { level: 1 });
|
||||
expect(title).toHaveTextContent('Gradebook');
|
||||
});
|
||||
|
||||
it('renders course ID subtitle', () => {
|
||||
render(<GradebookHeader />);
|
||||
const subtitle = screen.getByRole('heading', { level: 2 });
|
||||
expect(subtitle).toHaveTextContent('course-v1:TestU+CS101+2024');
|
||||
expect(subtitle).toHaveClass('text-break');
|
||||
});
|
||||
|
||||
it('renders subtitle row with correct classes', () => {
|
||||
render(<GradebookHeader />);
|
||||
const subtitleRow = screen.getByRole('heading', {
|
||||
level: 2,
|
||||
}).parentElement;
|
||||
expect(subtitleRow).toHaveClass(
|
||||
'subtitle-row',
|
||||
'd-flex',
|
||||
'justify-content-between',
|
||||
'align-items-center',
|
||||
);
|
||||
});
|
||||
|
||||
it('calls instructorDashboardUrl to get dashboard URL', () => {
|
||||
render(<GradebookHeader />);
|
||||
expect(instructorDashboardUrl).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls useGradebookHeaderData hook', () => {
|
||||
render(<GradebookHeader />);
|
||||
expect(useGradebookHeaderData).toHaveBeenCalled();
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
expect(useGradebookHeaderData).toHaveBeenCalledWith();
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulk management toggle button', () => {
|
||||
describe('when showBulkManagement is true', () => {
|
||||
describe('render', () => {
|
||||
describe('default view', () => {
|
||||
test('shapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('show bulk management', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValue({
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'course-v1:TestU+CS101+2024',
|
||||
handleToggleViewClick: mockHandleToggleViewClick,
|
||||
showBulkManagement: true,
|
||||
toggleViewMessage: messages.toActivityLog,
|
||||
});
|
||||
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true });
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
|
||||
it('renders toggle view button', () => {
|
||||
render(<GradebookHeader />);
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays correct button text from toggleViewMessage', () => {
|
||||
render(<GradebookHeader />);
|
||||
const toggleButton = screen.getByRole('button');
|
||||
expect(toggleButton).toHaveTextContent('View Bulk Management History');
|
||||
});
|
||||
|
||||
it('calls handleToggleViewClick when button is clicked', async () => {
|
||||
render(<GradebookHeader />);
|
||||
const user = userEvent.setup();
|
||||
const toggleButton = screen.getByRole('button');
|
||||
|
||||
await user.click(toggleButton);
|
||||
expect(mockHandleToggleViewClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('displays correct message from toggleViewMessage', () => {
|
||||
useGradebookHeaderData.mockReturnValue({
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'course-v1:TestU+CS101+2024',
|
||||
handleToggleViewClick: mockHandleToggleViewClick,
|
||||
showBulkManagement: true,
|
||||
toggleViewMessage: messages.toGradesView,
|
||||
});
|
||||
|
||||
render(<GradebookHeader />);
|
||||
const toggleButton = screen.getByRole('button');
|
||||
expect(toggleButton).toHaveTextContent('Return to Gradebook');
|
||||
test('snapshot: show toggle view message button with handleToggleViewClick method', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
const { onClick, children } = el.find(Button).props();
|
||||
expect(onClick).toEqual(hookProps.handleToggleViewClick);
|
||||
expect(children).toEqual(formatMessage(hookProps.toggleViewMessage));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when showBulkManagement is false', () => {
|
||||
describe('frozen grades', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValue({
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'course-v1:TestU+CS101+2024',
|
||||
handleToggleViewClick: mockHandleToggleViewClick,
|
||||
showBulkManagement: false,
|
||||
toggleViewMessage: messages.toActivityLog,
|
||||
});
|
||||
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true });
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
|
||||
it('does not render toggle view button', () => {
|
||||
render(<GradebookHeader />);
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
test('snapshot: show frozen warning', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('frozen grades warning', () => {
|
||||
describe('when areGradesFrozen is true', () => {
|
||||
describe('user cannot view gradebook', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValue({
|
||||
areGradesFrozen: true,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'course-v1:TestU+CS101+2024',
|
||||
handleToggleViewClick: mockHandleToggleViewClick,
|
||||
showBulkManagement: false,
|
||||
toggleViewMessage: messages.toActivityLog,
|
||||
});
|
||||
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false });
|
||||
el = shallow(<GradebookHeader />);
|
||||
});
|
||||
|
||||
it('renders frozen warning alert', () => {
|
||||
render(<GradebookHeader />);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveClass('alert', 'alert-warning');
|
||||
expect(alert).toHaveTextContent(
|
||||
'The grades for this course are now frozen. Editing of grades is no longer allowed.',
|
||||
);
|
||||
test('snapshot: show unauthorized warning', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when areGradesFrozen is false', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValue({
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'course-v1:TestU+CS101+2024',
|
||||
handleToggleViewClick: mockHandleToggleViewClick,
|
||||
showBulkManagement: false,
|
||||
toggleViewMessage: messages.toActivityLog,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render frozen warning alert', () => {
|
||||
render(<GradebookHeader />);
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'The grades for this course are now frozen. Editing of grades is no longer allowed.',
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unauthorized warning', () => {
|
||||
describe('when canUserViewGradebook is false', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValue({
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: false,
|
||||
courseId: 'course-v1:TestU+CS101+2024',
|
||||
handleToggleViewClick: mockHandleToggleViewClick,
|
||||
showBulkManagement: false,
|
||||
toggleViewMessage: messages.toActivityLog,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders unauthorized warning alert', () => {
|
||||
render(<GradebookHeader />);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveClass('alert', 'alert-warning');
|
||||
expect(alert).toHaveTextContent(
|
||||
'You are not authorized to view the gradebook for this course.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when canUserViewGradebook is true', () => {
|
||||
beforeEach(() => {
|
||||
useGradebookHeaderData.mockReturnValue({
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'course-v1:TestU+CS101+2024',
|
||||
handleToggleViewClick: mockHandleToggleViewClick,
|
||||
showBulkManagement: false,
|
||||
toggleViewMessage: messages.toActivityLog,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render unauthorized warning alert', () => {
|
||||
render(<GradebookHeader />);
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'You are not authorized to view the gradebook for this course.',
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple warnings', () => {
|
||||
it('renders both frozen and unauthorized warnings when both conditions are true', () => {
|
||||
useGradebookHeaderData.mockReturnValue({
|
||||
areGradesFrozen: true,
|
||||
canUserViewGradebook: false,
|
||||
courseId: 'course-v1:TestU+CS101+2024',
|
||||
handleToggleViewClick: mockHandleToggleViewClick,
|
||||
showBulkManagement: false,
|
||||
toggleViewMessage: messages.toActivityLog,
|
||||
});
|
||||
|
||||
render(<GradebookHeader />);
|
||||
const alerts = screen.getAllByRole('alert');
|
||||
expect(alerts).toHaveLength(2);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'The grades for this course are now frozen. Editing of grades is no longer allowed.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'You are not authorized to view the gradebook for this course.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('complete integration', () => {
|
||||
it('renders all elements when showBulkManagement is true', () => {
|
||||
useGradebookHeaderData.mockReturnValue({
|
||||
areGradesFrozen: false,
|
||||
canUserViewGradebook: true,
|
||||
courseId: 'course-v1:TestU+CS101+2024',
|
||||
handleToggleViewClick: mockHandleToggleViewClick,
|
||||
showBulkManagement: true,
|
||||
toggleViewMessage: messages.toActivityLog,
|
||||
});
|
||||
|
||||
render(<GradebookHeader />);
|
||||
|
||||
expect(screen.getByRole('link')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = `
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<NetworkButton
|
||||
label={
|
||||
Object {
|
||||
"defaultMessage": "Download Grades",
|
||||
"description": "A labeled button that allows an admin user to download course grades all at once (in bulk).",
|
||||
"id": "gradebook.GradesView.BulkManagementControls.bulkManagementLabel",
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
<ImportGradesButton />
|
||||
</div>
|
||||
`;
|
||||
@@ -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();
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -18,8 +18,7 @@ export const BulkManagementControls = () => {
|
||||
handleClickExportGrades,
|
||||
} = useBulkManagementControlsData();
|
||||
|
||||
if (!show) { return null; }
|
||||
return (
|
||||
return show && (
|
||||
<div className="d-flex">
|
||||
<NetworkButton
|
||||
label={messages.downloadGradesBtn}
|
||||
|
||||
@@ -1,160 +1,32 @@
|
||||
import React from 'react';
|
||||
import { render, screen, initializeMocks } from 'testUtilsExtra';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import NetworkButton from 'components/NetworkButton';
|
||||
import ImportGradesButton from '../ImportGradesButton';
|
||||
|
||||
import { BulkManagementControls } from './index';
|
||||
import useBulkManagementControlsData from './hooks';
|
||||
import messages from './messages';
|
||||
import BulkManagementControls from '.';
|
||||
|
||||
jest.mock('../ImportGradesButton', () => 'ImportGradesButton');
|
||||
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
||||
|
||||
jest.mock('components/NetworkButton', () => jest.fn(() => <div data-testid="network-button">NetworkButton</div>));
|
||||
jest.mock('../ImportGradesButton', () => jest.fn(() => (
|
||||
<div data-testid="import-grades-button">ImportGradesButton</div>
|
||||
)));
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
initializeMocks();
|
||||
const hookProps = {
|
||||
show: true,
|
||||
handleClickExportGrades: jest.fn(),
|
||||
};
|
||||
useBulkManagementControlsData.mockReturnValue(hookProps);
|
||||
|
||||
describe('BulkManagementControls', () => {
|
||||
const mockHandleClickExportGrades = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
describe('behavior', () => {
|
||||
shallow(<BulkManagementControls />);
|
||||
expect(useBulkManagementControlsData).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
describe('when show is false', () => {
|
||||
beforeEach(() => {
|
||||
useBulkManagementControlsData.mockReturnValue({
|
||||
show: false,
|
||||
handleClickExportGrades: mockHandleClickExportGrades,
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot - show - network and import buttons', () => {
|
||||
expect(shallow(<BulkManagementControls />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders nothing when show is false', () => {
|
||||
render(<BulkManagementControls />);
|
||||
expect(screen.queryByTestId('network-button')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('import-grades-button'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render NetworkButton when show is false', () => {
|
||||
render(<BulkManagementControls />);
|
||||
expect(NetworkButton).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not render ImportGradesButton when show is false', () => {
|
||||
render(<BulkManagementControls />);
|
||||
expect(ImportGradesButton).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when show is true', () => {
|
||||
beforeEach(() => {
|
||||
useBulkManagementControlsData.mockReturnValue({
|
||||
show: true,
|
||||
handleClickExportGrades: mockHandleClickExportGrades,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the container div with correct class when show is true', () => {
|
||||
render(<BulkManagementControls />);
|
||||
const containerDiv = screen.getByTestId('network-button').parentElement;
|
||||
expect(containerDiv).toHaveClass('d-flex');
|
||||
});
|
||||
|
||||
it('renders NetworkButton with correct props', () => {
|
||||
render(<BulkManagementControls />);
|
||||
|
||||
expect(NetworkButton).toHaveBeenCalledWith(
|
||||
{
|
||||
label: messages.downloadGradesBtn,
|
||||
onClick: mockHandleClickExportGrades,
|
||||
},
|
||||
{},
|
||||
);
|
||||
expect(screen.getByTestId('network-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders ImportGradesButton', () => {
|
||||
render(<BulkManagementControls />);
|
||||
|
||||
expect(ImportGradesButton).toHaveBeenCalledWith({}, {});
|
||||
expect(screen.getByTestId('import-grades-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls handleClickExportGrades when NetworkButton is clicked', () => {
|
||||
render(<BulkManagementControls />);
|
||||
|
||||
const networkButtonCall = NetworkButton.mock.calls[0][0];
|
||||
const { onClick } = networkButtonCall;
|
||||
|
||||
onClick();
|
||||
expect(mockHandleClickExportGrades).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('passes correct label to NetworkButton', () => {
|
||||
render(<BulkManagementControls />);
|
||||
|
||||
const networkButtonCall = NetworkButton.mock.calls[0][0];
|
||||
expect(networkButtonCall.label).toBe(messages.downloadGradesBtn);
|
||||
});
|
||||
|
||||
it('renders both buttons in the correct order', () => {
|
||||
render(<BulkManagementControls />);
|
||||
|
||||
expect(NetworkButton).toHaveBeenCalled();
|
||||
expect(ImportGradesButton).toHaveBeenCalled();
|
||||
|
||||
const networkButton = screen.getByTestId('network-button');
|
||||
const importButton = screen.getByTestId('import-grades-button');
|
||||
|
||||
expect(networkButton).toBeInTheDocument();
|
||||
expect(importButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hook integration', () => {
|
||||
it('calls useBulkManagementControlsData hook', () => {
|
||||
useBulkManagementControlsData.mockReturnValue({
|
||||
show: true,
|
||||
handleClickExportGrades: mockHandleClickExportGrades,
|
||||
});
|
||||
|
||||
render(<BulkManagementControls />);
|
||||
expect(useBulkManagementControlsData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('uses the show value from hook to determine rendering', () => {
|
||||
useBulkManagementControlsData.mockReturnValue({
|
||||
show: false,
|
||||
handleClickExportGrades: mockHandleClickExportGrades,
|
||||
});
|
||||
|
||||
render(<BulkManagementControls />);
|
||||
expect(screen.queryByTestId('network-button')).not.toBeInTheDocument();
|
||||
|
||||
useBulkManagementControlsData.mockReturnValue({
|
||||
show: true,
|
||||
handleClickExportGrades: mockHandleClickExportGrades,
|
||||
});
|
||||
|
||||
render(<BulkManagementControls />);
|
||||
expect(screen.getByTestId('network-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('passes handleClickExportGrades from hook to NetworkButton', () => {
|
||||
const customHandler = jest.fn();
|
||||
useBulkManagementControlsData.mockReturnValue({
|
||||
show: true,
|
||||
handleClickExportGrades: customHandler,
|
||||
});
|
||||
|
||||
render(<BulkManagementControls />);
|
||||
|
||||
const networkButtonCall = NetworkButton.mock.calls[0][0];
|
||||
expect(networkButtonCall.onClick).toBe(customHandler);
|
||||
test('snapshot - empty if show is not truthy', () => {
|
||||
useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false });
|
||||
expect(shallow(<BulkManagementControls />).isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,100 +1,17 @@
|
||||
import React from 'react';
|
||||
import { render, screen, initializeMocks } from 'testUtilsExtra';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import HistoryHeader from './HistoryHeader';
|
||||
|
||||
initializeMocks();
|
||||
|
||||
describe('HistoryHeader', () => {
|
||||
const defaultProps = {
|
||||
id: 'test-id',
|
||||
label: 'Test Label',
|
||||
value: 'Test Value',
|
||||
const props = {
|
||||
id: 'water',
|
||||
label: 'Brita',
|
||||
value: 'hydration',
|
||||
};
|
||||
|
||||
it('renders header with label and value', () => {
|
||||
render(<HistoryHeader {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Test Label:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders header element with correct classes', () => {
|
||||
render(<HistoryHeader {...defaultProps} />);
|
||||
|
||||
const headerElement = screen.getByText('Test Label:');
|
||||
expect(headerElement).toHaveClass('grade-history-header');
|
||||
expect(headerElement).toHaveClass('grade-history-test-id');
|
||||
});
|
||||
|
||||
it('renders with string value', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
value: 'String Value',
|
||||
};
|
||||
|
||||
render(<HistoryHeader {...props} />);
|
||||
expect(screen.getByText('String Value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with number value', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
value: 85,
|
||||
};
|
||||
|
||||
render(<HistoryHeader {...props} />);
|
||||
expect(screen.getByText('85')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with null value (default prop)', () => {
|
||||
const props = {
|
||||
id: 'test-id',
|
||||
label: 'Test Label',
|
||||
};
|
||||
|
||||
render(<HistoryHeader {...props} />);
|
||||
expect(screen.getByText('Test Label:')).toBeInTheDocument();
|
||||
|
||||
const valueDiv = screen.getByText('Test Label:').nextSibling;
|
||||
expect(valueDiv).toBeInTheDocument();
|
||||
expect(valueDiv).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders with React node as label', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
label: <strong>Bold Label</strong>,
|
||||
};
|
||||
|
||||
render(<HistoryHeader {...props} />);
|
||||
const strongElement = screen.getByText('Bold Label');
|
||||
expect(strongElement.tagName).toBe('STRONG');
|
||||
});
|
||||
|
||||
it('generates correct class name based on id', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
id: 'assignment-name',
|
||||
};
|
||||
|
||||
render(<HistoryHeader {...props} />);
|
||||
const headerElement = screen.getByText('Test Label:');
|
||||
expect(headerElement).toHaveClass('grade-history-assignment-name');
|
||||
});
|
||||
|
||||
it('renders container structure correctly', () => {
|
||||
render(<HistoryHeader {...defaultProps} />);
|
||||
|
||||
const headerElement = screen.getByText('Test Label:');
|
||||
const valueElement = screen.getByText('Test Value');
|
||||
|
||||
expect(headerElement).toBeInTheDocument();
|
||||
expect(valueElement).toBeInTheDocument();
|
||||
|
||||
expect(headerElement).toHaveClass(
|
||||
'grade-history-header',
|
||||
'grade-history-test-id',
|
||||
);
|
||||
describe('Component', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<HistoryHeader {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,45 +1,94 @@
|
||||
import React from 'react';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { render, screen, initializeMocks } from 'testUtilsExtra';
|
||||
import ModalHeaders from './ModalHeaders';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
app: { useModalData: jest.fn() },
|
||||
grades: { useGradeData: jest.fn() },
|
||||
import {
|
||||
ModalHeaders,
|
||||
mapStateToProps,
|
||||
} from './ModalHeaders';
|
||||
|
||||
jest.mock('./HistoryHeader', () => 'HistoryHeader');
|
||||
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
editUpdateData: jest.fn(state => ({ editUpdateData: state })),
|
||||
modalState: {
|
||||
assignmentName: jest.fn(state => ({ assignmentName: state })),
|
||||
updateUserName: jest.fn(state => ({ updateUserName: state })),
|
||||
},
|
||||
},
|
||||
grades: {
|
||||
gradeOverrideCurrentEarnedGradedOverride: jest.fn(state => ({ currentGrade: state })),
|
||||
gradeOriginalEarnedGraded: jest.fn(state => ({ originalGrade: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
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);
|
||||
initializeMocks();
|
||||
|
||||
describe('ModalHeaders', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
render(<ModalHeaders />);
|
||||
let el;
|
||||
const props = {
|
||||
currentGrade: 2,
|
||||
originalGrade: 20,
|
||||
modalState: {
|
||||
assignmentName: 'Qwerty',
|
||||
updateUserName: 'Uiop',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Component', () => {
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
});
|
||||
describe('gradeOverrideHistoryError is and empty and open is true', () => {
|
||||
test('modal open and StatusAlert showing', () => {
|
||||
el = shallow(<ModalHeaders {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('gradeOverrideHistoryError is empty and open is false', () => {
|
||||
test('modal closed and StatusAlert closed', () => {
|
||||
el = shallow(
|
||||
<ModalHeaders {...props} open={false} gradeOverrideHistoryError="" />,
|
||||
);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('assignment header', () => {
|
||||
expect(screen.getByText(modalData.assignmentName)).toBeInTheDocument();
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { he: 'lives in a', pineapple: 'under the sea' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('student header', () => {
|
||||
expect(screen.getByText(modalData.updateUserName)).toBeInTheDocument();
|
||||
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('originalGrade header', () => {
|
||||
expect(screen.getByText(gradeData.gradeOriginalEarnedGraded)).toBeInTheDocument();
|
||||
describe('originalGrade', () => {
|
||||
test('from grades.gradeOverrideCurrentEarnedGradedOverride', () => {
|
||||
expect(mapped.currentGrade).toEqual(
|
||||
selectors.grades.gradeOverrideCurrentEarnedGradedOverride(testState),
|
||||
);
|
||||
});
|
||||
});
|
||||
test('currentGrade header', () => {
|
||||
expect(screen.getByText(gradeData.gradeOverrideCurrentEarnedGradedOverride)).toBeInTheDocument();
|
||||
describe('originalGrade', () => {
|
||||
test('from grades.gradeOriginalEarnedGrades', () => {
|
||||
expect(mapped.originalGrade).toEqual(
|
||||
selectors.grades.gradeOriginalEarnedGraded(testState),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
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);
|
||||
|
||||
describe('AdjustedGradeInput component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
render(<AdjustedGradeInput />);
|
||||
});
|
||||
describe('render', () => {
|
||||
test('renders input with correct props', () => {
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(hookProps.value);
|
||||
expect(screen.getByText(hookProps.hintText)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -1,70 +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(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useRef: jest.fn((val) => ({ current: val, useRef: true })),
|
||||
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
|
||||
}));
|
||||
|
||||
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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -1,31 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import useReasonInputData from './hooks';
|
||||
import ReasonInput from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
ref: jest.fn().mockName('hook.ref'),
|
||||
onChange: jest.fn().mockName('hook.onChange'),
|
||||
value: 'test-value',
|
||||
};
|
||||
useReasonInputData.mockReturnValue(hookProps);
|
||||
|
||||
describe('ReasonInput component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
render(<ReasonInput />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hook data', () => {
|
||||
expect(useReasonInputData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('renders', () => {
|
||||
it('input correctly', () => {
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox')).toHaveValue(hookProps.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import useReasonInputData from './hooks';
|
||||
import ReasonInput, { controlTestId } from '.';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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"
|
||||
/>
|
||||
`;
|
||||
@@ -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}
|
||||
/>
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
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(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useContext: jest.fn(context => context),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: jest.fn(() => ({
|
||||
formatMessage: (message) => message.defaultMessage,
|
||||
})),
|
||||
}));
|
||||
|
||||
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(messages.dateHeader.defaultMessage);
|
||||
expect(accessor).toEqual(columns.date);
|
||||
});
|
||||
test('grader column', () => {
|
||||
const { Header, accessor } = out.columns[1];
|
||||
expect(Header).toEqual(messages.graderHeader.defaultMessage);
|
||||
expect(accessor).toEqual(columns.grader);
|
||||
});
|
||||
test('reason column', () => {
|
||||
const { Header, accessor } = out.columns[2];
|
||||
expect(Header).toEqual(messages.reasonHeader.defaultMessage);
|
||||
expect(accessor).toEqual(columns.reason);
|
||||
});
|
||||
test('adjustedGrade column', () => {
|
||||
const { Header, accessor } = out.columns[3];
|
||||
expect(Header).toEqual(messages.adjustedGradeHeader.defaultMessage);
|
||||
expect(accessor).toEqual(columns.adjustedGrade);
|
||||
});
|
||||
});
|
||||
test('data passed from grade data', () => {
|
||||
expect(out.data).toEqual(gradeOverrides);
|
||||
});
|
||||
});
|
||||
describe('with errors', () => {
|
||||
it('returns hide true and no other fields', () => {
|
||||
selectors.grades.useHasOverrideErrors.mockReturnValue(true);
|
||||
out = useOverrideTableData();
|
||||
expect(out).toEqual({ hide: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,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);
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import useOverrideTableData from './hooks';
|
||||
import OverrideTable from '.';
|
||||
import { renderWithIntl } from '../../../../testUtilsExtra';
|
||||
|
||||
jest.mock('utils', () => ({
|
||||
...jest.requireActual('utils'),
|
||||
formatDateForDisplay: (date) => ({ formatted: date }),
|
||||
}));
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
hide: false,
|
||||
data: [
|
||||
{ filename: 'data' },
|
||||
{ resultsSummary: 'test-data' },
|
||||
],
|
||||
columns: [{
|
||||
Header: 'Gradebook',
|
||||
accessor: 'filename',
|
||||
},
|
||||
{
|
||||
Header: 'Download Summary',
|
||||
accessor: 'resultsSummary',
|
||||
}],
|
||||
};
|
||||
|
||||
describe('OverrideTable component', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.clearAllMocks()
|
||||
.useFakeTimers('modern')
|
||||
.setSystemTime(new Date('2000-01-01').getTime());
|
||||
});
|
||||
describe('hooks', () => {
|
||||
it('initializes hook data', () => {
|
||||
useOverrideTableData.mockReturnValue(hookProps);
|
||||
renderWithIntl(<OverrideTable />);
|
||||
expect(useOverrideTableData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('null render if hide', () => {
|
||||
useOverrideTableData.mockReturnValue({ ...hookProps, hide: true });
|
||||
renderWithIntl(<OverrideTable />);
|
||||
expect(screen.queryByRole('table')).toBeNull();
|
||||
});
|
||||
it('renders table with correct data', () => {
|
||||
useOverrideTableData.mockReturnValue(hookProps);
|
||||
renderWithIntl(<OverrideTable />);
|
||||
const table = screen.getByRole('table');
|
||||
expect(table).toBeInTheDocument();
|
||||
expect(screen.getByText(hookProps.columns[0].Header)).toBeInTheDocument();
|
||||
expect(screen.getByText(hookProps.columns[1].Header)).toBeInTheDocument();
|
||||
expect(screen.getByText(hookProps.data[0].filename)).toBeInTheDocument();
|
||||
expect(screen.getByText(hookProps.data[1].resultsSummary)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
81
src/components/GradesView/EditModal/OverrideTable/test.jsx
Normal file
81
src/components/GradesView/EditModal/OverrideTable/test.jsx
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HistoryHeader Component snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="grade-history-header grade-history-water"
|
||||
>
|
||||
Brita
|
||||
:
|
||||
</div>
|
||||
<div>
|
||||
hydration
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user