Compare commits

..

64 Commits

Author SHA1 Message Date
edX requirements bot
ee3e4733ff chore: update browserslist DB (#561)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2026-03-16 00:43:39 +00:00
edX requirements bot
c442df7c6d chore: update browserslist DB (#560)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2026-03-09 00:38:37 +00:00
edX requirements bot
b7f387c45e chore: update browserslist DB (#558)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2026-03-02 00:45:21 +00:00
edX requirements bot
966d7dc849 chore: update browserslist DB (#554)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2026-02-23 19:21:23 +00:00
edX requirements bot
606d5ce631 chore: update browserslist DB (#551)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2026-02-16 00:42:04 +00:00
Brian Smith
f48db33355 fix(deps): regenerate package-lock.json (#547)
* fix(deps): regenerate package-lock.json

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(tests): update toast test for Paragon v23 changes

Paragon removed the redundant "alert" role from the toast container in
openedx/paragon#3434 (landed via openedx/paragon#3567). Updated test to
find the container using document.getElementById instead.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-02-13 17:26:35 -05:00
edX requirements bot
8f9c5f4247 chore: update browserslist DB (#549)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2026-02-09 00:39:16 +00:00
edX requirements bot
4759819071 chore: update browserslist DB (#543)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2026-01-26 00:34:22 +00:00
dependabot[bot]
f89963a0e1 build(deps): bump tmp, patch-package and inquirer
Bumps [tmp](https://github.com/raszi/node-tmp), [patch-package](https://github.com/ds300/patch-package) and [inquirer](https://github.com/SBoudrias/Inquirer.js). These dependencies needed to be updated together.

Updates `tmp` from 0.0.29 to 0.2.5
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.0.29...v0.2.5)

Updates `patch-package` from 8.0.0 to 8.0.1
- [Release notes](https://github.com/ds300/patch-package/releases)
- [Changelog](https://github.com/ds300/patch-package/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ds300/patch-package/commits)

Updates `inquirer` from 8.2.6 to 8.2.7
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/inquirer@8.2.6...inquirer@8.2.7)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.5
  dependency-type: indirect
- dependency-name: patch-package
  dependency-version: 8.0.1
  dependency-type: indirect
- dependency-name: inquirer
  dependency-version: 8.2.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 19:39:33 +05:30
dependabot[bot]
6daa3b3cec build(deps): bump form-data from 4.0.2 to 4.0.4
---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 19:38:37 +05:30
dependabot[bot]
8cb2fc18ff build(deps): bump brace-expansion
Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 1.1.11 to 1.1.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

Updates `brace-expansion` from 2.0.1 to 2.0.2
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 2.0.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 19:36:49 +05:30
dependabot[bot]
65d82ece23 build(deps): bump js-toml from 1.0.1 to 1.0.2
Bumps [js-toml](https://github.com/sunnyadn/js-toml) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/sunnyadn/js-toml/releases)
- [Commits](https://github.com/sunnyadn/js-toml/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: js-toml
  dependency-version: 1.0.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 19:29:53 +05:30
dependabot[bot]
5087bbb0e9 build(deps): bump js-yaml
Bumps  and [js-yaml](https://github.com/nodeca/js-yaml). These dependencies needed to be updated together.

Updates `js-yaml` from 4.1.0 to 4.1.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

Updates `js-yaml` from 3.14.1 to 3.14.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 19:28:35 +05:30
Anton Melser
81aa35edd6 docs: convert readme from .md to .rst
With a few minor corrections.

Signed-off-by: Anton Melser <anton.melser@outlook.com>
2026-01-20 19:23:44 +05:30
edX requirements bot
45e81aff46 chore: update browserslist DB (#540)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2026-01-19 00:33:35 +00:00
edX requirements bot
1e1ccda82e chore: update browserslist DB (#539)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2026-01-12 00:32:46 +00:00
edX requirements bot
2af3703d68 chore: update browserslist DB (#537)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2026-01-05 00:33:52 +00:00
edX requirements bot
17fa6e53b5 chore: update browserslist DB (#534)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-12-22 00:32:12 +00:00
edX requirements bot
1c886d9c21 chore: update browserslist DB (#533)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-12-15 00:32:25 +00:00
edX requirements bot
1e1250f93a chore: update browserslist DB (#530)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-12-08 00:30:36 +00:00
edX requirements bot
cae75368bf chore: update browserslist DB (#528)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-12-01 00:35:28 +00:00
edX requirements bot
ea2f1f9f6f chore: update browserslist DB (#525)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-11-24 00:31:09 +00:00
edX requirements bot
a4f63de389 chore: update browserslist DB (#523)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-11-17 00:29:35 +00:00
edX requirements bot
6e1688d627 chore: update browserslist DB (#522)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-11-10 00:30:20 +00:00
edX requirements bot
e759221e5f chore: update browserslist DB (#520)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-11-03 00:30:18 +00:00
edX requirements bot
02a3bda10a chore: update browserslist DB (#518)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-10-20 00:30:49 +00:00
edX requirements bot
aa4ddfa977 chore: update browserslist DB (#515)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-10-13 00:29:17 +00:00
edX requirements bot
2b88ef3144 chore: update browserslist DB (#514)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-10-06 00:27:49 +00:00
Feanil Patel
cd6cb71eb5 Merge pull request #513 from openedx/feanil/node24
build: Switch to Node 24 for building and testing.
2025-10-02 13:52:26 -04:00
Feanil Patel
c6d72bcf47 build: Switch to Node 24 for building and testing. 2025-10-02 11:24:12 -04:00
edX requirements bot
a0bdd0c012 chore: update browserslist DB (#512)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-09-29 00:28:42 +00:00
Feanil Patel
73e1421a90 Merge pull request #511 from openedx/feanil/remove-reactifex-packages
build: remove unused reactifex packages
2025-09-26 13:06:13 -04:00
Feanil Patel
d38ec004cb build: remove unused reactifex packages
Remove reactifex and/or @edx/reactifex packages from devDependencies
as they are no longer needed. Translation extraction functionality has
been verified to work correctly without these dependencies.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 10:40:12 -04:00
edX requirements bot
03d4d403b7 chore: update browserslist DB (#510)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-09-22 00:29:47 +00:00
Muhammad Anas
95a0cafac4 chore: upgrade frontend-component-header to v6.6.x (#509)
* chore: upgrade frontend-component-header to v6.6.x

* chore: bump version
2025-09-18 16:00:28 -04:00
Diana Villalvazo
4a221c9caa refactor: mocks cleanup and intl refactor (#508) 2025-09-08 14:57:00 -04:00
Victor Navarro
40d7167744 test: Deprecate react-unit-test-utils 7/9 (#491)
* test: Deprecate react-unit-test-utils 7/9

* test: coverage on app.test.jsx

* test: improve test cases for toast

* test: improve coverage on gradebook table

* test: missing snap/shallow depr

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
2025-09-08 10:08:56 -04:00
edX requirements bot
a2a3af4ea3 chore: update browserslist DB (#506)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-09-08 00:28:20 +00:00
Samuel Allan
5aacd38010 fix: update frontend-build to fix install issues (#504)
Earlier versions of @openedx/frontend-build used on older version of
'sharp', which caused intermittent installation issues. The version of
'sharp' was updated in @openedx/frontend-build to fix these issues, so
the frontend-build version can be updated here, to fix the issues in
this project too. See
https://github.com/openedx/frontend-build/issues/664 and
https://github.com/openedx/frontend-build/pull/665 for more information.

The frontend-build dependency was updated by:

```
npm install --package-lock-only @openedx/frontend-build
```

Private-ref: https://tasks.opencraft.com/browse/BB-9953
2025-09-05 11:56:41 -06:00
Diana Villalvazo
a5aad38cff test: Deprecate react-unit-test-utils 6/9 (#494)
* test: deprecate shallow

* test: remove debug
2025-09-03 14:32:35 -04:00
Victor Navarro
2456251790 test: Deprecate react-unit-test-utils 8/9 (#492)
* test: Deprecate react-unit-test-utils 8/9

* test: address comments

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
2025-09-02 10:01:09 -04:00
edX requirements bot
34a657d212 chore: update browserslist DB (#503)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-09-01 00:33:01 +00:00
Victor Navarro
99573f1d93 test: Deprecate react-unit-test-utils 5/9 (#490)
* test: deprecate react-unit-test-utils 5/9

* fix: lint issues

* test: change fireEvent for userEvent

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
2025-08-28 11:05:42 -04:00
Diana Villalvazo
1f16468bee feat: Deprecate react-unit-test-utils 9/9 (#489)
* feat: deprecate shallow on unit tests

* test: change fireEvent for userEvent
2025-08-27 15:46:14 -04:00
Diana Villalvazo
0225daf3d2 feat: Deprecate react-unit-test-utils 3/9 (#487)
* feat: deprecte shallow on unit tests

* test: improve coverage and remove extra word space

* test: remove debug
2025-08-27 11:06:04 -04:00
Diana Villalvazo
86ede70c41 feat: Deprecate react-unit-test-utils 4/9 (#488)
* feat: deprecte shallow on unit tests

* test: use userEvent instead of fireEvent
2025-08-26 12:58:08 -04:00
edX requirements bot
afd688d198 chore: update browserslist DB (#502)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-08-25 00:28:48 +00:00
edX requirements bot
8a82b60b22 chore: update browserslist DB (#501)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-08-18 00:31:28 +00:00
Javier Ontiveros
b608be06fe feat: Shallow test deprecation part 2/9 (#483)
* feat: deprecated shallow in tests

* chore: test utils from dev to dependencies

* chore: removed unused imports

* chore: restore packages to devDep

* chore: renamed header to pass lint

* feat: deprecated shallow in tests and added testing Utils

* chore: removed deprecated tests

* chore: commit to trigger test again

* chore: fix import warning for new testing file

* chore: address comments
2025-08-13 14:43:13 -04:00
edX requirements bot
00017e3be1 chore: update browserslist DB (#498)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-08-11 00:31:40 +00:00
Javier Ontiveros
25f686a875 feat: deprecate shallow in tests part 1/9 (#481)
* feat: deprecated shallow in tests

* chore: test utils from dev to dependencies

* chore: removed unused imports

* chore: restore packages to devDep

* chore: renamed header to pass lint
2025-08-07 18:12:39 -04:00
edX requirements bot
b88969c9bb chore: update browserslist DB (#495)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-08-04 00:47:46 +00:00
Kyle McCormick
427907fba2 chore: Delete CODEOWNERS (#493)
See: https://github.com/openedx/axim-engineering/issues/1511
2025-07-31 16:08:22 -04:00
edX requirements bot
a9608149db chore: update browserslist DB (#482)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-07-07 00:31:01 +00:00
dependabot[bot]
a4d1fb28aa build(deps): bump dawidd6/action-send-mail from 4 to 6
Bumps [dawidd6/action-send-mail](https://github.com/dawidd6/action-send-mail) from 4 to 6.
- [Release notes](https://github.com/dawidd6/action-send-mail/releases)
- [Commits](https://github.com/dawidd6/action-send-mail/compare/v4...v6)

---
updated-dependencies:
- dependency-name: dawidd6/action-send-mail
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-06 18:28:44 +05:30
edX requirements bot
42dbbee796 chore: update browserslist DB (#480)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-06-30 00:30:32 +00:00
Brian Smith
f0b6fc291e feat!: add design tokens support (#470)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 15:52:58 -04:00
edX requirements bot
a9cceb1ef9 chore: update browserslist DB (#466)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-06-16 00:29:40 +00:00
edX requirements bot
9921542f7e chore: update browserslist DB (#464)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-06-09 00:30:06 +00:00
edX requirements bot
c31185acfd chore: update browserslist DB (#459)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-06-02 00:29:30 +00:00
Brian Smith
d5cdbb8047 fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#457) 2025-05-19 11:02:24 -04:00
edX requirements bot
941f27a2f4 chore: update browserslist DB (#456)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-05-19 00:29:17 +00:00
edX requirements bot
5753412ede chore: update browserslist DB (#455)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-05-12 00:29:17 +00:00
edX requirements bot
790de20613 chore: update browserslist DB (#454)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-05-05 00:29:17 +00:00
116 changed files with 8123 additions and 6355 deletions

2
.env
View File

@@ -34,3 +34,5 @@ ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
APP_ID=''
MFE_CONFIG_API_URL=''
DISPLAY_FEEDBACK_WIDGET='true'
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -40,3 +40,5 @@ ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
APP_ID=''
MFE_CONFIG_API_URL=''
DISPLAY_FEEDBACK_WIDGET='false'
# Fallback in local style files
PARAGON_THEME_URLS={}

6
.github/CODEOWNERS vendored
View File

@@ -1,6 +0,0 @@
# 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

View File

@@ -47,7 +47,7 @@ jobs:
- name: Send failure notification
if: ${{ failure() }}
uses: dawidd6/action-send-mail@v4
uses: dawidd6/action-send-mail@v6
with:
server_address: email-smtp.us-east-1.amazonaws.com
server_port: 465

2
.nvmrc
View File

@@ -1 +1 @@
20
24

189
README.md
View File

@@ -1,189 +0,0 @@
# frontend-app-gradebook
[![Build Status](https://api.travis-ci.com/edx/frontend-app-gradebook.svg?branch=master)](https://travis-ci.com/edx/frontend-app-gradebook)
[![Codecov](https://img.shields.io/codecov/c/gh/openedx/frontend-app-gradebook)](https://app.codecov.io/gh/openedx/frontend-app-gradebook)
[![npm_version](https://img.shields.io/npm/v/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)
[![npm_downloads](https://img.shields.io/npm/dt/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)
[![license](https://img.shields.io/npm/l/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
# Purpose
Gradebook allows course staff to view, filter, and override subsection grades for a course. Additionally for Masters courses, Gradebook enables bulk management of subsection grades.
Jump to:
- [Should I use Gradebook in my course?](#should-i-use-gradebook-in-my-course)
- [Quickstart](#quickstart)
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)
## Should I use Gradebook in my course?
### What does this offer over the legacy gradebook?
The micro-frontend offers a great deal more granularity when searching for problems, an easy interface for editing grades, an
audit trail for seeing who edited what grade and what reason they gave (if any) for doing so.
UsageProblems can be filtered by student as in the traditional gradebook, but can also be filtered by scores to see who
scored within a certain range, and by assignment types (note: Not problem types, but categories like Exams or
Homework).
### What does the legacy gradebook offer that this project does not?
This project does not (yet, at least) create any graphs, which the traditional gradebook does. It also does not give
quick links to the problems for the instructor to visit. It expects the instructor to be familiar with the problems they
are grading and which unit they refer to.
The gradebook is expected to be much more performant for larger numbers of students as well. The Instructor Dashboard
link for the legacy gradebook reports that "this feature is available only to courses with a small number of enrolled
learners." However, this project comes with no such warning.
### Who should not change to this gradebook?
Groups whose instructors need not ever manually override grades do not need this project, but may not be any worse off
depending on their needs. Instructors that expect to review grades infrequently enough that not having a direct link
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
### Installation
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.
When not mounted, gradebook will run in the shared MFE container at http://apps.local.openedx.io/gradebook/course-v1:edX+DemoX+Demo_Course.
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.
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).
The parts of this MFE that can be customized in that manner are documented [here](/src/plugin-slots).
## Running tests
Run:
``nvm use``
``npm ci``
``npm test``
## Directory Structure
* `config`
* Directory for [`webpack`](https://webpack.js.org/) configurations
* `public`
* Entry point for the single-page application - `gradebook` has a single `index.html` file
* `src`
* `components`
* Directory for presentational `React` components
* `containers`
* Directory for container `React` components
* `data`
* `actions`
* Directory for `Redux` action creators
* `constants`
* `reducers`
* Directory for `Redux` reducers
## 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.

253
README.rst Normal file
View File

@@ -0,0 +1,253 @@
frontend-app-gradebook
#######################
|Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
Purpose
*******
Gradebook allows course staff to view, filter, and override subsection grades for a course. Additionally for Masters courses, Gradebook enables bulk management of subsection grades.
Jump to:
- `Should I use Gradebook in my course?`_
- Quickstart_
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>`_
Should I use Gradebook in my course?
*************************************
What does this offer over the legacy gradebook?
================================================
The micro-frontend offers a great deal more granularity when searching for problems, an easy interface for editing grades, an
audit trail for seeing who edited what grade and what reason they gave (if any) for doing so.
UsageProblems can be filtered by student as in the traditional gradebook, but can also be filtered by scores to see who
scored within a certain range, and by assignment types (note: Not problem types, but categories like 'Exams' or
'Homework').
What does the legacy gradebook offer that this project does not?
=================================================================
This project does not (yet, at least) create any graphs, which the traditional gradebook does. It also does not give
quick links to the problems for the instructor to visit. It expects the instructor to be familiar with the problems they
are grading and which unit they refer to.
The gradebook is expected to be much more performant for larger numbers of students as well. The Instructor Dashboard
link for the legacy gradebook reports that "this feature is available only to courses with a small number of enrolled
learners." However, this project comes with no such warning.
Who should not change to this gradebook?
=========================================
Groups whose instructors need not ever manually override grades do not need this project, but may not be any worse off
depending on their needs. Instructors that expect to review grades infrequently enough that not having a direct link
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
***************
Installation
============
To install gradebook into your project:
.. code-block:: bash
npm i --save @edx/frontend-app-gradebook
Quickstart
==========
Cloning and Setup
-----------------
1. Clone your new repo:
.. code-block:: bash
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:
.. code-block:: bash
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:
.. code-block:: bash
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:
.. code-block:: bash
tutor dev start lms cms mfe
Startup
-------
1. Install npm dependencies:
.. code-block:: bash
cd frontend-app-gradebook && npm install
2. Start the dev server:
.. code-block:: bash
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.
When not mounted, gradebook will run in the shared MFE container at http://apps.local.openedx.io/gradebook/course-v1:edX+DemoX+Demo_Course.
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.
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>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
Running tests
*************
Run:
.. code-block:: bash
nvm use
npm ci
npm test
Directory Structure
*******************
* ``config``
* Directory for `webpack <https://webpack.js.org/>`_ configurations
* ``public``
* Entry point for the single-page application - ``gradebook`` has a single ``index.html`` file
* ``src``
* ``components``
* Directory for presentational ``React`` components
* ``containers``
* Directory for container ``React`` components
* ``data``
* ``actions``
* Directory for ``Redux`` action creators
* ``constants``
* ``reducers``
* Directory for ``Redux`` reducers
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.
Please see `LICENSE <LICENSE>`_ for details.
Contributing
************
Contributions are very welcome. Please read `How To Contribute`_ for details.
.. _How To Contribute: https://openedx.org/r/how-to-contribute
This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure
to have a discussion about your new feature idea with the maintainers prior to
beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing
your idea.
Getting Help
************
If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a `Slack
invitation`_, then join our `community Slack workspace`_. Because this is a
frontend repository, the best place to discuss it would be in the
`#wg-frontend channel`_.
For anything non-trivial, the best path is to open an issue in this repository
with as many details about the issue you are facing as you can provide.
https://github.com/openedx/frontend-app-gradebook/issues
For more information about these options, see the `Getting Help`_ page.
.. _Slack invitation: https://openedx.org/slack
.. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/community/connect
The Open edX Code of Conduct
*****************************
All community members are expected to follow the `Open edX Code of Conduct`_.
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
Reporting Security Issues
**************************
Please do not report security issues in public. Please email security@openedx.org.
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-gradebook.svg?branch=master
:target: https://travis-ci.com/edx/frontend-app-gradebook
.. |Codecov| image:: https://img.shields.io/codecov/c/gh/openedx/frontend-app-gradebook
:target: https://app.codecov.io/gh/openedx/frontend-app-gradebook
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-gradebook.svg
:target: @edx/frontend-app-gradebook
.. |npm_downloads| image:: https://img.shields.io/npm/dt/@edx/frontend-app-gradebook.svg
:target: @edx/frontend-app-gradebook
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-gradebook.svg
:target: @edx/frontend-app-gradebook
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
:target: https://github.com/semantic-release/semantic-release

View File

@@ -9,5 +9,6 @@ module.exports = createConfig('jest', {
'src/segment.js',
'src/postcss.config.js',
'testUtils', // don't unit test jest mocking tools
'testUtilsExtra', // don't unit test jest mocking tools
],
});

7626
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.6.2",
"version": "1.6.3",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": {
"type": "git",
@@ -31,17 +31,15 @@
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.4.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/frontend-component-header": "^6.6.1",
"@edx/frontend-platform": "^8.3.7",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@edx/reactifex": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.5",
"@openedx/frontend-plugin-framework": "^1.6.0",
"@openedx/paragon": "^22.16.0",
"@openedx/paragon": "^23.4.5",
"@redux-beacon/segment": "^1.0.0",
"@reduxjs/toolkit": "^1.5.1",
"classnames": "^2.2.6",
@@ -69,15 +67,16 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/react": "^16.2.0",
"@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",
"es-check": "^2.3.0",
"fetch-mock": "^12.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"react-dev-utils": "^12.0.1",
"react-test-renderer": "^18.3.1",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.3"
}
}

View File

@@ -1,13 +1,10 @@
// frontend-app-*/src/index.scss
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints.css" as paragonCustomMediaBreakpoints;
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // 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";

View File

@@ -1,63 +1,63 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Route } from 'react-router-dom';
import store from 'data/store';
import GradebookPage from 'containers/GradebookPage';
import { render, screen } from '@testing-library/react';
import App from './App';
jest.mock('react-router-dom', () => ({
BrowserRouter: () => 'BrowserRouter',
Route: () => 'Route',
Routes: () => 'Routes',
Routes: ({ children }) => children,
Route: ({ element }) => element,
}));
jest.mock('@edx/frontend-platform/react', () => ({
AppProvider: () => 'AppProvider',
AppProvider: ({ children }) => children,
}));
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' }));
jest.mock('data/store', () => 'testStore');
jest.mock('containers/GradebookPage', () => 'GradebookPage');
jest.mock('@edx/frontend-component-header', () => 'Header');
jest.mock('./head/Head', () => 'Head');
let el;
let secondChild;
jest.mock('@edx/frontend-component-header', () => ({
__esModule: true,
default: () => <div>Header</div>,
}));
describe('App router component', () => {
test('snapshot', () => {
expect(shallow(<App />).snapshot).toMatchSnapshot();
jest.mock('@edx/frontend-component-footer', () => ({
FooterSlot: () => <div>Footer</div>,
}));
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('component', () => {
beforeEach(() => {
el = shallow(<App />);
secondChild = el.instance.children;
});
describe('AppProvider', () => {
test('AppProvider is the parent component, passed the redux store props', () => {
expect(el.instance.type).toBe('AppProvider');
expect(el.instance.props.store).toEqual(store);
});
});
describe('Head', () => {
test('first child of AppProvider', () => {
expect(el.instance.children[0].type).toBe('Head');
});
});
describe('Router', () => {
test('second child of AppProvider', () => {
expect(secondChild[1].type).toBe('div');
});
test('Header is above/outside-of the routing', () => {
expect(secondChild[1].children[0].type).toBe('Header');
expect(secondChild[1].children[1].type).toBe('main');
});
test('Routing - GradebookPage is only route', () => {
expect(secondChild[1].findByType(Route)).toHaveLength(1);
expect(secondChild[1].findByType(Route)[0].props.path).toEqual('/:courseId');
expect(secondChild[1].findByType(Route)[0].props.element.type).toEqual(GradebookPage);
});
});
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();
});
});

View File

@@ -1,21 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App router component snapshot 1`] = `
<AppProvider
store="testStore"
>
<Head />
<div>
<Header />
<main>
<Routes>
<Route
element={<GradebookPage />}
path="/:courseId"
/>
</Routes>
</main>
<FooterSlot />
</div>
</AppProvider>
`;

View File

@@ -1,20 +1,10 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Alert } from '@openedx/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('@openedx/paragon', () => ({
Alert: () => 'Alert',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
@@ -29,47 +19,19 @@ const errorMessage = 'Oh noooooo';
describe('BulkManagementAlerts', () => {
describe('component', () => {
let el;
describe('no errer, no upload success', () => {
beforeEach(() => {
el = shallow(<BulkManagementAlerts />);
describe('states of the warnings', () => {
test('no alert shown', () => {
renderWithIntl(<BulkManagementAlerts bulkImportError="" uploadSuccess={false} />);
expect(document.querySelectorAll('.alert').length).toEqual(0);
});
test('snapshot - bulkImportError closed, success closed', () => {
expect(el.snapshot).toMatchSnapshot();
test('Just success alert shown', () => {
renderWithIntl(<BulkManagementAlerts bulkImportError="" uploadSuccess />);
expect(document.querySelectorAll('.alert-success').length).toEqual(1);
});
test('closed danger alert', () => {
expect(el.instance.children[0].type).toBe('Alert');
expect(el.instance.findByType(Alert)[0].props.show).toEqual(false);
expect(el.instance.findByType(Alert)[0].props.variant).toEqual('danger');
});
test('closed success alert', () => {
expect(el.instance.children[1].type).toBe('Alert');
expect(el.instance.findByType(Alert)[1].props.show).toEqual(false);
expect(el.instance.findByType(Alert)[1].props.variant).toEqual('success');
});
});
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.snapshot).toMatchSnapshot();
});
test('open danger alert with bulkImportError content', () => {
expect(el.instance.children[0].type).toBe('Alert');
expect(el.instance.findByType(Alert)[0].children[0].el).toEqual(errorMessage);
expect(el.instance.findByType(Alert)[0].props.show).toEqual(true);
});
test('open success alert with messages.successDialog content', () => {
expect(el.instance.children[1].type).toBe('Alert');
expect(el.shallowWrapper.props.children[1].props.children).toEqual(
<FormattedMessage {...messages.successDialog} />,
);
expect(el.instance.children[1].props.show).toEqual(true);
test('Just error alert shown', () => {
renderWithIntl(<BulkManagementAlerts bulkImportError={errorMessage} uploadSuccess={false} />);
expect(document.querySelectorAll('.alert-danger').length).toEqual(1);
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
});
});

View File

@@ -1,108 +1,187 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import { DataTable } from '@openedx/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';
jest.mock('@openedx/paragon', () => ({ DataTable: () => 'DataTable' }));
initializeMocks();
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
jest.mock('@openedx/paragon', () => ({
...jest.requireActual('@openedx/paragon'),
DataTable: jest.fn(() => <div data-testid="data-table">DataTable</div>),
}));
jest.mock('./ResultsSummary', () => jest.fn(() => <div data-testid="results-summary">ResultsSummary</div>));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
grades: {
bulkManagementHistoryEntries: jest.fn(state => ({ historyEntries: state })),
bulkManagementHistoryEntries: jest.fn(),
},
},
}));
jest.mock('./ResultsSummary', () => 'ResultsSummary');
describe('HistoryTable', () => {
describe('component', () => {
const entry1 = {
originalFilename: 'blue.png',
user: 'Eifel',
timeUploaded: '65',
beforeEach(() => {
jest.clearAllMocks();
});
const mockBulkManagementHistory = [
{
originalFilename: 'test-file-1.csv',
user: 'test-user-1',
timeUploaded: '2025-01-01T10:00:00Z',
resultsSummary: {
rowId: 12,
courseId: 'Da Bu Dee',
text: 'Da ba daa',
rowId: 1,
text: 'Download results 1',
},
};
const entry2 = {
originalFilename: 'allStar.jpg',
user: 'Smashmouth',
timeUploaded: '2000s?',
},
{
originalFilename: 'test-file-2.csv',
user: 'test-user-2',
timeUploaded: '2025-01-02T10:00:00Z',
resultsSummary: {
courseId: 'rockstar',
rowId: 2,
text: 'all that glitters is gold',
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',
};
const props = {
bulkManagementHistory: [entry1, entry2],
};
let el;
describe('snapshot', () => {
beforeEach(() => {
el = shallow(<HistoryTable {...props} />);
});
test('snapshot - loads formatted table', () => {
expect(el.snapshot).toMatchSnapshot();
});
describe('history table', () => {
let table;
beforeEach(() => {
table = el.instance.findByType(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[0].props.data).toMatchSnapshot();
});
test(fieldAssertions.join(', '), () => {
const rows = table[0].props.data;
expect(rows[0].resultsSummary).toEqual(<ResultsSummary {...entry1.resultsSummary} />);
expect(rows[0].user).toEqual(<span className="wrap-text-in-cell">{entry1.user}</span>);
expect(
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[0].props.columns).toEqual(bulkManagementColumns);
});
});
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);
});
});
describe('mapStateToProps', () => {
const testState = { a: 'simple', test: 'state' };
let mapped;
const mockState = { test: 'state' };
const mockHistoryEntries = [
{ originalFilename: 'file1.csv', user: 'user1' },
{ originalFilename: 'file2.csv', user: 'user2' },
];
beforeEach(() => {
mapped = mapStateToProps(testState);
selectors.grades.bulkManagementHistoryEntries.mockReturnValue(
mockHistoryEntries,
);
});
test('bulkManagementHistory from grades.bulkManagementHistoryEntries', () => {
it('maps bulkManagementHistory from selector', () => {
const result = mapStateToProps(mockState);
expect(
mapped.bulkManagementHistory,
).toEqual(selectors.grades.bulkManagementHistoryEntries(testState));
selectors.grades.bulkManagementHistoryEntries,
).toHaveBeenCalledWith(mockState);
expect(result.bulkManagementHistory).toBe(mockHistoryEntries);
});
});
});

View File

@@ -19,10 +19,8 @@ const ResultsSummary = ({
text,
}) => (
<Hyperlink
href={lms.urls.bulkGradesUrlByRow(rowId)}
destination="www.edx.org"
destination={lms.urls.bulkGradesUrlByRow(rowId)}
target="_blank"
rel="noopener noreferrer"
showLaunchIcon={false}
>
<Icon src={Download} className="d-inline-block" />

View File

@@ -1,21 +1,12 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Download } from '@openedx/paragon/icons';
import lms from 'data/services/lms';
import { renderWithIntl, screen } from '../../testUtilsExtra';
import ResultsSummary from './ResultsSummary';
jest.mock('@openedx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
Icon: () => 'Icon',
}));
jest.mock('@openedx/paragon/icons', () => ({
Download: 'DownloadIcon',
}));
jest.mock('data/services/lms', () => ({
urls: {
bulkGradesUrlByRow: jest.fn((rowId) => ({ url: { rowId } })),
bulkGradesUrlByRow: jest.fn((rowId) => (`www.edx.org/${rowId}`)),
},
}));
@@ -24,29 +15,21 @@ describe('ResultsSummary component', () => {
rowId: 42,
text: 'texty',
};
let el;
const assertions = [
'safe hyperlink with bulkGradesUrl with course and row id',
'download icon',
'results text',
];
let link;
beforeEach(() => {
el = shallow(<ResultsSummary {...props} />);
});
test(`snapshot - ${assertions.join(', ')}`, () => {
expect(el.snapshot).toMatchSnapshot();
renderWithIntl(<ResultsSummary {...props} />);
link = screen.getByRole('link', { name: props.text });
});
test('Hyperlink has target="_blank" and rel="noopener noreferrer"', () => {
expect(el.instance.props.target).toEqual('_blank');
expect(el.instance.props.rel).toEqual('noopener noreferrer');
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});
test('Hyperlink has href to bulkGradesUrl', () => {
expect(el.instance.props.href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
expect(link).toHaveAttribute('href', lms.urls.bulkGradesUrlByRow(props.rowId));
});
test('displays Download Icon and text', () => {
const icon = el.instance.children[0];
expect(icon.type).toEqual('Icon');
expect(icon.props.src).toEqual(Download);
expect(el.instance.children[1].el).toEqual(props.text);
expect(link).toHaveTextContent(props.text);
const icon = screen.getByRole('img', { hidden: true });
expect(icon).toBeInTheDocument();
});
});

View File

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

View File

@@ -1,118 +0,0 @@
// 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`] = `
[
{
"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>,
},
{
"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={
[
{
"Header": "Gradebook",
"accessor": "filename",
"columnSortable": false,
"width": "col-5",
},
{
"Header": "Download Summary",
"accessor": "resultsSummary",
"columnSortable": false,
"width": "col",
},
{
"Header": "Who",
"accessor": "user",
"columnSortable": false,
"width": "col-1",
},
{
"Header": "When",
"accessor": "timeUploaded",
"columnSortable": false,
"width": "col",
},
]
}
data={
[
{
"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>,
},
{
"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}
/>
`;

View File

@@ -1,23 +0,0 @@
// 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={
{
"url": {
"rowId": 42,
},
}
}
rel="noopener noreferrer"
showLaunchIcon={false}
target="_blank"
>
<Icon
className="d-inline-block"
src="DownloadIcon"
/>
texty
</Hyperlink>
`;

View File

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

View File

@@ -1,46 +1,24 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { render, initializeMocks, screen } from 'testUtilsExtra';
import { BulkManagementHistoryView } from '.';
import BulkManagementAlerts from './BulkManagementAlerts';
import HistoryTable from './HistoryTable';
import messages from './messages';
jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts');
jest.mock('./HistoryTable', () => 'HistoryTable');
jest.mock('./BulkManagementAlerts', () => jest.fn(() => <div>BulkManagementAlerts</div>));
jest.mock('./HistoryTable', () => jest.fn(() => <div>HistoryTable</div>));
initializeMocks();
describe('BulkManagementHistoryView', () => {
describe('component', () => {
let el;
beforeEach(() => {
el = shallow(<BulkManagementHistoryView />);
render(<BulkManagementHistoryView />);
});
describe('snapshot', () => {
const snapshotSegments = [
'heading from messages.BulkManagementHistoryView.heading',
'<BulkManagementAlerts />',
'<HistoryTable />',
];
test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => {
expect(el.snapshot).toMatchSnapshot();
});
test('heading - h4 loaded from messages', () => {
const heading = el.instance.findByType('h4')[0];
const expectedHeading = shallow(
<h4>
<FormattedMessage {...messages.heading} />
</h4>,
);
expect(heading.el.type).toEqual(expectedHeading.type);
expect(heading.el.props).toEqual(expectedHeading.props);
});
test('heading, then alerts, then upload form, then table', () => {
expect(el.instance.children[0].type).toEqual('h4');
expect(el.instance.children[2].type).toEqual(BulkManagementAlerts);
expect(el.instance.children[3].type).toEqual(HistoryTable);
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();
});
});
});

View File

@@ -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: {

View File

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

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
// 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={
[
<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>
`;

View File

@@ -1,12 +1,9 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { render, screen, initializeMocks } from 'testUtilsExtra';
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();
@@ -25,29 +22,16 @@ useAssignmentFilterData.mockReturnValue({
const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilter component', () => {
beforeAll(() => {
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useAssignmentFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
initializeMocks();
render(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('filter options', () => {
const { options } = el.instance.findByType(SelectGroup)[0].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}`);
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);
});
});
});

View File

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

View File

@@ -1,17 +1,15 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
/* eslint-disable import/no-extraneous-dependencies */
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
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 = {
handleChange: jest.fn(),
handleSubmit: jest.fn(),
handleSetMax: jest.fn(),
handleSetMin: jest.fn(),
selectedAssignment: 'test-assignment',
@@ -22,37 +20,39 @@ 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', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
beforeEach(() => {
jest.clearAllMocks();
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
it('renders a PercentGroup for both Max and Min filters', () => {
let { props } = el.instance.findByType(PercentGroup)[0];
expect(props.value).toEqual(hookData.assignmentGradeMin);
expect(props.disabled).toEqual(false);
expect(props.onChange).toEqual(hookData.handleSetMin);
props = el.instance.findByType(PercentGroup)[1].props;
expect(props.value).toEqual(hookData.assignmentGradeMax);
expect(props.disabled).toEqual(false);
expect(props.onChange).toEqual(hookData.handleSetMax);
it('renders a 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 submit button', () => {
const { props } = el.instance.findByType(Button)[0];
expect(props.disabled).toEqual(false);
expect(props.onClick).toEqual(hookData.handleSubmit);
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();
});
});
describe('without selected assignment', () => {
@@ -61,16 +61,13 @@ describe('AssignmentFilter component', () => {
...hookData,
selectedAssignment: null,
});
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
it('disables controls', () => {
let { props } = el.instance.findByType(PercentGroup)[0];
expect(props.disabled).toEqual(true);
props = el.instance.findByType(PercentGroup)[1].props;
expect(props.disabled).toEqual(true);
const minGrade = screen.getByRole('spinbutton', { name: /Min Grade/ });
const maxGrade = screen.getByRole('spinbutton', { name: /Max Grade/ });
expect(minGrade).toHaveAttribute('disabled');
expect(maxGrade).toHaveAttribute('disabled');
});
});
});

View File

@@ -1,44 +0,0 @@
// 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={
[
<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>
`;

View File

@@ -1,12 +1,10 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { screen } from '@testing-library/react';
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();
@@ -21,27 +19,15 @@ useAssignmentFilterTypeData.mockReturnValue({
const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilterType component', () => {
beforeAll(() => {
el = shallow(<AssignmentFilterType updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useAssignmentFilterTypeData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
renderWithIntl(<AssignmentFilterType updateQueryParams={updateQueryParams} />);
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('filter options', () => {
const { options } = el.instance.findByType(SelectGroup)[0].props;
expect(options.length).toEqual(5);
const optionProps = options[1].props;
expect(optionProps.value).toEqual(assignmentTypes[0]);
expect(optionProps.children).toEqual(testType);
const options = screen.getAllByRole('option');
expect(options.length).toEqual(5); // 4 types + "All Types"
expect(options[1]).toHaveTextContent(testType);
});
});
});

View File

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

View File

@@ -1,13 +1,10 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { screen } from '@testing-library/react';
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 = {
@@ -27,48 +24,37 @@ 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', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
beforeEach(() => {
renderWithIntl(<CourseFilter updateQueryParams={updateQueryParams} />);
});
it('renders a PercentGroup for both Max and Min filters', () => {
let { props } = el.instance.findByType(PercentGroup)[0];
expect(props.value).toEqual(hookData.min.value);
expect(props.onChange).toEqual(hookData.min.onChange);
props = el.instance.findByType(PercentGroup)[1].props;
expect(props.value).toEqual(hookData.max.value);
expect(props.onChange).toEqual(hookData.max.onChange);
expect(screen.getByRole('spinbutton', { name: 'Min Grade' })).toHaveValue(hookData.min.value);
expect(screen.getByRole('spinbutton', { name: 'Max Grade' })).toHaveValue(hookData.max.value);
});
it('renders a submit button', () => {
const { props } = el.instance.findByType(Button)[0];
expect(props.disabled).toEqual(false);
expect(props.onClick).toEqual(hookData.handleApplyClick);
expect(screen.getByRole('button', { name: 'Apply' })).toBeInTheDocument();
// Expect it to be enabled
expect(screen.getByRole('button', { name: 'Apply' })).not.toBeDisabled();
});
});
describe('if disabled', () => {
beforeEach(() => {
jest.clearAllMocks();
useCourseGradeFilterData.mockReturnValueOnce({ ...hookData, isDisabled: true });
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
renderWithIntl(<CourseFilter updateQueryParams={updateQueryParams} />);
});
it('disables submit', () => {
const { props } = el.instance.findByType(Button)[0];
expect(props.disabled).toEqual(true);
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
});
});
});

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import PercentGroup from './PercentGroup';
@@ -12,6 +11,7 @@ describe('PercentGroup', () => {
};
beforeEach(() => {
initializeMocks();
props = {
...props,
onChange: jest.fn().mockName('props.onChange'),
@@ -19,15 +19,17 @@ describe('PercentGroup', () => {
});
describe('Component', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<PercentGroup {...props} />);
expect(el.snapshot).toMatchSnapshot();
});
test('disabled', () => {
const el = shallow(<PercentGroup {...props} disabled />);
expect(el.snapshot).toMatchSnapshot();
});
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();
});
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import SelectGroup from './SelectGroup';
@@ -24,15 +24,14 @@ describe('SelectGroup', () => {
});
describe('Component', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<SelectGroup {...props} />);
expect(el.snapshot).toMatchSnapshot();
});
test('disabled', () => {
const el = shallow(<SelectGroup {...props} disabled />);
expect(el.snapshot).toMatchSnapshot();
});
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();
});
});
});

View File

@@ -1,72 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StudentGroupsFilter component render snapshot 1`] = `
<Fragment>
<SelectGroup
id="Tracks"
label="Tracks"
onChange={[MockFunction]}
options={
[
<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={
[
<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>
`;

View File

@@ -1,84 +1,164 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import SelectGroup from '../SelectGroup';
import { StudentGroupsFilter } from './index';
import useStudentGroupsFilterData from './hooks';
import StudentGroupsFilter from '.';
jest.mock('../SelectGroup', () => 'SelectGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
jest.mock('../SelectGroup', () => jest.fn(() => <div data-testid="select-group">SelectGroup</div>));
jest.mock('./hooks', () => jest.fn());
const props = {
cohorts: {
value: 'test-cohort',
initializeMocks();
describe('StudentGroupsFilter', () => {
const mockUpdateQueryParams = jest.fn();
const mockTracksData = {
value: 'test-track-value',
entries: [
{ value: 'v1', name: 'n1' },
{ value: 'v2', name: 'n2' },
{ value: 'v3', name: 'n3' },
{ 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' },
],
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();
};
let el;
describe('StudentGroupsFilter component', () => {
beforeAll(() => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<StudentGroupsFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
useStudentGroupsFilterData.mockReturnValue({
tracks: mockTracksData,
cohorts: mockCohortsData,
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
it('calls useStudentGroupsFilterData hook with updateQueryParams', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({
updateQueryParams: mockUpdateQueryParams,
});
test('track options', () => {
const {
options,
onChange,
value,
} = el.instance.findByType(SelectGroup)[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('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('cohort options', () => {
const {
options,
onChange,
disabled,
value,
} = el.instance.findByType(SelectGroup)[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);
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);
});
});
});

View File

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

View File

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

View File

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

View File

@@ -1,82 +1,30 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Collapsible } from '@openedx/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 '.';
jest.mock('./AssignmentTypeFilter', () => 'AssignmentTypeFilter');
jest.mock('./AssignmentFilter', () => 'AssignmentFilter');
jest.mock('./AssignmentGradeFilter', () => 'AssignmentGradeFilter');
jest.mock('./CourseGradeFilter', () => 'CourseGradeFilter');
jest.mock('./StudentGroupsFilter', () => 'StudentGroupsFilter');
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();
initializeMocks();
describe('GradebookFilters', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<GradebookFilters updateQueryParams={updateQueryParams} />);
render(<GradebookFilters updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useGradebookFiltersData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
describe('All filters render together', () => {
test('Assignment filters', () => {
expect(el.instance.findByType(Collapsible)[0].children[0]).toMatchObject(shallow(
<div>
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
<AssignmentFilter updateQueryParams={updateQueryParams} />
<AssignmentGradeFilter updateQueryParams={updateQueryParams} />
</div>,
));
expect(screen.getByRole('combobox', { name: 'Assignment Types' })).toBeInTheDocument();
expect(screen.getByRole('combobox', { name: 'Assignment' })).toBeInTheDocument();
});
test('CourseGrade filters', () => {
expect(el.instance.findByType(Collapsible)[1].children[0]).toMatchObject(shallow(
<CourseGradeFilter updateQueryParams={updateQueryParams} />,
));
expect(screen.getByRole('button', { name: 'Overall Grade' })).toBeInTheDocument();
});
test('StudentGroups filters', () => {
expect(el.instance.findByType(Collapsible)[2].children[0]).toMatchObject(shallow(
<StudentGroupsFilter updateQueryParams={updateQueryParams} />,
));
expect(screen.getByRole('button', { name: 'Student Groups' })).toBeInTheDocument();
});
test('includeCourseTeamMembers', () => {
const checkbox = el.instance.findByType(Collapsible)[3].children[0];
expect(checkbox.props).toEqual({
checked: true,
onChange: hookProps.includeCourseTeamMembers.handleChange,
});
expect(checkbox.children[0].el).toEqual(formatMessage(messages.includeCourseTeamMembers));
expect(screen.getByRole('button', { name: 'Include Course Team Members' })).toBeInTheDocument();
});
});
});

View File

@@ -1,139 +0,0 @@
// 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"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
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"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
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"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
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"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
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>
`;

View File

@@ -1,77 +1,300 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import userEvent from '@testing-library/user-event';
import { formatMessage } from 'testUtils';
import { instructorDashboardUrl } from 'data/services/lms/urls';
import { GradebookHeader } from './index';
import useGradebookHeaderData from './hooks';
import GradebookHeader from '.';
import messages from './messages';
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
jest.mock('data/services/lms/urls', () => ({
instructorDashboardUrl: jest.fn(),
}));
jest.mock('./hooks', () => jest.fn());
instructorDashboardUrl.mockReturnValue('test-dashboard-url');
initializeMocks();
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);
describe('GradebookHeader', () => {
const mockHandleToggleViewClick = jest.fn();
let el;
describe('GradebookHeader component', () => {
beforeAll(() => {
el = shallow(<GradebookHeader />);
beforeEach(() => {
jest.clearAllMocks();
instructorDashboardUrl.mockReturnValue('https://example.com/dashboard');
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useGradebookHeaderData).toHaveBeenCalledWith();
expect(useIntl).toHaveBeenCalledWith();
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('render', () => {
describe('default view', () => {
test('shapshot', () => {
expect(el.snapshot).toMatchSnapshot();
describe('bulk management toggle button', () => {
describe('when showBulkManagement is true', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: true,
toggleViewMessage: messages.toActivityLog,
});
});
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');
});
});
describe('show bulk management', () => {
describe('when showBulkManagement is false', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true });
el = shallow(<GradebookHeader />);
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
test('snapshot: show toggle view message button with handleToggleViewClick method', () => {
expect(el.snapshot).toMatchSnapshot();
const { onClick } = el.instance.findByType(Button)[0].props;
expect(onClick).toEqual(hookProps.handleToggleViewClick);
expect(el.instance.findByType(Button)[0].children[0].el).toEqual(formatMessage(hookProps.toggleViewMessage));
it('does not render toggle view button', () => {
render(<GradebookHeader />);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});
describe('frozen grades', () => {
});
describe('frozen grades warning', () => {
describe('when areGradesFrozen is true', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true });
el = shallow(<GradebookHeader />);
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: true,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
test('snapshot: show frozen warning', () => {
expect(el.snapshot).toMatchSnapshot();
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.',
);
});
});
describe('user cannot view gradebook', () => {
describe('when areGradesFrozen is false', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false });
el = shallow(<GradebookHeader />);
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
test('snapshot: show unauthorized warning', () => {
expect(el.snapshot).toMatchSnapshot();
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();
});
});
});

View File

@@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = `
<div
className="d-flex"
>
<NetworkButton
label={
{
"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>
`;

View File

@@ -1,32 +1,160 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import NetworkButton from 'components/NetworkButton';
import ImportGradesButton from '../ImportGradesButton';
import { BulkManagementControls } from './index';
import useBulkManagementControlsData from './hooks';
import BulkManagementControls from '.';
jest.mock('../ImportGradesButton', () => 'ImportGradesButton');
jest.mock('components/NetworkButton', () => 'NetworkButton');
import messages from './messages';
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());
const hookProps = {
show: true,
handleClickExportGrades: jest.fn(),
};
useBulkManagementControlsData.mockReturnValue(hookProps);
initializeMocks();
describe('BulkManagementControls', () => {
describe('behavior', () => {
shallow(<BulkManagementControls />);
expect(useBulkManagementControlsData).toHaveBeenCalledWith();
const mockHandleClickExportGrades = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('render', () => {
test('snapshot - show - network and import buttons', () => {
expect(shallow(<BulkManagementControls />).snapshot).toMatchSnapshot();
describe('when show is false', () => {
beforeEach(() => {
useBulkManagementControlsData.mockReturnValue({
show: false,
handleClickExportGrades: mockHandleClickExportGrades,
});
});
test('snapshot - empty if show is not truthy', () => {
useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false });
expect(shallow(<BulkManagementControls />).isEmptyRender()).toEqual(true);
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);
});
});
});

View File

@@ -1,17 +1,100 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import HistoryHeader from './HistoryHeader';
initializeMocks();
describe('HistoryHeader', () => {
const props = {
id: 'water',
label: 'Brita',
value: 'hydration',
const defaultProps = {
id: 'test-id',
label: 'Test Label',
value: 'Test Value',
};
describe('Component', () => {
test('snapshot', () => {
expect(shallow(<HistoryHeader {...props} />).snapshot).toMatchSnapshot();
});
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',
);
});
});

View File

@@ -1,16 +1,8 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { selectors } from 'data/redux/hooks';
import { formatMessage } from 'testUtils';
import HistoryHeader from './HistoryHeader';
import ModalHeaders, { HistoryKeys } from './ModalHeaders';
import messages from './messages';
jest.mock('./HistoryHeader', () => 'HistoryHeader');
import { render, screen, initializeMocks } from 'testUtilsExtra';
import ModalHeaders from './ModalHeaders';
jest.mock('data/redux/hooks', () => ({
selectors: {
@@ -29,57 +21,25 @@ const gradeData = {
gradeOriginalEarnedGraded: 'test-original-grade',
};
selectors.grades.useGradeData.mockReturnValue(gradeData);
initializeMocks();
let el;
describe('ModalHeaders', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<ModalHeaders />);
});
describe('behavior', () => {
it('initializes intl', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
expect(selectors.app.useModalData).toHaveBeenCalled();
expect(selectors.grades.useGradeData).toHaveBeenCalled();
});
render(<ModalHeaders />);
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('assignment header', () => {
const headerProps = el.instance.findByType(HistoryHeader)[0].props;
expect(headerProps).toMatchObject({
id: HistoryKeys.assignment,
label: formatMessage(messages.assignmentHeader),
value: modalData.assignmentName,
});
expect(screen.getByText(modalData.assignmentName)).toBeInTheDocument();
});
test('student header', () => {
const headerProps = el.instance.findByType(HistoryHeader)[1].props;
expect(headerProps).toMatchObject({
id: HistoryKeys.student,
label: formatMessage(messages.studentHeader),
value: modalData.updateUserName,
});
expect(screen.getByText(modalData.updateUserName)).toBeInTheDocument();
});
test('originalGrade header', () => {
const headerProps = el.instance.findByType(HistoryHeader)[2].props;
expect(headerProps).toMatchObject({
id: HistoryKeys.originalGrade,
label: formatMessage(messages.originalGradeHeader),
value: gradeData.gradeOriginalEarnedGraded,
});
expect(screen.getByText(gradeData.gradeOriginalEarnedGraded)).toBeInTheDocument();
});
test('currentGrade header', () => {
const headerProps = el.instance.findByType(HistoryHeader)[3].props;
expect(headerProps).toMatchObject({
id: HistoryKeys.currentGrade,
label: formatMessage(messages.currentGradeHeader),
value: gradeData.gradeOverrideCurrentEarnedGradedOverride,
});
expect(screen.getByText(gradeData.gradeOverrideCurrentEarnedGradedOverride)).toBeInTheDocument();
});
});
});

View File

@@ -1,13 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AdjustedGradeInput component render snapshot 1`] = `
<span>
<Form.Control
name="adjustedGradeValue"
onChange={[MockFunction hook.onChange]}
type="text"
value="test-value"
/>
some-hint-text
</span>
`;

View File

@@ -1,7 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Form } from '@openedx/paragon';
import { render, screen } from '@testing-library/react';
import useAdjustedGradeInputData from './hooks';
import AdjustedGradeInput from '.';
@@ -15,24 +13,17 @@ const hookProps = {
};
useAdjustedGradeInputData.mockReturnValue(hookProps);
let el;
describe('AdjustedGradeInput component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<AdjustedGradeInput />);
});
describe('behavior', () => {
it('initializes hook data', () => {
expect(useAdjustedGradeInputData).toHaveBeenCalled();
});
render(<AdjustedGradeInput />);
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
const control = el.instance.findByType(Form.Control)[0];
expect(control.props.value).toEqual(hookProps.value);
expect(control.props.onChange).toEqual(hookProps.onChange);
expect(el.instance.children[1].el).toContain(hookProps.hintText);
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();
});
});
});

View File

@@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReasonInput component render snapshot 1`] = `
<Form.Control
data-testid="reason-input-control"
name="reasonForChange"
onChange={[MockFunction hook.onChange]}
type="text"
value="test-value"
/>
`;

View File

@@ -16,6 +16,12 @@ jest.mock('data/redux/hooks', () => ({
},
}));
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);
@@ -25,6 +31,7 @@ const ref = { current: { focus: jest.fn() }, useRef: true };
React.useRef.mockReturnValue(ref);
let out;
describe('useReasonInputData hook', () => {
beforeEach(() => {
jest.clearAllMocks();

View File

@@ -1,7 +1,4 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Form } from '@openedx/paragon';
import { render, screen } from '@testing-library/react';
import useReasonInputData from './hooks';
import ReasonInput from '.';
@@ -9,29 +6,26 @@ import ReasonInput from '.';
jest.mock('./hooks', () => jest.fn());
const hookProps = {
ref: 'reason-input-ref',
ref: jest.fn().mockName('hook.ref'),
onChange: jest.fn().mockName('hook.onChange'),
value: 'test-value',
};
useReasonInputData.mockReturnValue(hookProps);
let el;
describe('ReasonInput component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<ReasonInput />);
render(<ReasonInput />);
});
describe('behavior', () => {
it('initializes hook data', () => {
expect(useReasonInputData).toHaveBeenCalled();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
const control = el.instance.findByType(Form.Control)[0];
expect(control.props.value).toEqual(hookProps.value);
expect(control.props.onChange).toEqual(hookProps.onChange);
describe('renders', () => {
it('input correctly', () => {
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveValue(hookProps.value);
});
});
});

View File

@@ -4,8 +4,6 @@ import { render } from '@testing-library/react';
import useReasonInputData from './hooks';
import ReasonInput, { controlTestId } from '.';
jest.unmock('react');
jest.unmock('@openedx/paragon');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const focus = jest.fn();

View File

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

View File

@@ -1,5 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
import { selectors } from 'data/redux/hooks';
@@ -16,6 +15,18 @@ jest.mock('data/redux/hooks', () => ({
},
}));
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 };
@@ -44,22 +55,22 @@ describe('useOverrideTableData', () => {
describe('columns', () => {
test('date column', () => {
const { Header, accessor } = out.columns[0];
expect(Header).toEqual(formatMessage(messages.dateHeader));
expect(Header).toEqual(messages.dateHeader.defaultMessage);
expect(accessor).toEqual(columns.date);
});
test('grader column', () => {
const { Header, accessor } = out.columns[1];
expect(Header).toEqual(formatMessage(messages.graderHeader));
expect(Header).toEqual(messages.graderHeader.defaultMessage);
expect(accessor).toEqual(columns.grader);
});
test('reason column', () => {
const { Header, accessor } = out.columns[2];
expect(Header).toEqual(formatMessage(messages.reasonHeader));
expect(Header).toEqual(messages.reasonHeader.defaultMessage);
expect(accessor).toEqual(columns.reason);
});
test('adjustedGrade column', () => {
const { Header, accessor } = out.columns[3];
expect(Header).toEqual(formatMessage(messages.adjustedGradeHeader));
expect(Header).toEqual(messages.adjustedGradeHeader.defaultMessage);
expect(accessor).toEqual(columns.adjustedGrade);
});
});

View File

@@ -1,65 +1,61 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { screen } from '@testing-library/react';
import { DataTable } from '@openedx/paragon';
import { formatDateForDisplay } from 'utils';
import AdjustedGradeInput from './AdjustedGradeInput';
import ReasonInput from './ReasonInput';
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());
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
jest.mock('./ReasonInput', () => 'ReasonInput');
const hookProps = {
hide: false,
data: [
{ test: 'data' },
{ andOther: 'test-data' },
{ filename: 'data' },
{ resultsSummary: 'test-data' },
],
columns: 'test-columns',
columns: [{
Header: 'Gradebook',
accessor: 'filename',
},
{
Header: 'Download Summary',
accessor: 'resultsSummary',
}],
};
useOverrideTableData.mockReturnValue(hookProps);
let el;
describe('OverrideTable component', () => {
beforeEach(() => {
jest
.clearAllMocks()
.useFakeTimers('modern')
.setSystemTime(new Date('2000-01-01').getTime());
el = shallow(<OverrideTable />);
});
describe('behavior', () => {
describe('hooks', () => {
it('initializes hook data', () => {
useOverrideTableData.mockReturnValue(hookProps);
renderWithIntl(<OverrideTable />);
expect(useOverrideTableData).toHaveBeenCalled();
});
});
describe('render', () => {
test('null render if hide', () => {
useOverrideTableData.mockReturnValueOnce({ ...hookProps, hide: true });
el = shallow(<OverrideTable />);
expect(el.isEmptyRender()).toEqual(true);
describe('behavior', () => {
it('null render if hide', () => {
useOverrideTableData.mockReturnValue({ ...hookProps, hide: true });
renderWithIntl(<OverrideTable />);
expect(screen.queryByRole('table')).toBeNull();
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
const table = el.instance.findByType(DataTable)[0];
expect(table.props.columns).toEqual(hookProps.columns);
const data = [...table.props.data];
const inputRow = data.pop();
const formattedDate = formatDateForDisplay(new Date());
expect(data).toEqual(hookProps.data);
expect(inputRow).toMatchObject({
adjustedGrade: <AdjustedGradeInput />,
date: formattedDate,
reason: <ReasonInput />,
});
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();
});
});
});

View File

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

View File

@@ -1,26 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ModalHeaders render snapshot 1`] = `
<div>
<HistoryHeader
id="assignment"
label="Assignment"
value="test-assignment-name"
/>
<HistoryHeader
id="student"
label="Student"
value="test-user-name"
/>
<HistoryHeader
id="original-grade"
label="Original Grade"
value="test-original-grade"
/>
<HistoryHeader
id="current-grade"
label="Current Grade"
value="test-current-grade"
/>
</div>
`;

View File

@@ -1,91 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditModal component render with error snapshot 1`] = `
<ModalDialog
hasCloseButton={true}
isFullscreenOnMobile={true}
isOpen="test-is-open"
onClose={[MockFunction hooks.onClose]}
size="xl"
title="Edit Grades"
>
<ModalDialog.Body>
<div>
<ModalHeaders />
<Alert
dismissible={false}
show={true}
variant="danger"
>
test-error
</Alert>
<OverrideTable />
<div>
Showing most recent actions (max 5). To see more, please contact support
</div>
<div>
Note: Once you save, your changes will be visible to students.
</div>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton
variant="tertiary"
>
Cancel
</ModalDialog.CloseButton>
<Button
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
variant="primary"
>
Save Grades
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
`;
exports[`EditModal component render without error snapshot 1`] = `
<ModalDialog
hasCloseButton={true}
isFullscreenOnMobile={true}
isOpen="test-is-open"
onClose={[MockFunction hooks.onClose]}
size="xl"
title="Edit Grades"
>
<ModalDialog.Body>
<div>
<ModalHeaders />
<Alert
dismissible={false}
show={false}
variant="danger"
/>
<OverrideTable />
<div>
Showing most recent actions (max 5). To see more, please contact support
</div>
<div>
Note: Once you save, your changes will be visible to students.
</div>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton
variant="tertiary"
>
Cancel
</ModalDialog.CloseButton>
<Button
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
variant="primary"
>
Save Grades
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
`;

View File

@@ -1,126 +1,102 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
ActionRow,
ModalDialog,
} from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import ModalHeaders from './ModalHeaders';
import OverrideTable from './OverrideTable';
import useEditModalData from './hooks';
import EditModal from '.';
import messages from './messages';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('./hooks', () => jest.fn());
jest.mock('./ModalHeaders', () => 'ModalHeaders');
jest.mock('./OverrideTable', () => 'OverrideTable');
jest.mock('./ModalHeaders', () => jest.fn(() => <div>ModalHeaders</div>));
jest.mock('./OverrideTable', () => jest.fn(() => <div>OverrideTable</div>));
const hookProps = {
onClose: jest.fn().mockName('hooks.onClose'),
error: 'test-error',
handleAdjustedGradeClick: jest.fn().mockName('hooks.handleAdjustedGradeClick'),
isOpen: 'test-is-open',
isOpen: true,
};
useEditModalData.mockReturnValue(hookProps);
let el;
describe('EditModal component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<EditModal />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes component hooks', () => {
useEditModalData.mockReturnValue(hookProps);
renderWithIntl(<EditModal />);
expect(useEditModalData).toHaveBeenCalled();
});
});
describe('render', () => {
test('modal props', () => {
const modalProps = el.instance.findByType(ModalDialog)[0].props;
expect(modalProps.title).toEqual(formatMessage(messages.title));
expect(modalProps.isOpen).toEqual(hookProps.isOpen);
expect(modalProps.onClose).toEqual(hookProps.onClose);
});
const loadBody = () => {
const body = el.instance.findByType(ModalDialog)[0].children[0];
const { children } = body.children[0];
return { body, children };
describe('renders', () => {
const testModal = () => {
it('modal properly', () => {
const modal = screen.getByRole('dialog', { title: messages.title.defaultMessage });
expect(modal).toBeInTheDocument();
});
it('triggers onClose when closed', async () => {
const user = userEvent.setup();
const closeButton = screen.getByRole('button', { name: messages.closeText.defaultMessage });
await user.click(closeButton);
expect(hookProps.onClose).toHaveBeenCalled();
});
};
const testBody = () => {
test('type', () => {
const { body } = loadBody();
expect(body.type).toEqual('ModalDialog.Body');
it('headers row', () => {
const headers = screen.getByText('ModalHeaders');
expect(headers).toBeInTheDocument();
});
test('headers row', () => {
const { children } = loadBody();
expect(children[0]).toMatchObject(shallow(<ModalHeaders />));
it('table row', () => {
const table = screen.getByText('OverrideTable');
expect(table).toBeInTheDocument();
});
test('table row', () => {
const { children } = loadBody();
expect(children[2]).toMatchObject(shallow(<OverrideTable />));
});
test('messages', () => {
const { children } = loadBody();
expect(children[3].children[0].el).toEqual(formatMessage(messages.visibility));
expect(children[4].children[0].el).toEqual(formatMessage(messages.saveVisibility));
it('messages', () => {
const visibilityMessage = screen.getByText(messages.visibility.defaultMessage);
const saveVisibilityMessage = screen.getByText(messages.saveVisibility.defaultMessage);
expect(visibilityMessage).toBeInTheDocument();
expect(saveVisibilityMessage).toBeInTheDocument();
});
};
const testFooter = () => {
let footer;
beforeEach(() => {
footer = el.instance.findByType(ModalDialog)[0].children;
it('adjusted grade button', async () => {
const user = userEvent.setup();
const saveGradeButton = screen.getByRole('button', { name: messages.saveGrade.defaultMessage });
expect(saveGradeButton).toBeInTheDocument();
await user.click(saveGradeButton);
expect(hookProps.handleAdjustedGradeClick).toHaveBeenCalled();
});
test('type', () => {
expect(footer[1].type).toEqual('ModalDialog.Footer');
});
test('contains action row', () => {
expect(footer[1].children[0].type).toEqual('ActionRow');
});
test('close button', () => {
const button = footer[1].findByType(ActionRow)[0].children[0];
expect(button.children[0].el).toEqual(formatMessage(messages.closeText));
expect(button.type).toEqual('ModalDialog.CloseButton');
});
test('adjusted grade button', () => {
const button = footer[1].findByType(ActionRow)[0].children[1];
expect(button.children[0].el).toEqual(formatMessage(messages.saveGrade));
expect(button.type).toEqual('Button');
expect(button.props.onClick).toEqual(hookProps.handleAdjustedGradeClick);
it('close button', async () => {
const user = userEvent.setup();
const cancelButton = screen.getByRole('button', { name: messages.closeText.defaultMessage });
expect(cancelButton).toBeInTheDocument();
await user.click(cancelButton);
expect(hookProps.onClose).toHaveBeenCalled();
});
};
describe('without error', () => {
beforeEach(() => {
useEditModalData.mockReturnValueOnce({ ...hookProps, error: undefined });
el = shallow(<EditModal />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
renderWithIntl(<EditModal />);
});
testModal();
testBody();
testFooter();
test('alert row', () => {
const alert = loadBody().children[1];
expect(alert.type).toEqual('Alert');
expect(alert.props.show).toEqual(false);
const alert = screen.queryByRole('alert');
expect(alert).toBeNull();
});
});
describe('with error', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
beforeEach(() => {
useEditModalData.mockReturnValue(hookProps);
renderWithIntl(<EditModal />);
});
testModal();
testBody();
test('alert row', () => {
const alert = loadBody().children[1];
expect(alert.type).toEqual('Alert');
expect(alert.props.show).toEqual(true);
expect(alert.children[0].el).toEqual(hookProps.error);
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
expect(alert).toHaveTextContent(hookProps.error);
});
testFooter();
});

View File

@@ -1,15 +1,12 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { screen } from '@testing-library/react';
import { formatMessage } from 'testUtils';
import { Button } from '@openedx/paragon';
import { selectors } from 'data/redux/hooks';
import userEvent from '@testing-library/user-event';
import FilterBadge from './FilterBadge';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('@openedx/paragon', () => ({
Button: () => 'Button',
}));
jest.mock('data/redux/hooks', () => ({
selectors: {
root: {
@@ -18,11 +15,12 @@ jest.mock('data/redux/hooks', () => ({
},
}));
const handleClose = jest.fn(filters => ({ handleClose: filters }));
const handleClose = jest.fn();
const filterName = 'test-filter-name';
const hookProps = {
displayName: {
id: 'test.id',
defaultMessage: 'a common name',
},
isDefault: false,
@@ -32,63 +30,52 @@ const hookProps = {
};
selectors.root.useFilterBadgeConfig.mockReturnValue(hookProps);
let el;
describe('FilterBadge', () => {
beforeEach(() => {
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
describe('hooks', () => {
beforeEach(() => {
renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
});
it('initializes redux hooks', () => {
expect(selectors.root.useFilterBadgeConfig).toHaveBeenCalledWith(filterName);
});
});
describe('render', () => {
const testDisplayName = () => {
test('formatted display name appears on badge', () => {
expect(el.instance.findByTestId('display-name')[0].children[0].el).toEqual(formatMessage(hookProps.displayName));
});
};
const testCloseButton = () => {
test('close button forwards close method', () => {
expect(el.instance.findByType(Button)[0].props.onClick).toEqual(handleClose(hookProps.connectedFilters));
});
};
test('empty render if isDefault', () => {
it('empty render if isDefault', () => {
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
...hookProps,
isDefault: true,
});
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
expect(el.isEmptyRender()).toEqual(true);
renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
expect(screen.queryByText(hookProps.displayName)).toBeNull();
});
describe('hide Value', () => {
beforeEach(() => {
it('renders display name, value is not shown and close button has correct behavior', async () => {
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
...hookProps,
hideValue: true,
});
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
});
testDisplayName();
testCloseButton();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('value is note present in the badge', () => {
expect(el.instance.findByTestId('filter-value')[0].children).toHaveLength(0);
renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
const user = userEvent.setup();
expect(screen.getByTestId('display-name')).toHaveTextContent(formatMessage(hookProps.displayName));
expect(screen.queryByTestId('filter-value')).toHaveTextContent('');
const button = screen.getByRole('button', { name: /close/i });
await user.click(button);
expect(handleClose).toHaveBeenCalledWith(hookProps.connectedFilters);
});
});
describe('do not hide value', () => {
testDisplayName();
testCloseButton();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('value is present in the badge', () => {
expect(el.instance.findByTestId('filter-value')[0].children[0].el).toBe(`: ${hookProps.value}`);
it('renders display name and value, and close button has correct behavior', async () => {
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
...hookProps,
hideValue: false,
});
renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
const user = userEvent.setup();
expect(screen.getByTestId('display-name')).toHaveTextContent(formatMessage(hookProps.displayName));
expect(screen.getByTestId('filter-value')).toHaveTextContent(`: ${hookProps.value}`);
const button = screen.getByRole('button', { name: /close/i });
await user.click(button);
expect(handleClose).toHaveBeenCalledWith(hookProps.connectedFilters);
});
});
});

View File

@@ -1,75 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterBadge render do not hide value snapshot 1`] = `
<div>
<span
className="badge badge-info"
>
<span
data-testid="display-name"
>
a common name
</span>
<span
data-testid="filter-value"
>
: a common value
</span>
<Button
aria-label="close"
className="btn-info"
onClick={
{
"handleClose": [
"some",
"filters",
],
}
}
>
<span
aria-hidden="true"
>
×
</span>
</Button>
</span>
<br />
</div>
`;
exports[`FilterBadge render hide Value snapshot 1`] = `
<div>
<span
className="badge badge-info"
>
<span
data-testid="display-name"
>
a common name
</span>
<span
data-testid="filter-value"
/>
<Button
aria-label="close"
className="btn-info"
onClick={
{
"handleClose": [
"some",
"filters",
],
}
}
>
<span
aria-hidden="true"
>
×
</span>
</Button>
</span>
<br />
</div>
`;

View File

@@ -1,21 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterBadges component snapshot - has a filterbadge with handleClose for each filter in badgeOrder 1`] = `
<div>
<FilterBadge
filterName="filter1"
handleClose={[MockFunction this.props.handleClose]}
key="filter1"
/>
<FilterBadge
filterName="filter2"
handleClose={[MockFunction this.props.handleClose]}
key="filter2"
/>
<FilterBadge
filterName="filter3"
handleClose={[MockFunction this.props.handleClose]}
key="filter3"
/>
</div>
`;

View File

@@ -1,11 +1,10 @@
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import FilterBadges from '.';
import FilterBadge from './FilterBadge';
jest.mock('./FilterBadge', () => 'FilterBadge');
const order = ['filter1', 'filter2', 'filter3'];
jest.mock('data/constants/filters', () => ({
@@ -13,24 +12,24 @@ jest.mock('data/constants/filters', () => ({
badgeOrder: order,
}));
// eslint-disable-next-line react/button-has-type
jest.mock('./FilterBadge', () => jest.fn(({ filterName, handleClose }) => <button onClick={handleClose}>{filterName}</button>));
const handleClose = jest.fn();
describe('FilterBadges', () => {
describe('component', () => {
let el;
let handleClose;
beforeEach(() => {
handleClose = jest.fn().mockName('this.props.handleClose');
el = shallow(<FilterBadges handleClose={handleClose} />);
});
test('snapshot - has a filterbadge with handleClose for each filter in badgeOrder', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('has a filterbadge with handleClose for each filter in badgeOrder', () => {
const badgeProps = el.instance.findByType(FilterBadge).map(badgeEl => badgeEl.props);
// key prop is not rendered by react
expect(badgeProps[0]).toMatchObject({ filterName: order[0], handleClose });
expect(badgeProps[1]).toMatchObject({ filterName: order[1], handleClose });
expect(badgeProps[2]).toMatchObject({ filterName: order[2], handleClose });
expect(badgeProps.length).toEqual(3);
it('has a filterbadge with handleClose for each filter in badgeOrder', async () => {
render(<FilterBadges handleClose={handleClose} />);
const user = userEvent.setup();
const badge1 = screen.getByText(order[0]);
const badge2 = screen.getByText(order[1]);
const badge3 = screen.getByText(order[2]);
expect(badge1).toBeInTheDocument();
expect(badge2).toBeInTheDocument();
expect(badge3).toBeInTheDocument();
await user.click(badge1);
expect(handleClose).toHaveBeenCalled();
});
});
});

View File

@@ -1,16 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterMenuToggle component render snapshot 1`] = `
<Button
className="btn-primary align-self-start"
id="edit-filters-btn"
onClick={[MockFunction hooks.toggleFilterMenu]}
>
<Icon
className="mr-1"
src="FilterAlt"
/>
Edit Filters
</Button>
`;

View File

@@ -1,13 +1,11 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { formatMessage } from 'testUtils';
import { thunkActions } from 'data/redux/hooks';
import FilterMenuToggle from '.';
import messages from './messages';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('data/redux/hooks', () => ({
thunkActions: {
@@ -22,26 +20,23 @@ jest.mock('data/redux/hooks', () => ({
const toggleFilterMenu = jest.fn().mockName('hooks.toggleFilterMenu');
thunkActions.app.filterMenu.useToggleMenu.mockReturnValue(toggleFilterMenu);
let el;
describe('FilterMenuToggle component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<FilterMenuToggle />);
renderWithIntl(<FilterMenuToggle />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
expect(thunkActions.app.filterMenu.useToggleMenu).toHaveBeenCalled();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el.instance.type).toEqual('Button');
expect(el.instance.props.onClick).toEqual(toggleFilterMenu);
expect(el.instance.children[2].el).toContain(formatMessage(messages.editFilters));
describe('renders', () => {
it('button and triggers click', async () => {
const user = userEvent.setup();
const button = screen.getByRole('button', { name: formatMessage(messages.editFilters) });
expect(button).toBeInTheDocument();
await user.click(button);
expect(toggleFilterMenu).toHaveBeenCalled();
});
});
});

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilteredUsersLabel component render snapshot 1`] = `
<format-message-function
message={
{
"defaultMessage": "Showing {filteredUsers} of {totalUsers} total learners",
"description": "Users visibility label",
"id": "gradebook.GradesTab.usersVisibilityLabel",
}
}
values={
{
"filteredUsers": <BoldText
text={100}
/>,
"totalUsers": <BoldText
text={123}
/>,
}
}
/>
`;

View File

@@ -1,13 +1,9 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { screen } from '@testing-library/react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { selectors } from 'data/redux/hooks';
import FilteredUsersLabel, { BoldText } from '.';
import messages from './messages';
import FilteredUsersLabel from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('data/redux/hooks', () => ({
selectors: {
@@ -23,34 +19,29 @@ const userCounts = {
};
selectors.grades.useUserCounts.mockReturnValue(userCounts);
let el;
describe('FilteredUsersLabel component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<FilteredUsersLabel />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
renderWithIntl(<FilteredUsersLabel />);
expect(selectors.grades.useUserCounts).toHaveBeenCalled();
});
});
describe('render', () => {
test('null render if totalUsersCount is 0', () => {
it('null render if totalUsersCount is 0', () => {
selectors.grades.useUserCounts.mockReturnValueOnce({
...userCounts,
totalUsersCount: 0,
});
expect(shallow(<FilteredUsersLabel />).isEmptyRender()).toEqual(true);
const { container } = renderWithIntl(<FilteredUsersLabel />);
expect(container.firstChild).toBeNull();
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el.instance).toMatchObject(shallow(formatMessage(messages.visibilityLabel, {
filteredUsers: <BoldText text={userCounts.filteredUsersCount} />,
totalUsers: <BoldText text={userCounts.totalUsersCount} />,
})));
it('renders users count correctly', () => {
renderWithIntl(<FilteredUsersLabel />);
expect(screen.getByText((text) => text.includes(userCounts.filteredUsersCount))).toBeInTheDocument();
expect(screen.getByText((text) => text.includes(userCounts.totalUsersCount))).toBeInTheDocument();
});
});
});

View File

@@ -1,11 +1,9 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import Fields from './Fields';
describe('Gradebook Table Fields', () => {
describe('Username', () => {
let el;
const username = 'MyNameFromHere';
describe('with external_user_key', () => {
const props = {
@@ -13,40 +11,32 @@ describe('Gradebook Table Fields', () => {
userKey: 'My name from another land',
};
beforeEach(() => {
el = shallow(<Fields.Username {...props} />);
render(<Fields.Username {...props} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('wraps external user key and username', () => {
expect(el.instance.findByType('span')[0].el).toMatchSnapshot();
const content = el.instance.findByType('span')[0].children[0];
expect(content.children[0].children[0].el).toEqual(username);
expect(content.children[1].children[0].el).toEqual(props.userKey);
it('wraps external user key and username', () => {
const usernameField = screen.getByText(username);
expect(usernameField).toBeInTheDocument();
const userKeyField = screen.getByText(props.userKey);
expect(userKeyField).toBeInTheDocument();
});
});
describe('without external_user_key', () => {
beforeEach(() => {
el = shallow(<Fields.Username username={username} />);
render(<Fields.Username username={username} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('wraps username only', () => {
const content = el.instance.findByType('span')[0].children[0];
expect(content.children[0].children[0].el).toEqual(username);
expect(content.children).toHaveLength(1);
it('wraps username only', () => {
const usernameField = screen.getByText(username);
expect(usernameField).toBeInTheDocument();
});
});
});
describe('Text', () => {
const value = 'myTag@place.com';
test('snapshot', () => {
expect(shallow(<Fields.Text value={value} />).snapshot).toMatchSnapshot();
});
test('wraps entry value', () => {
expect(shallow(<Fields.Text value={value} />).instance.children[0].el).toEqual(value);
it('wraps entry value', () => {
render(<Fields.Text value={value} />);
const textElement = screen.getByText(value);
expect(textElement).toBeInTheDocument();
});
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { selectors, thunkActions } from 'data/redux/hooks';
import transforms from 'data/redux/transforms';
@@ -47,7 +47,6 @@ selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
thunkActions.app.useSetModalStateFromTable.mockReturnValue(setModalState);
transforms.grades.subsectionGrade.mockReturnValue(subsectionGrade);
let el;
let out;
describe('GradeButton', () => {
beforeEach(() => {
@@ -98,23 +97,24 @@ describe('GradeButton', () => {
describe('frozen grades', () => {
beforeEach(() => {
hookSpy.mockReturnValue({ ...hookProps, areGradesFrozen: true });
el = shallow(<GradeButton {...props} />);
render(<GradeButton {...props} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el.instance.el).toEqual(hookProps.label);
it('renders only labels', () => {
const label = screen.getByText(hookProps.label);
expect(label).toBeInTheDocument();
});
});
describe('not frozen grades', () => {
beforeEach(() => {
hookSpy.mockReturnValue(hookProps);
el = shallow(<GradeButton {...props} />);
render(<GradeButton {...props} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el.instance.type).toEqual('Button');
expect(el.instance.props.onClick).toEqual(hookProps.onClick);
expect(el.instance.children[0].el).toEqual(hookProps.label);
it('renders button', async () => {
const user = userEvent.setup();
const button = screen.getByRole('button', { name: hookProps.label });
expect(button).toBeInTheDocument();
await user.click(button);
expect(hookProps.onClick).toHaveBeenCalled();
});
});
});

View File

@@ -1,10 +1,8 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { screen } from '@testing-library/react';
import { getLocale } from '@edx/frontend-platform/i18n';
import { OverlayTrigger } from '@openedx/paragon';
import LabelReplacements from './LabelReplacements';
import messages from './messages';
import { renderWithIntl } from '../../../testUtilsExtra';
const {
TotalGradeLabelReplacement,
@@ -12,52 +10,36 @@ const {
MastersOnlyLabelReplacement,
} = LabelReplacements;
jest.mock('@openedx/paragon', () => ({
Icon: () => 'Icon',
OverlayTrigger: () => 'OverlayTrigger',
Tooltip: () => 'Tooltip',
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
isRtl: jest.fn(),
}));
describe('LabelReplacements', () => {
describe('TotalGradeLabelReplacement', () => {
let el;
beforeEach(() => {
el = shallow(<TotalGradeLabelReplacement />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('displays overlay tooltip', () => {
expect(el.instance.findByType(OverlayTrigger)[0].props.overlay).toMatchSnapshot();
getLocale.mockImplementation(() => 'en');
renderWithIntl(<TotalGradeLabelReplacement />);
it('displays overlay tooltip', () => {
const tooltip = screen.getByText(messages.totalGradePercentage.defaultMessage);
expect(tooltip).toBeInTheDocument();
});
});
describe('UsernameLabelReplacement', () => {
test('snapshot', () => {
expect(shallow(<UsernameLabelReplacement />).snapshot).toMatchSnapshot();
it('renders correctly', () => {
renderWithIntl(<UsernameLabelReplacement />);
expect(screen.getByText(messages.usernameHeading.defaultMessage)).toBeInTheDocument();
});
});
describe('MastersOnlyLabelReplacement', () => {
test('snapshot', () => {
it('renders correctly', () => {
const message = {
id: 'id',
defaultMessage: 'defaultMessAge',
description: 'desCripTion',
};
expect(shallow(<MastersOnlyLabelReplacement {...message} />).snapshot).toMatchSnapshot();
renderWithIntl(<MastersOnlyLabelReplacement {...message} />);
expect(screen.getByText(message.defaultMessage)).toBeInTheDocument();
});
});
});
describe('snapshot', () => {
let el;
test('right to left overlay placement', () => {
getLocale.mockImplementation(() => 'en');
el = shallow(<TotalGradeLabelReplacement />);
expect(el.snapshot).toMatchSnapshot();
});
test('left to right overlay placement', () => {
getLocale.mockImplementation(() => 'ar');
el = shallow(<TotalGradeLabelReplacement />);
expect(el.snapshot).toMatchSnapshot();
});
});

View File

@@ -1,75 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Gradebook Table Fields Text snapshot 1`] = `
<span
className="wrap-text-in-cell"
>
myTag@place.com
</span>
`;
exports[`Gradebook Table Fields Username with external_user_key snapshot 1`] = `
<div>
<span
className="wrap-text-in-cell"
>
<div>
<div>
MyNameFromHere
</div>
<div
className="student-key"
>
My name from another land
</div>
</div>
</span>
</div>
`;
exports[`Gradebook Table Fields Username with external_user_key wraps external user key and username 1`] = `
{
"children": [
{
"children": [
{
"children": [
"MyNameFromHere",
],
"props": {},
"type": "div",
},
{
"children": [
"My name from another land",
],
"props": {
"className": "student-key",
},
"type": "div",
},
],
"props": {},
"type": "div",
},
],
"props": {
"className": "wrap-text-in-cell",
},
"type": "span",
}
`;
exports[`Gradebook Table Fields Username without external_user_key snapshot 1`] = `
<div>
<span
className="wrap-text-in-cell"
>
<div>
<div>
MyNameFromHere
</div>
</div>
</span>
</div>
`;

View File

@@ -1,13 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradeButton component frozen grades snapshot 1`] = `"test-label"`;
exports[`GradeButton component not frozen grades snapshot 1`] = `
<Button
className="btn-header grade-button"
onClick={[MockFunction hooks.onClick]}
variant="link"
>
test-label
</Button>
`;

View File

@@ -1,140 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LabelReplacements MastersOnlyLabelReplacement snapshot 1`] = `
<div>
defaultMessAge
<span
className="font-weight-normal"
>
*
</span>
</div>
`;
exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1`] = `
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage
</Tooltip>
`;
exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
<div>
<OverlayTrigger
key="left-basic"
overlay={
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage
</Tooltip>
}
placement="left"
trigger={
[
"hover",
"focus",
]
}
>
<div>
Total Grade (%)
<div
id="courseGradeTooltipIcon"
>
<Icon
className="fa fa-info-circle"
screenReaderText="Total Grade values are always displayed as a percentage"
/>
</div>
</div>
</OverlayTrigger>
</div>
`;
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
<div>
<div>
Username
</div>
<div
className="font-weight-normal student-key"
>
Student Key
<span
className="font-weight-normal"
>
*
</span>
</div>
</div>
`;
exports[`snapshot left to right overlay placement 1`] = `
<div>
<OverlayTrigger
key="left-basic"
overlay={
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage
</Tooltip>
}
placement="right"
trigger={
[
"hover",
"focus",
]
}
>
<div>
Total Grade (%)
<div
id="courseGradeTooltipIcon"
>
<Icon
className="fa fa-info-circle"
screenReaderText="Total Grade values are always displayed as a percentage"
/>
</div>
</div>
</OverlayTrigger>
</div>
`;
exports[`snapshot right to left overlay placement 1`] = `
<div>
<OverlayTrigger
key="left-basic"
overlay={
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage
</Tooltip>
}
placement="left"
trigger={
[
"hover",
"focus",
]
}
>
<div>
Total Grade (%)
<div
id="courseGradeTooltipIcon"
>
<Icon
className="fa fa-info-circle"
screenReaderText="Total Grade values are always displayed as a percentage"
/>
</div>
</div>
</OverlayTrigger>
</div>
`;

View File

@@ -1,32 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookTable snapshot 1`] = `
<div
className="gradebook-container"
>
<DataTable
RowStatusComponent={[MockFunction hooks.nullMethod]}
columns={
[
"some",
"columns",
]
}
data={
[
"some",
"data",
]
}
hasFixedColumnWidths={true}
itemCount={3}
rowHeaderColumnKey="username"
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable
content="empty-table-content"
/>
</DataTable>
</div>
`;

View File

@@ -1,47 +1,32 @@
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { getLocalizedPercentSign } from 'i18n/utils';
import { selectors } from 'data/redux/hooks';
import transforms from 'data/redux/transforms';
import React from 'react';
import { Headings } from 'data/constants/grades';
import LabelReplacements from './LabelReplacements';
import Fields from './Fields';
import GradeButton from './GradeButton';
import { initializeMocks, render } from '../../../testUtilsExtra';
import * as hooks from './hooks';
import messages from './messages';
import useGradebookTableData from './hooks';
jest.mock('i18n/utils', () => ({
getLocalizedPercentSign: () => '%',
}));
jest.mock('./GradeButton', () => 'GradeButton');
jest.mock('./Fields', () => jest.requireActual('testUtils').mockNestedComponents({
Username: 'Fields.Username',
Text: 'Fields.Text',
}));
jest.mock('./LabelReplacements', () => jest.requireActual('testUtils').mockNestedComponents({
TotalGradeLabelReplacement: 'LabelReplacements.TotalGradeLabelReplacement',
UsernameLabelReplacement: 'LabelReplacements.UsernameLabelReplacement',
MastersOnlyLabelReplacement: 'LabelReplacements.MastersOnlyLabelReplacement',
}));
let mockUseAllGrades;
let mockUseGetHeadings;
jest.mock('data/redux/hooks', () => ({
selectors: {
grades: { useAllGrades: jest.fn() },
root: { useGetHeadings: jest.fn() },
grades: { useAllGrades: () => mockUseAllGrades() },
root: { useGetHeadings: () => mockUseGetHeadings() },
},
}));
jest.mock('data/redux/transforms', () => ({
grades: { roundGrade: jest.fn() },
grades: { roundGrade: jest.fn((val) => val) },
}));
const roundGrade = grade => grade * 20;
transforms.grades.roundGrade.mockImplementation(roundGrade);
jest.mock('i18n/utils', () => ({ getLocalizedPercentSign: () => '%' }));
jest.mock('./Fields', () => ({ Username: () => null, Text: () => null }));
jest.mock('./GradeButton', () => ({ __esModule: true, default: () => null }));
jest.mock('./LabelReplacements', () => ({
TotalGradeLabelReplacement: () => null,
UsernameLabelReplacement: () => null,
MastersOnlyLabelReplacement: () => null,
}));
const subsectionLabels = [
'subsectionLabel1',
@@ -85,7 +70,9 @@ const allGrades = [
],
},
];
const testHeading = 'test-heading-value';
const headings = [
Headings.totalGrade,
Headings.username,
@@ -93,100 +80,60 @@ const headings = [
Headings.fullName,
testHeading,
];
selectors.grades.useAllGrades.mockReturnValue(allGrades);
selectors.root.useGetHeadings.mockReturnValue(headings);
let out;
describe('useGradebookTableData', () => {
describe('useGradebookTableData hook', () => {
beforeAll(() => {
mockUseAllGrades = jest.fn();
mockUseGetHeadings = jest.fn();
});
beforeEach(() => {
jest.clearAllMocks();
out = useGradebookTableData();
mockUseAllGrades.mockReset();
mockUseGetHeadings.mockReset();
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
expect(selectors.grades.useAllGrades).toHaveBeenCalled();
expect(selectors.root.useGetHeadings).toHaveBeenCalled();
let hookResult;
const TestComponent = () => {
hookResult = hooks.useGradebookTableData();
return null;
};
beforeEach(() => {
initializeMocks();
hookResult = null;
mockUseAllGrades.mockReturnValue([]);
mockUseGetHeadings.mockReturnValue([]);
});
it('returns expected structure with empty data', () => {
render(<TestComponent />);
expect(hookResult).toEqual({
columns: [],
data: [],
grades: [],
nullMethod: expect.any(Function),
emptyContent: expect.any(String),
});
});
describe('output', () => {
describe('columns', () => {
test('total grade heading produces TotalGradeLabelReplacement label', () => {
const { Header, accessor } = out.columns[0];
expect(accessor).toEqual(headings[0]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.TotalGradeLabelReplacement />),
);
});
test('username heading produces UsernameLabelReplacement', () => {
const { Header, accessor } = out.columns[1];
expect(accessor).toEqual(headings[1]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.UsernameLabelReplacement />),
);
});
test('email heading replaces with email heading message', () => {
const { Header, accessor } = out.columns[2];
expect(accessor).toEqual(headings[2]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />),
);
});
test('fullName heading replaces with fullName heading message', () => {
const { Header, accessor } = out.columns[3];
expect(accessor).toEqual(headings[3]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />),
);
});
test('other headings are passed through', () => {
const { Header, accessor } = out.columns[4];
expect(accessor).toEqual(headings[4]);
expect(Header).toEqual(headings[4]);
});
});
describe('data', () => {
test('username field', () => {
allGrades.forEach((entry, index) => {
expect(out.data[index][Headings.username]).toMatchObject(
<Fields.Username username={entry.username} userKey={entry.external_user_key} />,
);
});
});
test('email field', () => {
allGrades.forEach((entry, index) => {
expect(out.data[index][Headings.email]).toMatchObject(
<Fields.Text value={entry.email} />,
);
});
});
test('totalGrade field', () => {
allGrades.forEach((entry, index) => {
expect(out.data[index][Headings.totalGrade]).toEqual(
`${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
);
});
});
test('section breakdown', () => {
allGrades.forEach((entry, gradeIndex) => {
subsectionLabels.forEach((label, labelIndex) => {
expect(out.data[gradeIndex][label]).toMatchObject(
<GradeButton entry={entry} subsection={entry.section_breakdown[labelIndex]} />,
);
});
});
});
});
it('forwards grades from redux', () => {
expect(out.grades).toEqual(allGrades);
});
test('nullMethod returns null', () => {
expect(out.nullMethod()).toEqual(null);
});
test('emptyContent', () => {
expect(out.emptyContent).toEqual(formatMessage(messages.noResultsFound));
});
it('nullMethod returns null', () => {
render(<TestComponent />);
expect(hookResult.nullMethod()).toBeNull();
});
it('returns expected structure with grades and headings data', () => {
mockUseAllGrades.mockReturnValue(allGrades);
mockUseGetHeadings.mockReturnValue(headings);
render(<TestComponent />);
expect(hookResult.columns.length).toBe(headings.length);
expect(hookResult.columns[0].accessor).toEqual(headings[0]);
expect(hookResult.data.length).toBe(allGrades.length);
expect(hookResult.data[0]).toHaveProperty(Headings.username);
expect(hookResult.grades).toEqual(allGrades);
expect(hookResult.nullMethod()).toBeNull();
expect(hookResult.emptyContent).toBe(messages.noResultsFound.defaultMessage);
expect(mockUseAllGrades).toHaveBeenCalled();
expect(mockUseGetHeadings).toHaveBeenCalled();
});
});

View File

@@ -1,39 +1,42 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { DataTable } from '@openedx/paragon';
import { screen } from '@testing-library/react';
import useGradebookTableData from './hooks';
import GradebookTable from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('./hooks', () => jest.fn());
const hookProps = {
columns: ['some', 'columns'],
data: ['some', 'data'],
columns: [{ Header: 'Username', accessor: 'username' }, { Header: 'Email', accessor: 'email' }, { Header: 'Total Grade', accessor: 'totalGrade' }],
data: [{ username: 'instructor', email: 'instructor@example.com', totalGrade: '100' }, { username: 'student', email: 'student@example.com', totalGrade: '90' }],
grades: ['a', 'few', 'grades'],
nullMethod: jest.fn().mockName('hooks.nullMethod'),
emptyContent: 'empty-table-content',
};
useGradebookTableData.mockReturnValue(hookProps);
let el;
describe('GradebookTable', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<GradebookTable />);
it('renders Datatable correctly', () => {
useGradebookTableData.mockReturnValue(hookProps);
renderWithIntl(<GradebookTable />);
expect(useGradebookTableData).toHaveBeenCalled();
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(3);
expect(headers[0]).toHaveTextContent(hookProps.columns[0].Header);
expect(headers[1]).toHaveTextContent(hookProps.columns[1].Header);
expect(headers[2]).toHaveTextContent(hookProps.columns[2].Header);
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(3);
expect(screen.getByText(hookProps.data[0].username)).toBeInTheDocument();
expect(screen.getByText(hookProps.data[0].email)).toBeInTheDocument();
expect(screen.getByText(hookProps.data[0].totalGrade)).toBeInTheDocument();
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('Datatable props', () => {
const datatable = el.instance.findByType(DataTable)[0];
const { props } = datatable;
expect(props.columns).toEqual(hookProps.columns);
expect(props.data).toEqual(hookProps.data);
expect(props.itemCount).toEqual(hookProps.grades.length);
expect(props.RowStatusComponent).toEqual(hookProps.nullMethod);
expect(datatable.children[2].type).toEqual('DataTable.EmptyTable');
expect(datatable.children[2].props.content).toEqual(hookProps.emptyContent);
it('renders empty table content when no data is available', () => {
useGradebookTableData.mockReturnValue({
...hookProps,
data: [],
grades: [],
});
renderWithIntl(<GradebookTable />);
expect(screen.getByText(hookProps.emptyContent)).toBeInTheDocument();
});
});

View File

@@ -120,25 +120,25 @@ select#ScoreView.form-control {
&:before {
border-width: 0.4rem 0.4rem 0.4rem 0;
border-right-color: $black;
border-right-color: var(--pgn-color-black);
}
}
#edit-filters-btn {
@include media-breakpoint-down(xs) {
@media (--pgn-size-breakpoint-max-width-xs) {
width: 100%;
margin-bottom: 1rem;
}
}
.search-container {
@include media-breakpoint-down(xs) {
@media (--pgn-size-breakpoint-max-width-xs) {
width: 100%;
}
}
.pgn__modal-body-content .pgn__data-table-layout-wrapper {
@include media-breakpoint-down(sm) {
@media (--pgn-size-breakpoint-max-width-xs) {
clear: both;
padding: 1rem 0;
}

View File

@@ -1,53 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImportGradesButton component render Form 1`] = `
<Form
action="test-grade-export-url"
method="post"
>
<Form.Group
controlId="csv"
>
<Form.Control
className="d-none"
data-testid="file-control"
label="Upload Grade CSV"
onChange={[MockFunction props.handleFileInputChange]}
type="file"
/>
</Form.Group>
</Form>
`;
exports[`ImportGradesButton component render snapshot 1`] = `
<Fragment>
<Form
action="test-grade-export-url"
method="post"
>
<Form.Group
controlId="csv"
>
<Form.Control
className="d-none"
data-testid="file-control"
label="Upload Grade CSV"
onChange={[MockFunction props.handleFileInputChange]}
type="file"
/>
</Form.Group>
</Form>
<NetworkButton
className="import-grades-btn"
import={true}
label={
{
"defaultMessage": "Import Grades",
"description": "A labeled button to import grades in the BulkManagement Tab File Upload Form",
"id": "gradebook.GradesView.importGradesBtnText",
}
}
onClick={[MockFunction props.handleClickImportGrades]}
/>
</Fragment>
`;

View File

@@ -12,6 +12,11 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn((val) => ({ current: val, useRef: true })),
}));
let out;
let submitThen;

View File

@@ -1,46 +1,26 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import {
render, screen, initializeMocks,
} from 'testUtilsExtra';
import NetworkButton from 'components/NetworkButton';
import useImportGradesButtonData from './hooks';
import ImportGradesButton from '.';
jest.mock('components/NetworkButton', () => 'NetworkButton');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
initializeMocks();
let el;
let props;
describe('ImportGradesButton component', () => {
beforeAll(() => {
props = {
fileInputRef: { current: null },
gradeExportUrl: 'test-grade-export-url',
handleClickImportGrades: jest.fn().mockName('props.handleClickImportGrades'),
handleFileInputChange: jest.fn().mockName('props.handleFileInputChange'),
};
useImportGradesButtonData.mockReturnValue(props);
el = shallow(<ImportGradesButton />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useImportGradesButtonData).toHaveBeenCalledWith();
expect(useIntl).toHaveBeenCalledWith();
});
beforeEach(() => {
jest.clearAllMocks();
render(<ImportGradesButton />);
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('Form', () => {
expect(el.instance.findByType(Form)[0].snapshot).toMatchSnapshot();
expect(el.instance.findByType(Form)[0].props.action).toEqual(props.gradeExportUrl);
expect(el.instance.findByType(Form.Control)[0].props.onChange).toEqual(props.handleFileInputChange);
test('Form', async () => {
const uploader = screen.getByTestId('file-control');
expect(uploader).toBeInTheDocument();
});
test('import button', () => {
expect(el.instance.findByType(NetworkButton)[0].props.onClick).toEqual(props.handleClickImportGrades);
expect(screen.getByRole('button', { name: 'Import Grades' })).toBeInTheDocument();
});
});
});

View File

@@ -1,11 +1,8 @@
import React from 'react';
import { render } from '@testing-library/react';
import useImportGradesButtonData from './hooks';
import ImportGradesButton from '.';
import { renderWithIntl, screen } from '../../../testUtilsExtra';
jest.unmock('react');
jest.unmock('@openedx/paragon');
jest.mock('components/NetworkButton', () => 'network-button');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
@@ -17,11 +14,10 @@ const props = {
};
useImportGradesButtonData.mockReturnValue(props);
let el;
describe('ImportGradesButton ref test', () => {
it('loads ref from hook', () => {
el = render(<ImportGradesButton />);
const input = el.getByTestId('file-control');
renderWithIntl(<ImportGradesButton />);
const input = screen.getByTestId('file-control');
expect(input).toEqual(props.fileInputRef.current);
});
});

View File

@@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImportSuccessToast component render snapshot 1`] = `
<Toast
action="test-action"
onClose={[MockFunction hooks.onClose]}
show="test-show"
>
test-description
</Toast>
`;

View File

@@ -19,6 +19,18 @@ jest.mock('data/redux/hooks', () => ({
},
}));
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,
})),
}));
const setView = jest.fn().mockName('hooks.setView');
const setShowToast = jest.fn().mockName('hooks.setShowImportSuccessToast');
actions.app.useSetView.mockReturnValue(setView);

View File

@@ -1,39 +1,88 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import useImportSuccessToastData from './hooks';
import { render, initializeMocks, screen } from 'testUtilsExtra';
import ImportSuccessToast from '.';
import useImportSuccessToastData from './hooks';
jest.mock('data/redux/hooks', () => ({
actions: {
app: {
useSetView: jest.fn(),
useSetShowImportSuccessToast: jest.fn(),
},
},
selectors: {
app: {
useShowImportSuccessToast: jest.fn(),
},
},
}));
jest.mock('./hooks', () => jest.fn());
const hookProps = {
action: 'test-action',
onClose: jest.fn().mockName('hooks.onClose'),
show: 'test-show',
description: 'test-description',
};
useImportSuccessToastData.mockReturnValue(hookProps);
initializeMocks();
let el;
describe('ImportSuccessToast component', () => {
beforeAll(() => {
el = shallow(<ImportSuccessToast />);
describe('ImportSuccessToast', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
it('initializes component hook', () => {
expect(useImportSuccessToastData).toHaveBeenCalled();
it('renders with show false', () => {
useImportSuccessToastData.mockReturnValue({
action: {
label: 'View Activity Log',
onClick: jest.fn(),
},
onClose: jest.fn(),
show: false,
description: 'Import Successful! Grades will be updated momentarily.',
});
render(<ImportSuccessToast />);
const toastRoot = document.getElementById('toast-root');
expect(toastRoot).toBeInTheDocument();
expect(toastRoot).toHaveClass('toast-container');
const toastMessage = screen.queryByText('Import Successful! Grades will be updated momentarily.');
expect(toastMessage).toBeNull();
expect(useImportSuccessToastData).toHaveBeenCalled();
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
it('renders with show true', () => {
useImportSuccessToastData.mockReturnValue({
action: {
label: 'View Activity Log',
onClick: jest.fn(),
},
onClose: jest.fn(),
show: true,
description: 'Import Successful! Grades will be updated momentarily.',
});
test('Toast', () => {
expect(el.instance.type).toEqual('Toast');
expect(el.instance.props.action).toEqual(hookProps.action);
expect(el.instance.props.onClose).toEqual(hookProps.onClose);
expect(el.instance.props.show).toEqual(hookProps.show);
expect(el.instance.children[0].el).toEqual(hookProps.description);
render(<ImportSuccessToast />);
const toastMessage = screen.getByText('Import Successful! Grades will be updated momentarily.');
expect(toastMessage).toBeInTheDocument();
expect(useImportSuccessToastData).toHaveBeenCalled();
});
it('passes correct props to Toast component', () => {
const mockOnClose = jest.fn();
const mockOnClick = jest.fn();
useImportSuccessToastData.mockReturnValue({
action: {
label: 'View Activity Log',
onClick: mockOnClick,
},
onClose: mockOnClose,
show: true,
description: 'Import Successful! Grades will be updated momentarily.',
});
const { container } = render(<ImportSuccessToast />);
expect(container).toBeInTheDocument();
expect(useImportSuccessToastData).toHaveBeenCalled();
});
});

View File

@@ -1,30 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InterventionsReport component output snapshot 1`] = `
<div>
<h4
className="mt-0"
>
Interventions Report
</h4>
<div
className="d-flex justify-content-between align-items-center"
>
<div
className="intervention-report-description"
>
Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.
</div>
<NetworkButton
label={
{
"defaultMessage": "Download Interventions",
"description": "The labeled button to download the Intervention report from the Grades View",
"id": "gradebook.GradesView.InterventionsReport.downloadBtn",
}
}
onClick={[MockFunction]}
/>
</div>
</div>
`;

View File

@@ -1,42 +1,73 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import NetworkButton from 'components/NetworkButton';
import messages from './messages';
import useInterventionsReportData from './hooks';
import InterventionsReport from '.';
jest.mock('components/NetworkButton', () => 'NetworkButton');
jest.mock('./hooks', () => jest.fn());
const hookProps = { show: true, handleClick: jest.fn() };
useInterventionsReportData.mockReturnValue(hookProps);
const useInterventionsReportData = require('./hooks');
let el;
describe('InterventionsReport component', () => {
initializeMocks();
describe('InterventionsReport', () => {
beforeEach(() => {
el = shallow(<InterventionsReport />);
jest.clearAllMocks();
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useInterventionsReportData).toHaveBeenCalledWith();
expect(useIntl).toHaveBeenCalledWith();
it('renders without errors when show is true', () => {
useInterventionsReportData.mockReturnValue({
show: true,
handleClick: jest.fn(),
});
render(<InterventionsReport />);
expect(screen.getByRole('heading', { level: 4 })).toBeInTheDocument();
expect(useInterventionsReportData).toHaveBeenCalled();
});
describe('output', () => {
it('does now render if show is false', () => {
useInterventionsReportData.mockReturnValueOnce({ ...hookProps, show: false });
el = shallow(<InterventionsReport />);
expect(el.isEmptyRender()).toEqual(true);
it('renders nothing when show is false', () => {
useInterventionsReportData.mockReturnValue({
show: false,
handleClick: jest.fn(),
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
const btnProps = el.instance.findByType(NetworkButton)[0].props;
expect(btnProps.label).toEqual(messages.downloadBtn);
expect(btnProps.onClick).toEqual(hookProps.handleClick);
render(<InterventionsReport />);
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
expect(screen.queryByText('Interventions Report')).not.toBeInTheDocument();
expect(useInterventionsReportData).toHaveBeenCalled();
});
it('calls useInterventionsReportData hook', () => {
useInterventionsReportData.mockReturnValue({
show: true,
handleClick: jest.fn(),
});
render(<InterventionsReport />);
expect(useInterventionsReportData).toHaveBeenCalled();
});
it('renders with correct content when show is true', () => {
const mockReportData = {
show: true,
handleClick: jest.fn(),
};
useInterventionsReportData.mockReturnValue(mockReportData);
render(<InterventionsReport />);
expect(screen.getByText('Interventions Report')).toBeInTheDocument();
expect(
screen.getByText(/Need to find students who may be falling behind/),
).toBeInTheDocument();
const networkButton = document.querySelector('networkbutton');
expect(networkButton).toBeInTheDocument();
expect(useInterventionsReportData).toHaveBeenCalled();
});
});

View File

@@ -1,37 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PageButtons component render snapshot 1`] = `
<div
className="d-flex justify-content-center"
style={
{
"paddingBottom": "20px",
}
}
>
<Button
disabled="prev-disabled"
onClick={[MockFunction hooks.prev.onClick]}
style={
{
"margin": "20px",
}
}
variant="outline-primary"
>
prev-text
</Button>
<Button
disabled="next-disabled"
onClick={[MockFunction hooks.next.onClick]}
style={
{
"margin": "20px",
}
}
variant="outline-primary"
>
next-text
</Button>
</div>
`;

View File

@@ -15,6 +15,18 @@ jest.mock('data/redux/hooks', () => ({
},
}));
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,
})),
}));
const gradeData = { nextPage: 'test-next-page', prevPage: 'test-prev-page' };
selectors.grades.useGradeData.mockReturnValue(gradeData);

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Button } from '@openedx/paragon';
import { initializeMocks, render, screen } from 'testUtilsExtra';
import usePageButtonsData from './hooks';
import PageButtons from '.';
@@ -10,44 +9,53 @@ jest.mock('./hooks', () => jest.fn());
const hookProps = {
prev: {
disabled: 'prev-disabled',
disabled: false,
onClick: jest.fn().mockName('hooks.prev.onClick'),
text: 'prev-text',
},
next: {
disabled: 'next-disabled',
disabled: false,
onClick: jest.fn().mockName('hooks.next.onClick'),
text: 'next-text',
},
};
usePageButtonsData.mockReturnValue(hookProps);
let el;
describe('PageButtons component', () => {
beforeEach(() => {
beforeAll(() => {
jest.clearAllMocks();
el = shallow(<PageButtons />);
initializeMocks();
});
describe('behavior', () => {
it('initializes component hooks', () => {
expect(usePageButtonsData).toHaveBeenCalled();
describe('renders enabled buttons', () => {
beforeEach(() => {
usePageButtonsData.mockReturnValue(hookProps);
render(<PageButtons />);
});
test('prev button enabled', () => {
expect(screen.getByText(hookProps.prev.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.next.text)).toBeEnabled();
});
test('next button enabled', () => {
expect(screen.getByText(hookProps.next.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.prev.text)).toBeEnabled();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
describe('renders disabled buttons', () => {
beforeAll(() => {
hookProps.prev.disabled = true;
hookProps.next.disabled = true;
});
test('prev button', () => {
const button = el.instance.findByType(Button)[0];
expect(button.props.disabled).toEqual(hookProps.prev.disabled);
expect(button.props.onClick).toEqual(hookProps.prev.onClick);
expect(button.children[0].el).toEqual(hookProps.prev.text);
beforeEach(() => {
usePageButtonsData.mockReturnValue(hookProps);
render(<PageButtons />);
});
test('next button', () => {
const button = el.instance.findByType(Button)[1];
expect(button.props.disabled).toEqual(hookProps.next.disabled);
expect(button.props.onClick).toEqual(hookProps.next.onClick);
expect(button.children[0].el).toEqual(hookProps.next.text);
test('prev button disabled', () => {
expect(screen.getByText(hookProps.next.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.prev.text)).toBeDisabled();
});
test('next button disabled', () => {
expect(screen.getByText(hookProps.prev.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.next.text)).toBeDisabled();
});
});
});

View File

@@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScoreViewInput component render snapshot 1`] = `
<Form.Group
controlId="ScoreView"
>
<Form.Label>
Score View
:
</Form.Label>
<Form.Control
as="select"
onChange={[MockFunction hooks.toggleGradeFormat]}
value="test-grade-format"
>
<option
value="percent"
>
Percent
</option>
<option
value="absolute"
>
Absolute
</option>
</Form.Control>
</Form.Group>
`;

View File

@@ -1,67 +1,237 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { GradeFormats } from 'data/constants/grades';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import userEvent from '@testing-library/user-event';
import { formatMessage } from 'testUtils';
import { actions, selectors } from 'data/redux/hooks';
import ScoreViewInput from '.';
import messages from './messages';
import { ScoreViewInput } from '.';
jest.mock('data/redux/hooks', () => ({
actions: {
grades: { useToggleGradeFormat: jest.fn() },
grades: {
useToggleGradeFormat: jest.fn(),
},
},
selectors: {
grades: { useGradeData: jest.fn() },
grades: {
useGradeData: jest.fn(),
},
},
}));
const toggleGradeFormat = jest.fn().mockName('hooks.toggleGradeFormat');
actions.grades.useToggleGradeFormat.mockReturnValue(toggleGradeFormat);
const gradeFormat = 'test-grade-format';
selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
const { actions, selectors } = require('data/redux/hooks');
initializeMocks();
describe('ScoreViewInput', () => {
const mockToggleFormat = jest.fn();
let el;
describe('ScoreViewInput component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<ScoreViewInput />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'percent',
});
it('initializes redux hooks', () => {
expect(actions.grades.useToggleGradeFormat).toHaveBeenCalled();
expect(selectors.grades.useGradeData).toHaveBeenCalled();
actions.grades.useToggleGradeFormat.mockReturnValue(mockToggleFormat);
});
it('renders without errors', () => {
render(<ScoreViewInput />);
expect(document.body).toBeInTheDocument();
});
it('renders form group with correct label', () => {
render(<ScoreViewInput />);
expect(screen.getByLabelText(/score view/i)).toBeInTheDocument();
});
it('renders select element with correct options', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toBeInTheDocument();
expect(
screen.getByRole('option', { name: /percent/i }),
).toBeInTheDocument();
expect(
screen.getByRole('option', { name: /absolute/i }),
).toBeInTheDocument();
});
it('displays correct selected value for percent format', () => {
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'percent',
});
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('percent');
});
it('displays correct selected value for absolute format', () => {
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'absolute',
});
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('absolute');
});
it('calls toggle function when selection changes', async () => {
render(<ScoreViewInput />);
const user = userEvent.setup();
const select = screen.getByRole('combobox', { name: /score view/i });
await user.selectOptions(select, 'absolute');
expect(mockToggleFormat).toHaveBeenCalledTimes(1);
});
describe('accessibility', () => {
it('has proper form structure', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
const label = screen.getByText(/score view/i);
expect(select).toBeInTheDocument();
expect(label).toBeInTheDocument();
});
it('has accessible label association', () => {
render(<ScoreViewInput />);
const label = screen.getByText(/score view/i);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(label).toBeInTheDocument();
expect(select).toHaveAccessibleName(/score view/i);
});
it('has correct control ID for accessibility', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveAttribute('id', 'ScoreView');
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
describe('form control behavior', () => {
it('renders as a select element', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select.tagName).toBe('SELECT');
});
test('label', () => {
const label = el.instance.children[0];
expect(label.children[0].el).toEqual(`${formatMessage(messages.scoreView)}`);
it('has correct option values', () => {
render(<ScoreViewInput />);
const percentOption = screen.getByRole('option', { name: /percent/i });
const absoluteOption = screen.getByRole('option', { name: /absolute/i });
expect(percentOption).toHaveValue('percent');
expect(absoluteOption).toHaveValue('absolute');
});
describe('form control', () => {
let control;
beforeEach(() => {
control = el.instance.children;
});
test('value and onChange from redux hooks', () => {
expect(control[1].props.value).toEqual(gradeFormat);
expect(control[1].props.onChange).toEqual(toggleGradeFormat);
});
test('absolute and percent options', () => {
const { children } = control[1];
expect(children[0].props.value).toEqual(GradeFormats.percent);
expect(children[0].children[0].el).toEqual(formatMessage(messages.percent));
expect(children[1].props.value).toEqual(GradeFormats.absolute);
expect(children[1].children[0].el).toEqual(formatMessage(messages.absolute));
it('has exactly two options', () => {
render(<ScoreViewInput />);
const options = screen.getAllByRole('option');
expect(options).toHaveLength(2);
});
});
describe('redux integration', () => {
it('uses grade data selector hook', () => {
render(<ScoreViewInput />);
expect(selectors.grades.useGradeData).toHaveBeenCalledTimes(1);
});
it('uses toggle grade format action hook', () => {
render(<ScoreViewInput />);
expect(actions.grades.useToggleGradeFormat).toHaveBeenCalledTimes(1);
});
it('responds to different grade format values', () => {
const { rerender } = render(<ScoreViewInput />);
let select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('percent');
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'absolute',
});
rerender(<ScoreViewInput />);
select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('absolute');
});
});
describe('user interactions', () => {
it('handles option selection', async () => {
render(<ScoreViewInput />);
const user = userEvent.setup();
const select = screen.getByRole('combobox', { name: /score view/i });
await user.selectOptions(select, 'absolute');
expect(mockToggleFormat).toHaveBeenCalledWith(expect.any(Object));
});
it('maintains state consistency', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
const percentOption = screen.getByRole('option', { name: /percent/i });
expect(select).toHaveValue('percent');
expect(percentOption).toBeInTheDocument();
});
});
describe('internationalization', () => {
it('displays localized label text', () => {
render(<ScoreViewInput />);
expect(screen.getByText('Score View:')).toBeInTheDocument();
});
it('displays localized option text', () => {
render(<ScoreViewInput />);
expect(screen.getByText('Percent')).toBeInTheDocument();
expect(screen.getByText('Absolute')).toBeInTheDocument();
});
});
describe('component structure', () => {
it('uses proper Bootstrap form classes', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveClass('form-control');
});
it('renders within form group structure', () => {
render(<ScoreViewInput />);
const label = screen.getByText(/score view/i);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(label).toBeInTheDocument();
expect(select).toBeInTheDocument();
expect(select).toHaveAccessibleName(expect.stringMatching(/score view/i));
});
});
});

View File

@@ -1,20 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SearchControls component render snapshot 1`] = `
<div
className="search-container"
>
<SearchField
inputLabel="test-input-label"
onBlur={[MockFunction hooks.onBlur]}
onClear={[MockFunction hooks.onClear]}
onSubmit={[MockFunction hooks.onSubmit]}
value="test-search-value"
/>
<small
className="form-text text-muted search-help-text"
>
test-hint-text
</small>
</div>
`;

View File

@@ -18,6 +18,18 @@ jest.mock('data/redux/hooks', () => ({
},
}));
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,
})),
}));
const searchValue = 'test-search-value';
selectors.app.useSearchValue.mockReturnValue(searchValue);
const setSearchValue = jest.fn();

View File

@@ -21,7 +21,7 @@ export const SearchControls = () => {
<div className="search-container">
<SearchField
onSubmit={onSubmit}
inputLabel={inputLabel}
label={inputLabel}
onBlur={onBlur}
onClear={onClear}
value={searchValue}

View File

@@ -1,7 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { SearchField } from '@openedx/paragon';
import { initializeMocks, render, screen } from 'testUtilsExtra';
import useSearchControlsData from './hooks';
import SearchControls from '.';
@@ -17,32 +15,19 @@ const hookProps = {
hintText: 'test-hint-text',
};
useSearchControlsData.mockReturnValue(hookProps);
let el;
describe('SearchControls component', () => {
beforeEach(() => {
initializeMocks();
render(<SearchControls />);
jest.clearAllMocks();
el = shallow(<SearchControls />);
});
describe('behavior', () => {
it('initializes component hooks', () => {
expect(useSearchControlsData).toHaveBeenCalled();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('search field', () => {
const { props } = el.instance.findByType(SearchField)[0];
expect(props.onSubmit).toEqual(hookProps.onSubmit);
expect(props.onBlur).toEqual(hookProps.onBlur);
expect(props.onClear).toEqual(hookProps.onClear);
expect(props.inputLabel).toEqual(hookProps.inputLabel);
expect(props.value).toEqual(hookProps.searchValue);
expect(screen.getByLabelText(hookProps.inputLabel)).toBeInTheDocument();
expect(screen.getByRole('searchbox')).toHaveValue(hookProps.searchValue);
});
test('hint text', () => {
expect(el.instance.findByType('small')[0].children[0].el).toEqual(hookProps.hintText);
expect(screen.getByText(hookProps.hintText)).toBeInTheDocument();
});
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render } from '@testing-library/react';
import { selectors } from 'data/redux/hooks';
import SpinnerIcon from './SpinnerIcon';
@@ -10,26 +10,19 @@ jest.mock('data/redux/hooks', () => ({
},
}));
selectors.root.useShouldShowSpinner.mockReturnValue(true);
let el;
describe('SpinnerIcon', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<SpinnerIcon />);
});
describe('behavior', () => {
it('initializes redux hook', () => {
expect(selectors.root.useShouldShowSpinner).toHaveBeenCalled();
});
it('does not render if show: false', () => {
selectors.root.useShouldShowSpinner.mockReturnValueOnce(false);
const { container } = render(<SpinnerIcon />);
expect(container.querySelector('.fa.fa-spinner')).not.toBeInTheDocument();
});
describe('component', () => {
it('does not render if show: false', () => {
selectors.root.useShouldShowSpinner.mockReturnValueOnce(false);
el = shallow(<SpinnerIcon />);
expect(el.isEmptyRender()).toEqual(true);
});
test('snapshot - displays spinner overlay with spinner icon', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('displays spinner overlay with spinner icon', () => {
selectors.root.useShouldShowSpinner.mockReturnValueOnce(true);
const { container } = render(<SpinnerIcon />);
expect(container.querySelector('.fa.fa-spinner')).toBeInTheDocument();
});
});

View File

@@ -1,20 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatusAlerts component render snapshot 1`] = `
<Fragment>
<Alert
onClose={[MockFunction hooks.successBanner.onClose]}
show="hooks.show-success-banner"
variant="success"
>
hooks.success-banner-text
</Alert>
<Alert
dismissible={false}
show="hooks.show-grade-filter"
variant="danger"
>
hooks.grade-filter-text
</Alert>
</Fragment>
`;

View File

@@ -1,6 +1,5 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { actions, selectors } from 'data/redux/hooks';
import useStatusAlertsData from './hooks';
@@ -16,6 +15,18 @@ jest.mock('data/redux/hooks', () => ({
},
}));
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,
})),
}));
const validity = {
isMinValid: true,
isMaxValid: true,
@@ -49,7 +60,7 @@ describe('useStatusAlertsData', () => {
expect(out.successBanner.show).toEqual(showSuccess);
});
test('message', () => {
expect(out.successBanner.text).toEqual(formatMessage(messages.editSuccessAlert));
expect(out.successBanner.text).toEqual(messages.editSuccessAlert.defaultMessage);
});
});
describe('gradeFilter', () => {
@@ -70,7 +81,7 @@ describe('useStatusAlertsData', () => {
expect(out.gradeFilter.show).toEqual(true);
});
test('filter message', () => {
expect(out.gradeFilter.text).toEqual(formatMessage(messages.minGradeInvalid));
expect(out.gradeFilter.text).toEqual(messages.minGradeInvalid.defaultMessage);
});
});
describe('max filter is invalid', () => {
@@ -85,7 +96,7 @@ describe('useStatusAlertsData', () => {
expect(out.gradeFilter.show).toEqual(true);
});
test('filter message', () => {
expect(out.gradeFilter.text).toEqual(formatMessage(messages.maxGradeInvalid));
expect(out.gradeFilter.text).toEqual(messages.maxGradeInvalid.defaultMessage);
});
});
describe('both filters are invalid', () => {
@@ -101,7 +112,7 @@ describe('useStatusAlertsData', () => {
});
test('filter message', () => {
expect(out.gradeFilter.text).toEqual(
`${formatMessage(messages.minGradeInvalid)}${formatMessage(messages.maxGradeInvalid)}`,
`${messages.minGradeInvalid.defaultMessage}${messages.maxGradeInvalid.defaultMessage}`,
);
});
});

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