Compare commits

...

76 Commits

Author SHA1 Message Date
dependabot[bot]
9ef87e682d build(deps): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 12:37:37 +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
Brian Smith
6d67a807a4 feat: import FooterSlot from component package instead of slot package (#450) 2025-04-24 12:36:42 -04:00
Brian Smith
490e3ea67e fix(deps): update frontend-component-header to ^6.4.0 (#451) 2025-04-23 17:05:45 -04:00
edX requirements bot
77c65b469f chore: update browserslist DB (#448)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-04-21 00:28:39 +00:00
Sarina Canelake
645a5447e7 Merge pull request #437 from sarina/update-rtd-links
Update README with Tutor instructions; update edx.rtd links
2025-04-18 09:55:14 -04:00
sarina
0cfb9a5f85 docs: Update Devstack instructions to Tutor 2025-04-16 16:53:05 -04:00
sarina
90d011e45e docs: Update migrated edx.rtd links to docs.openedx.org 2025-04-16 16:52:02 -04:00
sarina
cecfbf1830 docs: Convert rst syntax to md in README 2025-04-16 16:52:02 -04:00
Adolfo R. Brandes
46d59027b5 Merge pull request #442 from regisb/regisb/no-husky
chore: remove husky 🪓🐶
2025-04-14 14:12:28 -03:00
Régis Behmo
3b3384b6b5 chore: remove husky 🪓🐶
We remove husky, which is triggering pre-push git hooks, including
running "npm lint". This is causing failures when building Docker
images, because "npm clean-install --omit=dev" automatically triggers "npm
prepare", which attemps to run "husky". But husky is not listed in the
build dependencies, only in devDependencies. As a consequence, package
installation is failing with the following error:

        14.13 > @edx/frontend-app-ora-grading@0.0.1 prepare
        14.13 > husky install
        14.13
        14.15 sh: 1: husky: not found

Similar to: https://github.com/openedx/frontend-app-learning/pull/1622
2025-04-14 18:57:51 +02:00
edX requirements bot
4210e8a7f6 chore: update browserslist DB (#447)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-04-14 00:28:11 +00:00
Brian Smith
6d4c5e7702 feat: upgrade to react 18 (#445) 2025-04-09 15:08:24 -04:00
edX requirements bot
dd3b128904 chore: update browserslist DB (#443)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-04-07 00:27:13 +00:00
Brian Smith
f720e0a849 chore(deps): update @openedx dependencies to versions that support React 18 (#441) 2025-03-27 16:16:26 -04:00
edX requirements bot
8f7eb57dfd chore: update browserslist DB (#440)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-03-24 00:27:10 +00:00
edX requirements bot
49da0175e8 chore: update browserslist DB (#435)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-03-17 00:27:04 +00:00
edX requirements bot
6db15693d6 chore: update browserslist DB (#434)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-03-10 00:20:23 +00:00
edX requirements bot
9859fa1a45 chore: update browserslist DB (#432)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-03-03 00:24:28 +00:00
jansenk
b2ebc800ad chore: update browserslist DB 2025-02-24 20:51:40 +05:30
Feanil Patel
97eded4432 Merge pull request #428 from salman2013/salman/update-catalog-info-file
Update catalog-info file for release data
2025-02-05 09:19:27 -05:00
edX requirements bot
ae94cae1ef chore: update browserslist DB (#429)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-02-02 19:23:02 -05:00
salman2013
0448860bca chore: update catalog-info file for release data 2025-01-31 16:16:27 +05:00
jansenk
d07312866b chore: update browserslist DB 2025-01-26 21:35:46 +05:30
dependabot[bot]
e458d6fb05 build(deps): bump http-proxy-middleware from 2.0.6 to 2.0.7
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.7)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 16:37:56 +05:30
dependabot[bot]
515307c923 build(deps): bump codecov/codecov-action from 4 to 5
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 16:30:10 +05:30
dependabot[bot]
e57cab41e5 build(deps): bump dawidd6/action-send-mail from 3 to 4
Bumps [dawidd6/action-send-mail](https://github.com/dawidd6/action-send-mail) from 3 to 4.
- [Release notes](https://github.com/dawidd6/action-send-mail/releases)
- [Commits](https://github.com/dawidd6/action-send-mail/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 16:29:19 +05:30
edX requirements bot
957991d472 chore: update browserslist DB (#420)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-01-11 16:22:42 +05:30
dependabot[bot]
f72faa824c build(deps-dev): bump cross-spawn from 6.0.5 to 6.0.6
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 6.0.5 to 6.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/v6.0.6/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v6.0.5...v6.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 16:21:45 +05:30
dependabot[bot]
6d21cbb616 build(deps): bump path-to-regexp, express and fetch-mock
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.12 and updates ancestor dependencies [path-to-regexp](https://github.com/pillarjs/path-to-regexp), [express](https://github.com/expressjs/express) and [fetch-mock](https://github.com/wheresrhys/fetch-mock/tree/HEAD/packages/fetch-mock). These dependencies need to be updated together.


Updates `path-to-regexp` from 2.4.0 to 0.1.12
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v2.4.0...v0.1.12)

Updates `express` from 4.21.0 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.0...4.21.2)

Updates `fetch-mock` from 6.5.2 to 12.2.0
- [Release notes](https://github.com/wheresrhys/fetch-mock/releases)
- [Changelog](https://github.com/wheresrhys/fetch-mock/blob/main/packages/fetch-mock/CHANGELOG.md)
- [Commits](https://github.com/wheresrhys/fetch-mock/commits/fetch-mock-v12.2.0/packages/fetch-mock)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
- dependency-name: fetch-mock
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 16:19:48 +05:30
dependabot[bot]
de445a97be build(deps): bump nanoid from 3.3.7 to 3.3.8
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 16:18:17 +05:30
Farhaan Bukhsh
570e843fd4 chore: Removes unwanted dependency.
Signed-off-by: Farhaan Bukhsh <farhaan@opencraft.com>
2024-12-04 21:27:04 +05:30
edX requirements bot
e803c818ab chore: update browserslist DB (#416)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2024-11-13 14:25:40 +05:30
dependabot[bot]
7a0483a896 build(deps): bump actions/checkout from 2 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-06 16:16:27 +05:30
jansenk
201584a7ea chore: update browserslist DB 2024-11-06 15:28:03 +05:30
Emad Rad
c222bec9ec chore: npm publish action removed (#415)
* chore: npm publish action removed

* chore: semantic release removed
2024-11-06 15:19:46 +05:30
Bilal Qamar
215662ba16 test: Remove support for Node 18 (#408) 2024-10-31 14:45:11 -04:00
edX requirements bot
cf5e1a65bf chore: enable github action auto update in dependabot.yml 2024-10-28 16:10:09 +05:30
126 changed files with 10311 additions and 13241 deletions

2
.env
View File

@@ -34,3 +34,5 @@ ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
APP_ID='' APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''
DISPLAY_FEEDBACK_WIDGET='true' 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='' APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''
DISPLAY_FEEDBACK_WIDGET='false' 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

7
.github/dependabot.yml vendored Normal file
View File

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

View File

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

View File

@@ -1,36 +0,0 @@
name: Release CI
on:
push:
tags:
- '*'
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
run: npm ci
- name: Create Build
run: npm run build
- name: Release Package
env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
run: npm semantic-release

2
.nvmrc
View File

@@ -1 +1 @@
20 24

View File

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

102
README.md
View File

@@ -18,8 +18,8 @@ Jump to:
For existing documentation see: For existing documentation see:
- Basic Usage: [Review Learner Grades (read-the-docs)](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/student_progress/course_grades.html#review-learner-grades-on-the-instructor-dashboard) - 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://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/student_progress/course_grades.html#override-learner-subsection-scores-in-bulk) - 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? ## Should I use Gradebook in my course?
@@ -58,56 +58,53 @@ To install gradebook into your project:
``` ```
npm i --save @edx/frontend-app-gradebook npm i --save @edx/frontend-app-gradebook
``` ```
Cloning and Startup Cloning and Startup
=================== ===================
1. Clone your new repo: 1. Clone your new repo:
``git clone https://github.com/openedx/frontend-app-gradebook.git`` ``git clone https://github.com/openedx/frontend-app-gradebook.git``
2. Install npm dependencies: 2. Use the version of Node specified in ``.nvmrc``
``cd frontend-app-gradebook && npm install`` 3. Stop the Tutor devstack, if it's running:
3. Start the dev server: ``tutor dev stop``
``npm start`` 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 ## Running the UI Standalone
To install the project please refer to the [`edX Developer Stack`](https://github.com/openedx/devstack) instructions. 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.
The web application runs on port **1994**, so when you go to `http://localhost:1994/course-v1:edX+DemoX+Demo_Course` you should see the UI (assuming you have such a Demo Course in your devstack). Note that you always have to provide a course id to actually see a gradebook. When not mounted, gradebook will run in the shared MFE container at http://apps.local.openedx.io/gradebook/course-v1:edX+DemoX+Demo_Course.
If you don't, you can see the log messages for the docker container by executing `make gradebook-logs` in the `devstack` directory. 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. 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.
## Configuring for local use in edx-platform
Assuming you've got the UI running at `http://localhost:1994`, you can configure the LMS in edx-platform
to point to your local gradebook from the instructor dashboard by putting this setting in `lms/env/private.py`:
```
WRITABLE_GRADEBOOK_URL = 'http://localhost:1994'
```
There are also several edx-platform waffle and feature flags you'll have to enable from the Django admin:
1. Grades > Persistent grades enabled flag. Add this flag if it doesn't exist,
check the ``enabled`` and ``enabled for all courses`` boxes.
2. Waffle > Switches. Add the ``grades.assume_zero_grade_if_absent`` switch and make it active.
3. Waffle_utils > Waffle flag course overrides. Activate waffle flags for courses where you want to enable Gradebook functionality:
- Enable Gradebook by adding the ``grades.writable_gradebook`` add checking the ``enabled`` box.
- Enable Bulk Grade Management by adding the ``grades.bulk_management`` flag and checking the ``enabled`` box.
Alternatively, you could add these as regular waffle flags to enable the functionality for all courses.
**NOTE:** IF the above flags are not configured correctly, the gradebook may appear to work, but will return bogus
numbers for grades. If your gradebook isn't accepting your changes, or the changes aren't resulting in sane,
recalculated grade values, verify you've set all flags correctly.
## Plugins ## Plugins
This MFE can be customized using [Frontend Plugin Framework](https://github.com/openedx/frontend-plugin-framework). This MFE can be customized using [Frontend Plugin Framework](https://github.com/openedx/frontend-plugin-framework).
@@ -115,10 +112,13 @@ The parts of this MFE that can be customized in that manner are documented [here
## Running tests ## Running tests
1. Assuming that you're operating in the context of the edX devstack, Run:
run `gradebook-shell` from your devstack directory. This will start a bash shell inside your
running gradebook container. ``nvm use``
2. Run `make test` (which executes `npm run test`). This will run all of the gradebook tests.
``npm ci``
``npm test``
## Directory Structure ## Directory Structure
@@ -151,9 +151,7 @@ noted.
Contributing Contributing
============ ============
Contributions are very welcome. Please read `How To Contribute`_ for details. Contributions are very welcome. Please read [How To Contribute](https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html) for details.
.. _How To Contribute: https://openedx.org/r/how-to-contribute
This project is currently accepting all types of contributions, bug fixes, This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure security fixes, maintenance work, or new features. However, please make sure
@@ -168,29 +166,23 @@ Getting Help
If you're having trouble, we have discussion forums at If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community. 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 Our real-time conversations are on Slack. You can request a [Slack
invitation`_, then join our `community Slack workspace`_. Because this is a invitation](https://openedx.org/slack), then join our
frontend repository, the best place to discuss it would be in the `#wg-frontend [community Slack workspace](https://openedx.slack.com/) Because this is a
channel`_. 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 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. with as many details about the issue you are facing as you can provide.
https://github.com/openedx/frontend-app-gradebook/issues https://github.com/openedx/frontend-app-gradebook/issues
For more information about these options, see the `Getting Help`_ page. For more information about these options, see the [Getting Help](https://openedx.org/community/connect) 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 The Open edX Code of Conduct
============================ ============================
All community members are expected to follow 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/).
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
Reporting Security Issues Reporting Security Issues
========================= =========================

View File

@@ -7,6 +7,7 @@ metadata:
description: "The frontend (MFE) for Open edX Gradebook" description: "The frontend (MFE) for Open edX Gradebook"
annotations: annotations:
openedx.org/arch-interest-groups: "" openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec: spec:
owner: user:farhaanbukhsh owner: user:farhaanbukhsh
type: 'website' type: 'website'

View File

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

View File

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

View File

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

16869
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", "name": "@edx/frontend-app-gradebook",
"version": "1.6.2", "version": "1.6.3",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections", "description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -15,6 +15,7 @@
"prepush": "npm run lint", "prepush": "npm run lint",
"semantic-release": "semantic-release", "semantic-release": "semantic-release",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/gradebook/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests", "test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
"watch-tests": "jest --watch" "watch-tests": "jest --watch"
}, },
@@ -29,18 +30,16 @@
], ],
"dependencies": { "dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0", "@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-platform": "8.0.0", "@edx/frontend-component-header": "^6.6.1",
"@edx/frontend-platform": "^8.3.7",
"@edx/openedx-atlas": "^0.6.0", "@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "^3.0.0",
"@openedx/frontend-plugin-framework": "^1.2.1",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.1.1",
"@edx/reactifex": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2", "@fortawesome/free-brands-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.5", "@fortawesome/react-fontawesome": "^0.1.5",
"@openedx/frontend-plugin-framework": "^1.6.0",
"@openedx/paragon": "^23.4.5",
"@redux-beacon/segment": "^1.0.0", "@redux-beacon/segment": "^1.0.0",
"@reduxjs/toolkit": "^1.5.1", "@reduxjs/toolkit": "^1.5.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
@@ -50,8 +49,8 @@
"history": "4.10.1", "history": "4.10.1",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "6.13.0", "query-string": "6.13.0",
"react": "17.0.2", "react": "^18.3.1",
"react-dom": "17.0.2", "react-dom": "^18.3.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-redux": "^7.2.9", "react-redux": "^7.2.9",
"react-router": "6.15.0", "react-router": "6.15.0",
@@ -68,19 +67,16 @@
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "^1.1.1", "@edx/browserslist-config": "^1.1.1",
"@openedx/frontend-build": "14.0.3", "@openedx/frontend-build": "^14.6.2",
"@testing-library/react": "12.1.5", "@testing-library/jest-dom": "^6.6.4",
"axios": "0.21.2", "@testing-library/react": "^16.3.0",
"axios-mock-adapter": "^1.17.0", "@testing-library/user-event": "^14.6.1",
"es-check": "^2.3.0", "es-check": "^2.3.0",
"fetch-mock": "^6.5.2", "fetch-mock": "^12.2.0",
"husky": "2.7.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"react-dev-utils": "^12.0.1", "react-dev-utils": "^12.0.1",
"react-test-renderer": "17.0.2", "react-test-renderer": "^18.3.1",
"reactifex": "1.1.1", "redux-mock-store": "^1.5.3"
"redux-mock-store": "^1.5.3",
"semantic-release": "^19.0.3"
} }
} }

View File

@@ -3,7 +3,7 @@ import { Route, Routes } from 'react-router-dom';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
import FooterSlot from '@openedx/frontend-slot-footer'; import { FooterSlot } from '@edx/frontend-component-footer';
import Header from '@edx/frontend-component-header'; import Header from '@edx/frontend-component-header';
import store from 'data/store'; import store from 'data/store';

View File

@@ -1,13 +1,10 @@
// frontend-app-*/src/index.scss // frontend-app-*/src/index.scss
@import "~@edx/brand/paragon/fonts"; @use "@openedx/paragon/styles/css/core/custom-media-breakpoints.css" as paragonCustomMediaBreakpoints;
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
$fa-font-path: "~font-awesome/fonts"; $fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome"; @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-header/dist/index";
@import "~@edx/frontend-component-footer/dist/_footer"; @import "~@edx/frontend-component-footer/dist/_footer";

View File

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

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

View File

@@ -1,108 +1,187 @@
/* eslint-disable import/no-named-as-default */
import React from 'react'; import React from 'react';
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen, initializeMocks } from 'testUtilsExtra';
import { DataTable } from '@openedx/paragon'; import { DataTable } from '@openedx/paragon';
import selectors from 'data/selectors'; import selectors from 'data/selectors';
import { bulkManagementColumns } from 'data/constants/app'; import { bulkManagementColumns } from 'data/constants/app';
import { HistoryTable, mapHistoryRows, mapStateToProps } from './HistoryTable';
import ResultsSummary from './ResultsSummary'; import ResultsSummary from './ResultsSummary';
import { HistoryTable, mapStateToProps } from './HistoryTable';
jest.mock('@openedx/paragon', () => ({ DataTable: () => 'DataTable' })); initializeMocks();
jest.mock('@edx/frontend-platform/i18n', () => ({ jest.mock('@openedx/paragon', () => ({
defineMessages: m => m, ...jest.requireActual('@openedx/paragon'),
FormattedMessage: () => 'FormattedMessage', 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', () => ({ jest.mock('data/selectors', () => ({
__esModule: true, __esModule: true,
default: { default: {
grades: { grades: {
bulkManagementHistoryEntries: jest.fn(state => ({ historyEntries: state })), bulkManagementHistoryEntries: jest.fn(),
}, },
}, },
})); }));
jest.mock('./ResultsSummary', () => 'ResultsSummary');
describe('HistoryTable', () => { describe('HistoryTable', () => {
describe('component', () => { beforeEach(() => {
const entry1 = { jest.clearAllMocks();
originalFilename: 'blue.png', });
user: 'Eifel',
timeUploaded: '65', const mockBulkManagementHistory = [
{
originalFilename: 'test-file-1.csv',
user: 'test-user-1',
timeUploaded: '2025-01-01T10:00:00Z',
resultsSummary: { resultsSummary: {
rowId: 12, rowId: 1,
courseId: 'Da Bu Dee', text: 'Download results 1',
text: 'Da ba daa',
}, },
}; },
const entry2 = { {
originalFilename: 'allStar.jpg', originalFilename: 'test-file-2.csv',
user: 'Smashmouth', user: 'test-user-2',
timeUploaded: '2000s?', timeUploaded: '2025-01-02T10:00:00Z',
resultsSummary: { resultsSummary: {
courseId: 'rockstar',
rowId: 2, 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], it('transforms row data correctly', () => {
}; const result = mapHistoryRows(mockRow);
let el;
describe('snapshot', () => { expect(result).toHaveProperty('resultsSummary');
beforeEach(() => { expect(result).toHaveProperty('filename');
el = shallow(<HistoryTable {...props} />); expect(result).toHaveProperty('user');
}); expect(result).toHaveProperty('timeUploaded');
test('snapshot - loads formatted table', () => { expect(result.timeUploaded).toBe('2025-01-01T10:00:00Z');
expect(el.snapshot).toMatchSnapshot(); });
});
describe('history table', () => { it('wraps filename in span with correct class', () => {
let table; const result = mapHistoryRows(mockRow);
beforeEach(() => { render(<div>{result.filename}</div>);
table = el.instance.findByType(DataTable);
}); const filenameSpan = screen.getByText('test-file.csv');
describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => { expect(filenameSpan).toBeInTheDocument();
const fieldAssertions = [ expect(filenameSpan).toHaveClass('wrap-text-in-cell');
'maps resultsSummay to ResultsSummary', });
'wraps filename and user',
'forwards the rest', it('wraps user in span with correct class', () => {
]; const result = mapHistoryRows(mockRow);
test(`snapshot: ${fieldAssertions.join(', ')}`, () => { render(<div>{result.user}</div>);
expect(table[0].props.data).toMatchSnapshot();
}); const userSpan = screen.getByText('test-user');
test(fieldAssertions.join(', '), () => { expect(userSpan).toBeInTheDocument();
const rows = table[0].props.data; expect(userSpan).toHaveClass('wrap-text-in-cell');
expect(rows[0].resultsSummary).toEqual(<ResultsSummary {...entry1.resultsSummary} />); });
expect(rows[0].user).toEqual(<span className="wrap-text-in-cell">{entry1.user}</span>);
expect( it('renders ResultsSummary component with correct props', () => {
rows[0].filename, const result = mapHistoryRows(mockRow);
).toEqual(<span className="wrap-text-in-cell">{entry1.originalFilename}</span>); render(<div>{result.resultsSummary}</div>);
expect(rows[1].resultsSummary).toEqual(<ResultsSummary {...entry2.resultsSummary} />);
expect(rows[1].user).toEqual(<span className="wrap-text-in-cell">{entry2.user}</span>); expect(ResultsSummary).toHaveBeenCalledWith(mockRow.resultsSummary, {});
expect( expect(screen.getByTestId('results-summary')).toBeInTheDocument();
rows[1].filename, });
).toEqual(<span className="wrap-text-in-cell">{entry2.originalFilename}</span>); });
});
}); describe('component', () => {
test('columns from bulkManagementColumns', () => { it('renders DataTable with empty data when no history provided', () => {
expect(table[0].props.columns).toEqual(bulkManagementColumns); 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', () => { describe('mapStateToProps', () => {
const testState = { a: 'simple', test: 'state' }; const mockState = { test: 'state' };
let mapped; const mockHistoryEntries = [
{ originalFilename: 'file1.csv', user: 'user1' },
{ originalFilename: 'file2.csv', user: 'user2' },
];
beforeEach(() => { beforeEach(() => {
mapped = mapStateToProps(testState); selectors.grades.bulkManagementHistoryEntries.mockReturnValue(
mockHistoryEntries,
);
}); });
test('bulkManagementHistory from grades.bulkManagementHistoryEntries', () => {
it('maps bulkManagementHistory from selector', () => {
const result = mapStateToProps(mockState);
expect( expect(
mapped.bulkManagementHistory, selectors.grades.bulkManagementHistoryEntries,
).toEqual(selectors.grades.bulkManagementHistoryEntries(testState)); ).toHaveBeenCalledWith(mockState);
expect(result.bulkManagementHistory).toBe(mockHistoryEntries);
}); });
}); });
}); });

View File

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

View File

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

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 { render, initializeMocks, screen } from 'testUtilsExtra';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { BulkManagementHistoryView } from '.'; import { BulkManagementHistoryView } from '.';
import BulkManagementAlerts from './BulkManagementAlerts';
import HistoryTable from './HistoryTable';
import messages from './messages'; import messages from './messages';
jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts'); jest.mock('./BulkManagementAlerts', () => jest.fn(() => <div>BulkManagementAlerts</div>));
jest.mock('./HistoryTable', () => 'HistoryTable'); jest.mock('./HistoryTable', () => jest.fn(() => <div>HistoryTable</div>));
initializeMocks();
describe('BulkManagementHistoryView', () => { describe('BulkManagementHistoryView', () => {
describe('component', () => { describe('component', () => {
let el;
beforeEach(() => { beforeEach(() => {
el = shallow(<BulkManagementHistoryView />); render(<BulkManagementHistoryView />);
}); });
describe('snapshot', () => { describe('render alerts and heading', () => {
const snapshotSegments = [ it('heading - h4 loaded from messages', () => {
'heading from messages.BulkManagementHistoryView.heading', expect(screen.getByText(messages.heading.defaultMessage)).toBeInTheDocument();
'<BulkManagementAlerts />', expect(screen.getByText(messages.helpText.defaultMessage)).toBeInTheDocument();
'<HistoryTable />', expect(screen.getByText('BulkManagementAlerts')).toBeInTheDocument();
]; expect(screen.getByText('HistoryTable')).toBeInTheDocument();
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);
}); });
}); });
}); });

View File

@@ -8,7 +8,7 @@ const messages = defineMessages({
}, },
helpText: { helpText: {
id: 'gradebook.BulkManagementHistoryView', 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', description: 'Bulk Management History View help text',
}, },
successDialog: { 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 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 SelectGroup from '../SelectGroup';
import useAssignmentFilterData from './hooks'; import useAssignmentFilterData from './hooks';
import AssignmentFilter from '.'; import AssignmentFilter from '.';
jest.mock('../SelectGroup', () => 'SelectGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() })); jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const handleChange = jest.fn(); const handleChange = jest.fn();
@@ -25,29 +22,16 @@ useAssignmentFilterData.mockReturnValue({
const updateQueryParams = jest.fn(); const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilter component', () => { describe('AssignmentFilter component', () => {
beforeAll(() => { beforeAll(() => {
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />); initializeMocks();
}); render(<AssignmentFilter updateQueryParams={updateQueryParams} />);
describe('behavior', () => {
it('initializes hooks', () => {
expect(useAssignmentFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
}); });
describe('render', () => { describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('filter options', () => { test('filter options', () => {
const { options } = el.instance.findByType(SelectGroup)[0].props; expect(screen.getByRole('combobox', { name: 'Assignment' })).toBeInTheDocument();
expect(options.length).toEqual(5); expect(screen.getAllByRole('option')).toHaveLength(assignmentFilterOptions.length + 1); // +1 for the default option
const testOption = assignmentFilterOptions[0]; expect(screen.getAllByRole('option')[assignmentFilterOptions.length]).toHaveTextContent(assignmentFilterOptions[assignmentFilterOptions.length - 1].label);
const optionProps = options[1].props;
expect(optionProps.value).toEqual(testOption.label);
expect(optionProps.children.join(''))
.toEqual(`${testOption.label}: ${testOption.subsectionLabel}`);
}); });
}); });
}); });

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'; /* eslint-disable import/no-extraneous-dependencies */
import { shallow } from '@edx/react-unit-test-utils'; import { screen } from '@testing-library/react';
import { useIntl } from '@edx/frontend-platform/i18n'; import userEvent from '@testing-library/user-event';
import { Button } from '@openedx/paragon';
import PercentGroup from '../PercentGroup';
import useAssignmentGradeFilterData from './hooks'; import useAssignmentGradeFilterData from './hooks';
import AssignmentFilter from '.'; import AssignmentFilter from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('../PercentGroup', () => 'PercentGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() })); jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const hookData = { const hookData = {
handleChange: jest.fn(), handleSubmit: jest.fn(),
handleSetMax: jest.fn(), handleSetMax: jest.fn(),
handleSetMin: jest.fn(), handleSetMin: jest.fn(),
selectedAssignment: 'test-assignment', selectedAssignment: 'test-assignment',
@@ -22,37 +20,39 @@ useAssignmentGradeFilterData.mockReturnValue(hookData);
const updateQueryParams = jest.fn(); const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilter component', () => { describe('AssignmentFilter component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => { describe('behavior', () => {
it('initializes hooks', () => { it('initializes hooks', () => {
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
expect(useAssignmentGradeFilterData).toHaveBeenCalledWith({ updateQueryParams }); expect(useAssignmentGradeFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
}); });
}); });
describe('render', () => { describe('render', () => {
describe('with selected assignment', () => { describe('with selected assignment', () => {
test('snapshot', () => { beforeEach(() => {
expect(el.snapshot).toMatchSnapshot(); jest.clearAllMocks();
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
}); });
it('renders a PercentGroup for both Max and Min filters', () => { it('renders a PercentGroup for both Max and Min filters', async () => {
let { props } = el.instance.findByType(PercentGroup)[0]; const user = userEvent.setup();
expect(props.value).toEqual(hookData.assignmentGradeMin); const minGradeInput = screen.getByRole('spinbutton', { name: /Min Grade/i });
expect(props.disabled).toEqual(false); const maxGradeInput = screen.getByRole('spinbutton', { name: /Max Grade/i });
expect(props.onChange).toEqual(hookData.handleSetMin); expect(minGradeInput).toBeInTheDocument();
props = el.instance.findByType(PercentGroup)[1].props; expect(maxGradeInput).toBeInTheDocument();
expect(props.value).toEqual(hookData.assignmentGradeMax); expect(minGradeInput).toBeEnabled();
expect(props.disabled).toEqual(false); expect(maxGradeInput).toBeEnabled();
expect(props.onChange).toEqual(hookData.handleSetMax); await user.type(minGradeInput, '25');
expect(hookData.handleSetMin).toHaveBeenCalled();
await user.type(maxGradeInput, '50');
expect(hookData.handleSetMax).toHaveBeenCalled();
}); });
it('renders a submit button', () => { it('renders a submit button', async () => {
const { props } = el.instance.findByType(Button)[0]; const user = userEvent.setup();
expect(props.disabled).toEqual(false); const submitButton = screen.getByRole('button', { name: /Apply/ });
expect(props.onClick).toEqual(hookData.handleSubmit); expect(submitButton).toBeInTheDocument();
expect(submitButton).not.toHaveAttribute('disabled');
await user.click(submitButton);
expect(hookData.handleSubmit).toHaveBeenCalled();
}); });
}); });
describe('without selected assignment', () => { describe('without selected assignment', () => {
@@ -61,16 +61,13 @@ describe('AssignmentFilter component', () => {
...hookData, ...hookData,
selectedAssignment: null, selectedAssignment: null,
}); });
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />); renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
}); });
it('disables controls', () => { it('disables controls', () => {
let { props } = el.instance.findByType(PercentGroup)[0]; const minGrade = screen.getByRole('spinbutton', { name: /Min Grade/ });
expect(props.disabled).toEqual(true); const maxGrade = screen.getByRole('spinbutton', { name: /Max Grade/ });
props = el.instance.findByType(PercentGroup)[1].props; expect(minGrade).toHaveAttribute('disabled');
expect(props.disabled).toEqual(true); 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 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 SelectGroup from '../SelectGroup';
import useAssignmentFilterTypeData from './hooks'; import useAssignmentFilterTypeData from './hooks';
import AssignmentFilterType from '.'; import AssignmentFilterType from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('../SelectGroup', () => 'SelectGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() })); jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const handleChange = jest.fn(); const handleChange = jest.fn();
@@ -21,27 +19,15 @@ useAssignmentFilterTypeData.mockReturnValue({
const updateQueryParams = jest.fn(); const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilterType component', () => { describe('AssignmentFilterType component', () => {
beforeAll(() => { beforeAll(() => {
el = shallow(<AssignmentFilterType updateQueryParams={updateQueryParams} />); renderWithIntl(<AssignmentFilterType updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useAssignmentFilterTypeData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
}); });
describe('render', () => { describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('filter options', () => { test('filter options', () => {
const { options } = el.instance.findByType(SelectGroup)[0].props; const options = screen.getAllByRole('option');
expect(options.length).toEqual(5); expect(options.length).toEqual(5); // 4 types + "All Types"
const optionProps = options[1].props; expect(options[1]).toHaveTextContent(testType);
expect(optionProps.value).toEqual(assignmentTypes[0]);
expect(optionProps.children).toEqual(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 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 { Button } from '@openedx/paragon';
import PercentGroup from '../PercentGroup';
import useCourseGradeFilterData from './hooks'; import useCourseGradeFilterData from './hooks';
import CourseFilter from '.'; import CourseFilter from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('../PercentGroup', () => 'PercentGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() })); jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const hookData = { const hookData = {
@@ -27,48 +24,37 @@ useCourseGradeFilterData.mockReturnValue(hookData);
const updateQueryParams = jest.fn(); const updateQueryParams = jest.fn();
let el;
describe('CourseFilter component', () => { describe('CourseFilter component', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useCourseGradeFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
}); });
describe('render', () => { describe('render', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('with selected assignment', () => { describe('with selected assignment', () => {
test('snapshot', () => { beforeEach(() => {
expect(el.snapshot).toMatchSnapshot(); renderWithIntl(<CourseFilter updateQueryParams={updateQueryParams} />);
}); });
it('renders a PercentGroup for both Max and Min filters', () => { it('renders a PercentGroup for both Max and Min filters', () => {
let { props } = el.instance.findByType(PercentGroup)[0]; expect(screen.getByRole('spinbutton', { name: 'Min Grade' })).toHaveValue(hookData.min.value);
expect(props.value).toEqual(hookData.min.value); expect(screen.getByRole('spinbutton', { name: 'Max Grade' })).toHaveValue(hookData.max.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);
}); });
it('renders a submit button', () => { it('renders a submit button', () => {
const { props } = el.instance.findByType(Button)[0]; expect(screen.getByRole('button', { name: 'Apply' })).toBeInTheDocument();
expect(props.disabled).toEqual(false); // Expect it to be enabled
expect(props.onClick).toEqual(hookData.handleApplyClick); expect(screen.getByRole('button', { name: 'Apply' })).not.toBeDisabled();
}); });
}); });
describe('if disabled', () => { describe('if disabled', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks();
useCourseGradeFilterData.mockReturnValueOnce({ ...hookData, isDisabled: true }); useCourseGradeFilterData.mockReturnValueOnce({ ...hookData, isDisabled: true });
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />); renderWithIntl(<CourseFilter updateQueryParams={updateQueryParams} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
}); });
it('disables submit', () => { it('disables submit', () => {
const { props } = el.instance.findByType(Button)[0]; expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
expect(props.disabled).toEqual(true);
}); });
}); });
}); });

View File

@@ -1,5 +1,4 @@
import React from 'react'; import { render, screen, initializeMocks } from 'testUtilsExtra';
import { shallow } from '@edx/react-unit-test-utils';
import PercentGroup from './PercentGroup'; import PercentGroup from './PercentGroup';
@@ -12,6 +11,7 @@ describe('PercentGroup', () => {
}; };
beforeEach(() => { beforeEach(() => {
initializeMocks();
props = { props = {
...props, ...props,
onChange: jest.fn().mockName('props.onChange'), onChange: jest.fn().mockName('props.onChange'),
@@ -19,15 +19,17 @@ describe('PercentGroup', () => {
}); });
describe('Component', () => { describe('Component', () => {
describe('snapshots', () => { test('is displayed', () => {
test('basic snapshot', () => { render(<PercentGroup {...props} />);
const el = shallow(<PercentGroup {...props} />); expect(screen.getByRole('spinbutton', { name: 'Group Label' })).toBeInTheDocument();
expect(el.snapshot).toMatchSnapshot(); expect(screen.getByText('Group Label')).toBeVisible();
}); expect(screen.getByText('%')).toBeVisible();
test('disabled', () => { });
const el = shallow(<PercentGroup {...props} disabled />); test('disabled', () => {
expect(el.snapshot).toMatchSnapshot(); 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 React from 'react';
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import SelectGroup from './SelectGroup'; import SelectGroup from './SelectGroup';
@@ -24,15 +24,14 @@ describe('SelectGroup', () => {
}); });
describe('Component', () => { describe('Component', () => {
describe('snapshots', () => { test('rendered with all options and label', () => {
test('basic snapshot', () => { render(<SelectGroup {...props} />);
const el = shallow(<SelectGroup {...props} />); expect(screen.getAllByRole('option')).toHaveLength(props.options.length);
expect(el.snapshot).toMatchSnapshot(); expect(screen.getByLabelText(props.label)).toBeInTheDocument();
}); });
test('disabled', () => { test('disabled', () => {
const el = shallow(<SelectGroup {...props} disabled />); render(<SelectGroup {...props} disabled />);
expect(el.snapshot).toMatchSnapshot(); 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 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 SelectGroup from '../SelectGroup'; import SelectGroup from '../SelectGroup';
import { StudentGroupsFilter } from './index';
import useStudentGroupsFilterData from './hooks'; import useStudentGroupsFilterData from './hooks';
import StudentGroupsFilter from '.';
jest.mock('../SelectGroup', () => 'SelectGroup'); jest.mock('../SelectGroup', () => jest.fn(() => <div data-testid="select-group">SelectGroup</div>));
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() })); jest.mock('./hooks', () => jest.fn());
const props = { initializeMocks();
cohorts: {
value: 'test-cohort', describe('StudentGroupsFilter', () => {
const mockUpdateQueryParams = jest.fn();
const mockTracksData = {
value: 'test-track-value',
entries: [ entries: [
{ value: 'v1', name: 'n1' }, { value: 'track1', name: 'Track 1' },
{ value: 'v2', name: 'n2' }, { value: 'track2', name: 'Track 2' },
{ value: 'v3', name: 'n3' }, ],
handleChange: jest.fn(),
};
const mockCohortsData = {
value: 'test-cohort-value',
entries: [
{ value: 'cohort1', name: 'Cohort 1' },
{ value: 'cohort2', name: 'Cohort 2' },
], ],
handleChange: jest.fn(), handleChange: jest.fn(),
isDisabled: false, 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; beforeEach(() => {
describe('StudentGroupsFilter component', () => {
beforeAll(() => {
jest.clearAllMocks(); jest.clearAllMocks();
el = shallow(<StudentGroupsFilter updateQueryParams={updateQueryParams} />); useStudentGroupsFilterData.mockReturnValue({
}); tracks: mockTracksData,
describe('behavior', () => { cohorts: mockCohortsData,
it('initializes hooks', () => {
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
}); });
}); });
describe('render', () => {
test('snapshot', () => { it('calls useStudentGroupsFilterData hook with updateQueryParams', () => {
expect(el.snapshot).toMatchSnapshot(); render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({
updateQueryParams: mockUpdateQueryParams,
}); });
test('track options', () => { });
const {
options, it('renders two SelectGroup components', () => {
onChange, render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
value,
} = el.instance.findByType(SelectGroup)[0].props; expect(SelectGroup).toHaveBeenCalledTimes(2);
expect(value).toEqual(props.tracks.value); expect(screen.getAllByTestId('select-group')).toHaveLength(2);
expect(onChange).toEqual(props.tracks.handleChange); });
expect(options.length).toEqual(5);
const testEntry = props.tracks.entries[0]; describe('tracks SelectGroup', () => {
const optionProps = options[1].props; it('renders tracks SelectGroup with correct props', () => {
expect(optionProps.value).toEqual(testEntry.value); render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
expect(optionProps.children).toEqual(testEntry.name);
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 { it('includes trackAll option in tracks SelectGroup', () => {
options, render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
onChange,
disabled, const tracksCall = SelectGroup.mock.calls[0][0];
value, const { options } = tracksCall;
} = el.instance.findByType(SelectGroup)[1].props;
expect(value).toEqual(props.cohorts.value); expect(options).toHaveLength(3);
expect(disabled).toEqual(false); expect(options[0].props.value).toBeDefined();
expect(onChange).toEqual(props.cohorts.handleChange); expect(options[0].props.children).toBeDefined();
expect(options.length).toEqual(4); });
const testEntry = props.cohorts.entries[0];
const optionProps = options[1].props; it('includes track entries in tracks SelectGroup options', () => {
expect(optionProps.value).toEqual(testEntry.value); render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
expect(optionProps.children).toEqual(testEntry.name);
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 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 '.'; 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(); const updateQueryParams = jest.fn();
initializeMocks();
describe('GradebookFilters', () => { describe('GradebookFilters', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
el = shallow(<GradebookFilters updateQueryParams={updateQueryParams} />); render(<GradebookFilters updateQueryParams={updateQueryParams} />);
}); });
describe('behavior', () => { describe('All filters render together', () => {
it('initializes hooks', () => {
expect(useGradebookFiltersData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('Assignment filters', () => { test('Assignment filters', () => {
expect(el.instance.findByType(Collapsible)[0].children[0]).toMatchObject(shallow( expect(screen.getByRole('combobox', { name: 'Assignment Types' })).toBeInTheDocument();
<div> expect(screen.getByRole('combobox', { name: 'Assignment' })).toBeInTheDocument();
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
<AssignmentFilter updateQueryParams={updateQueryParams} />
<AssignmentGradeFilter updateQueryParams={updateQueryParams} />
</div>,
));
}); });
test('CourseGrade filters', () => { test('CourseGrade filters', () => {
expect(el.instance.findByType(Collapsible)[1].children[0]).toMatchObject(shallow( expect(screen.getByRole('button', { name: 'Overall Grade' })).toBeInTheDocument();
<CourseGradeFilter updateQueryParams={updateQueryParams} />,
));
}); });
test('StudentGroups filters', () => { test('StudentGroups filters', () => {
expect(el.instance.findByType(Collapsible)[2].children[0]).toMatchObject(shallow( expect(screen.getByRole('button', { name: 'Student Groups' })).toBeInTheDocument();
<StudentGroupsFilter updateQueryParams={updateQueryParams} />,
));
}); });
test('includeCourseTeamMembers', () => { test('includeCourseTeamMembers', () => {
const checkbox = el.instance.findByType(Collapsible)[3].children[0]; expect(screen.getByRole('button', { name: 'Include Course Team Members' })).toBeInTheDocument();
expect(checkbox.props).toEqual({
checked: true,
onChange: hookProps.includeCourseTeamMembers.handleChange,
});
expect(checkbox.children[0].el).toEqual(formatMessage(messages.includeCourseTeamMembers));
}); });
}); });
}); });

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 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 userEvent from '@testing-library/user-event';
import { Button } from '@openedx/paragon';
import { formatMessage } from 'testUtils';
import { instructorDashboardUrl } from 'data/services/lms/urls'; import { instructorDashboardUrl } from 'data/services/lms/urls';
import { GradebookHeader } from './index';
import useGradebookHeaderData from './hooks'; 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', () => ({ jest.mock('data/services/lms/urls', () => ({
instructorDashboardUrl: jest.fn(), instructorDashboardUrl: jest.fn(),
})); }));
jest.mock('./hooks', () => jest.fn());
instructorDashboardUrl.mockReturnValue('test-dashboard-url'); initializeMocks();
const hookProps = { describe('GradebookHeader', () => {
areGradesFrozen: false, const mockHandleToggleViewClick = jest.fn();
canUserViewGradebook: true,
courseId: 'test-course-id',
handleToggleViewClick: jest.fn().mockName('hooks.handleToggleViewClick'),
showBulkManagement: false,
toggleViewMessage: { defaultMessage: 'toggle-view-message' },
};
useGradebookHeaderData.mockReturnValue(hookProps);
let el; beforeEach(() => {
describe('GradebookHeader component', () => { jest.clearAllMocks();
beforeAll(() => { instructorDashboardUrl.mockReturnValue('https://example.com/dashboard');
el = shallow(<GradebookHeader />);
}); });
describe('behavior', () => {
it('initializes hooks', () => { describe('basic rendering', () => {
expect(useGradebookHeaderData).toHaveBeenCalledWith(); beforeEach(() => {
expect(useIntl).toHaveBeenCalledWith(); 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', () => { describe('bulk management toggle button', () => {
test('shapshot', () => { describe('when showBulkManagement is true', () => {
expect(el.snapshot).toMatchSnapshot(); 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(() => { beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true }); useGradebookHeaderData.mockReturnValue({
el = shallow(<GradebookHeader />); 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(); it('does not render toggle view button', () => {
const { onClick } = el.instance.findByType(Button)[0].props; render(<GradebookHeader />);
expect(onClick).toEqual(hookProps.handleToggleViewClick); expect(screen.queryByRole('button')).not.toBeInTheDocument();
expect(el.instance.findByType(Button)[0].children[0].el).toEqual(formatMessage(hookProps.toggleViewMessage));
}); });
}); });
describe('frozen grades', () => { });
describe('frozen grades warning', () => {
describe('when areGradesFrozen is true', () => {
beforeEach(() => { beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true }); useGradebookHeaderData.mockReturnValue({
el = shallow(<GradebookHeader />); 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(() => { beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false }); useGradebookHeaderData.mockReturnValue({
el = shallow(<GradebookHeader />); 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 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 useBulkManagementControlsData from './hooks';
import BulkManagementControls from '.'; import messages from './messages';
jest.mock('../ImportGradesButton', () => 'ImportGradesButton');
jest.mock('components/NetworkButton', () => 'NetworkButton');
jest.mock('components/NetworkButton', () => jest.fn(() => <div data-testid="network-button">NetworkButton</div>));
jest.mock('../ImportGradesButton', () => jest.fn(() => (
<div data-testid="import-grades-button">ImportGradesButton</div>
)));
jest.mock('./hooks', () => jest.fn()); jest.mock('./hooks', () => jest.fn());
const hookProps = { initializeMocks();
show: true,
handleClickExportGrades: jest.fn(),
};
useBulkManagementControlsData.mockReturnValue(hookProps);
describe('BulkManagementControls', () => { describe('BulkManagementControls', () => {
describe('behavior', () => { const mockHandleClickExportGrades = jest.fn();
shallow(<BulkManagementControls />);
expect(useBulkManagementControlsData).toHaveBeenCalledWith(); beforeEach(() => {
jest.clearAllMocks();
}); });
describe('render', () => {
test('snapshot - show - network and import buttons', () => { describe('when show is false', () => {
expect(shallow(<BulkManagementControls />).snapshot).toMatchSnapshot(); beforeEach(() => {
useBulkManagementControlsData.mockReturnValue({
show: false,
handleClickExportGrades: mockHandleClickExportGrades,
});
}); });
test('snapshot - empty if show is not truthy', () => {
useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false }); it('renders nothing when show is false', () => {
expect(shallow(<BulkManagementControls />).isEmptyRender()).toEqual(true); 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 React from 'react';
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen, initializeMocks } from 'testUtilsExtra';
import HistoryHeader from './HistoryHeader'; import HistoryHeader from './HistoryHeader';
initializeMocks();
describe('HistoryHeader', () => { describe('HistoryHeader', () => {
const props = { const defaultProps = {
id: 'water', id: 'test-id',
label: 'Brita', label: 'Test Label',
value: 'hydration', value: 'Test Value',
}; };
describe('Component', () => {
test('snapshot', () => { it('renders header with label and value', () => {
expect(shallow(<HistoryHeader {...props} />).snapshot).toMatchSnapshot(); 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 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 { selectors } from 'data/redux/hooks';
import { formatMessage } from 'testUtils'; import { render, screen, initializeMocks } from 'testUtilsExtra';
import ModalHeaders from './ModalHeaders';
import HistoryHeader from './HistoryHeader';
import ModalHeaders, { HistoryKeys } from './ModalHeaders';
import messages from './messages';
jest.mock('./HistoryHeader', () => 'HistoryHeader');
jest.mock('data/redux/hooks', () => ({ jest.mock('data/redux/hooks', () => ({
selectors: { selectors: {
@@ -29,57 +21,25 @@ const gradeData = {
gradeOriginalEarnedGraded: 'test-original-grade', gradeOriginalEarnedGraded: 'test-original-grade',
}; };
selectors.grades.useGradeData.mockReturnValue(gradeData); selectors.grades.useGradeData.mockReturnValue(gradeData);
initializeMocks();
let el;
describe('ModalHeaders', () => { describe('ModalHeaders', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
el = shallow(<ModalHeaders />); render(<ModalHeaders />);
});
describe('behavior', () => {
it('initializes intl', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
expect(selectors.app.useModalData).toHaveBeenCalled();
expect(selectors.grades.useGradeData).toHaveBeenCalled();
});
}); });
describe('render', () => { describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('assignment header', () => { test('assignment header', () => {
const headerProps = el.instance.findByType(HistoryHeader)[0].props; expect(screen.getByText(modalData.assignmentName)).toBeInTheDocument();
expect(headerProps).toMatchObject({
id: HistoryKeys.assignment,
label: formatMessage(messages.assignmentHeader),
value: modalData.assignmentName,
});
}); });
test('student header', () => { test('student header', () => {
const headerProps = el.instance.findByType(HistoryHeader)[1].props; expect(screen.getByText(modalData.updateUserName)).toBeInTheDocument();
expect(headerProps).toMatchObject({
id: HistoryKeys.student,
label: formatMessage(messages.studentHeader),
value: modalData.updateUserName,
});
}); });
test('originalGrade header', () => { test('originalGrade header', () => {
const headerProps = el.instance.findByType(HistoryHeader)[2].props; expect(screen.getByText(gradeData.gradeOriginalEarnedGraded)).toBeInTheDocument();
expect(headerProps).toMatchObject({
id: HistoryKeys.originalGrade,
label: formatMessage(messages.originalGradeHeader),
value: gradeData.gradeOriginalEarnedGraded,
});
}); });
test('currentGrade header', () => { test('currentGrade header', () => {
const headerProps = el.instance.findByType(HistoryHeader)[3].props; expect(screen.getByText(gradeData.gradeOverrideCurrentEarnedGradedOverride)).toBeInTheDocument();
expect(headerProps).toMatchObject({
id: HistoryKeys.currentGrade,
label: formatMessage(messages.currentGradeHeader),
value: gradeData.gradeOverrideCurrentEarnedGradedOverride,
});
}); });
}); });
}); });

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

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 modalData = { reasonForChange: 'test-reason-for-change' };
const setModalState = jest.fn(); const setModalState = jest.fn();
selectors.app.useModalData.mockReturnValue(modalData); selectors.app.useModalData.mockReturnValue(modalData);
@@ -25,6 +31,7 @@ const ref = { current: { focus: jest.fn() }, useRef: true };
React.useRef.mockReturnValue(ref); React.useRef.mockReturnValue(ref);
let out; let out;
describe('useReasonInputData hook', () => { describe('useReasonInputData hook', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();

View File

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

View File

@@ -4,8 +4,6 @@ import { render } from '@testing-library/react';
import useReasonInputData from './hooks'; import useReasonInputData from './hooks';
import ReasonInput, { controlTestId } from '.'; import ReasonInput, { controlTestId } from '.';
jest.unmock('react');
jest.unmock('@openedx/paragon');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() })); jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const focus = 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 { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app'; import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
import { selectors } from 'data/redux/hooks'; 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); selectors.grades.useHasOverrideErrors.mockReturnValue(false);
const gradeOverrides = ['some', 'override', 'data']; const gradeOverrides = ['some', 'override', 'data'];
const gradeData = { gradeOverrideHistoryResults: gradeOverrides }; const gradeData = { gradeOverrideHistoryResults: gradeOverrides };
@@ -44,22 +55,22 @@ describe('useOverrideTableData', () => {
describe('columns', () => { describe('columns', () => {
test('date column', () => { test('date column', () => {
const { Header, accessor } = out.columns[0]; const { Header, accessor } = out.columns[0];
expect(Header).toEqual(formatMessage(messages.dateHeader)); expect(Header).toEqual(messages.dateHeader.defaultMessage);
expect(accessor).toEqual(columns.date); expect(accessor).toEqual(columns.date);
}); });
test('grader column', () => { test('grader column', () => {
const { Header, accessor } = out.columns[1]; const { Header, accessor } = out.columns[1];
expect(Header).toEqual(formatMessage(messages.graderHeader)); expect(Header).toEqual(messages.graderHeader.defaultMessage);
expect(accessor).toEqual(columns.grader); expect(accessor).toEqual(columns.grader);
}); });
test('reason column', () => { test('reason column', () => {
const { Header, accessor } = out.columns[2]; const { Header, accessor } = out.columns[2];
expect(Header).toEqual(formatMessage(messages.reasonHeader)); expect(Header).toEqual(messages.reasonHeader.defaultMessage);
expect(accessor).toEqual(columns.reason); expect(accessor).toEqual(columns.reason);
}); });
test('adjustedGrade column', () => { test('adjustedGrade column', () => {
const { Header, accessor } = out.columns[3]; const { Header, accessor } = out.columns[3];
expect(Header).toEqual(formatMessage(messages.adjustedGradeHeader)); expect(Header).toEqual(messages.adjustedGradeHeader.defaultMessage);
expect(accessor).toEqual(columns.adjustedGrade); expect(accessor).toEqual(columns.adjustedGrade);
}); });
}); });

View File

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

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

View File

@@ -1,15 +1,12 @@
import React from 'react'; 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 { formatMessage } from 'testUtils';
import { Button } from '@openedx/paragon';
import { selectors } from 'data/redux/hooks'; import { selectors } from 'data/redux/hooks';
import userEvent from '@testing-library/user-event';
import FilterBadge from './FilterBadge'; import FilterBadge from './FilterBadge';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('@openedx/paragon', () => ({
Button: () => 'Button',
}));
jest.mock('data/redux/hooks', () => ({ jest.mock('data/redux/hooks', () => ({
selectors: { selectors: {
root: { 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 filterName = 'test-filter-name';
const hookProps = { const hookProps = {
displayName: { displayName: {
id: 'test.id',
defaultMessage: 'a common name', defaultMessage: 'a common name',
}, },
isDefault: false, isDefault: false,
@@ -32,63 +30,52 @@ const hookProps = {
}; };
selectors.root.useFilterBadgeConfig.mockReturnValue(hookProps); selectors.root.useFilterBadgeConfig.mockReturnValue(hookProps);
let el;
describe('FilterBadge', () => { describe('FilterBadge', () => {
beforeEach(() => { describe('hooks', () => {
el = shallow(<FilterBadge {...{ handleClose, filterName }} />); beforeEach(() => {
}); renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
}); });
it('initializes redux hooks', () => { it('initializes redux hooks', () => {
expect(selectors.root.useFilterBadgeConfig).toHaveBeenCalledWith(filterName); expect(selectors.root.useFilterBadgeConfig).toHaveBeenCalledWith(filterName);
}); });
}); });
describe('render', () => { describe('render', () => {
const testDisplayName = () => { it('empty render if isDefault', () => {
test('formatted display name appears on badge', () => {
expect(el.instance.findByTestId('display-name')[0].children[0].el).toEqual(formatMessage(hookProps.displayName));
});
};
const testCloseButton = () => {
test('close button forwards close method', () => {
expect(el.instance.findByType(Button)[0].props.onClick).toEqual(handleClose(hookProps.connectedFilters));
});
};
test('empty render if isDefault', () => {
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({ selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
...hookProps, ...hookProps,
isDefault: true, isDefault: true,
}); });
el = shallow(<FilterBadge {...{ handleClose, filterName }} />); renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
expect(el.isEmptyRender()).toEqual(true); expect(screen.queryByText(hookProps.displayName)).toBeNull();
}); });
describe('hide Value', () => { describe('hide Value', () => {
beforeEach(() => { it('renders display name, value is not shown and close button has correct behavior', async () => {
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({ selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
...hookProps, ...hookProps,
hideValue: true, hideValue: true,
}); });
el = shallow(<FilterBadge {...{ handleClose, filterName }} />); renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
}); const user = userEvent.setup();
testDisplayName(); expect(screen.getByTestId('display-name')).toHaveTextContent(formatMessage(hookProps.displayName));
testCloseButton(); expect(screen.queryByTestId('filter-value')).toHaveTextContent('');
test('snapshot', () => { const button = screen.getByRole('button', { name: /close/i });
expect(el.snapshot).toMatchSnapshot(); await user.click(button);
}); expect(handleClose).toHaveBeenCalledWith(hookProps.connectedFilters);
test('value is note present in the badge', () => {
expect(el.instance.findByTestId('filter-value')[0].children).toHaveLength(0);
}); });
}); });
describe('do not hide value', () => { describe('do not hide value', () => {
testDisplayName(); it('renders display name and value, and close button has correct behavior', async () => {
testCloseButton(); selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
test('snapshot', () => { ...hookProps,
expect(el.snapshot).toMatchSnapshot(); hideValue: false,
}); });
test('value is present in the badge', () => { renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
expect(el.instance.findByTestId('filter-value')[0].children[0].el).toBe(`: ${hookProps.value}`); 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 */ /* eslint-disable import/no-named-as-default */
import React from 'react'; 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 FilterBadges from '.';
import FilterBadge from './FilterBadge';
jest.mock('./FilterBadge', () => 'FilterBadge');
const order = ['filter1', 'filter2', 'filter3']; const order = ['filter1', 'filter2', 'filter3'];
jest.mock('data/constants/filters', () => ({ jest.mock('data/constants/filters', () => ({
@@ -13,24 +12,24 @@ jest.mock('data/constants/filters', () => ({
badgeOrder: order, 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('FilterBadges', () => {
describe('component', () => { describe('component', () => {
let el; it('has a filterbadge with handleClose for each filter in badgeOrder', async () => {
let handleClose; render(<FilterBadges handleClose={handleClose} />);
beforeEach(() => { const user = userEvent.setup();
handleClose = jest.fn().mockName('this.props.handleClose'); const badge1 = screen.getByText(order[0]);
el = shallow(<FilterBadges handleClose={handleClose} />); const badge2 = screen.getByText(order[1]);
}); const badge3 = screen.getByText(order[2]);
test('snapshot - has a filterbadge with handleClose for each filter in badgeOrder', () => { expect(badge1).toBeInTheDocument();
expect(el.snapshot).toMatchSnapshot(); expect(badge2).toBeInTheDocument();
}); expect(badge3).toBeInTheDocument();
test('has a filterbadge with handleClose for each filter in badgeOrder', () => { await user.click(badge1);
const badgeProps = el.instance.findByType(FilterBadge).map(badgeEl => badgeEl.props); expect(handleClose).toHaveBeenCalled();
// 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);
}); });
}); });
}); });

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 { screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils'; import userEvent from '@testing-library/user-event';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils'; import { formatMessage } from 'testUtils';
import { thunkActions } from 'data/redux/hooks'; import { thunkActions } from 'data/redux/hooks';
import FilterMenuToggle from '.'; import FilterMenuToggle from '.';
import messages from './messages'; import messages from './messages';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('data/redux/hooks', () => ({ jest.mock('data/redux/hooks', () => ({
thunkActions: { thunkActions: {
@@ -22,26 +20,23 @@ jest.mock('data/redux/hooks', () => ({
const toggleFilterMenu = jest.fn().mockName('hooks.toggleFilterMenu'); const toggleFilterMenu = jest.fn().mockName('hooks.toggleFilterMenu');
thunkActions.app.filterMenu.useToggleMenu.mockReturnValue(toggleFilterMenu); thunkActions.app.filterMenu.useToggleMenu.mockReturnValue(toggleFilterMenu);
let el;
describe('FilterMenuToggle component', () => { describe('FilterMenuToggle component', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
el = shallow(<FilterMenuToggle />); renderWithIntl(<FilterMenuToggle />);
}); });
describe('behavior', () => { describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => { it('initializes redux hooks', () => {
expect(thunkActions.app.filterMenu.useToggleMenu).toHaveBeenCalled(); expect(thunkActions.app.filterMenu.useToggleMenu).toHaveBeenCalled();
}); });
}); });
describe('render', () => { describe('renders', () => {
test('snapshot', () => { it('button and triggers click', async () => {
expect(el.snapshot).toMatchSnapshot(); const user = userEvent.setup();
expect(el.instance.type).toEqual('Button'); const button = screen.getByRole('button', { name: formatMessage(messages.editFilters) });
expect(el.instance.props.onClick).toEqual(toggleFilterMenu); expect(button).toBeInTheDocument();
expect(el.instance.children[2].el).toContain(formatMessage(messages.editFilters)); 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 { screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { selectors } from 'data/redux/hooks'; import { selectors } from 'data/redux/hooks';
import FilteredUsersLabel, { BoldText } from '.'; import FilteredUsersLabel from '.';
import messages from './messages'; import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('data/redux/hooks', () => ({ jest.mock('data/redux/hooks', () => ({
selectors: { selectors: {
@@ -23,34 +19,29 @@ const userCounts = {
}; };
selectors.grades.useUserCounts.mockReturnValue(userCounts); selectors.grades.useUserCounts.mockReturnValue(userCounts);
let el;
describe('FilteredUsersLabel component', () => { describe('FilteredUsersLabel component', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
el = shallow(<FilteredUsersLabel />);
}); });
describe('behavior', () => { describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => { it('initializes redux hooks', () => {
renderWithIntl(<FilteredUsersLabel />);
expect(selectors.grades.useUserCounts).toHaveBeenCalled(); expect(selectors.grades.useUserCounts).toHaveBeenCalled();
}); });
}); });
describe('render', () => { describe('render', () => {
test('null render if totalUsersCount is 0', () => { it('null render if totalUsersCount is 0', () => {
selectors.grades.useUserCounts.mockReturnValueOnce({ selectors.grades.useUserCounts.mockReturnValueOnce({
...userCounts, ...userCounts,
totalUsersCount: 0, totalUsersCount: 0,
}); });
expect(shallow(<FilteredUsersLabel />).isEmptyRender()).toEqual(true); const { container } = renderWithIntl(<FilteredUsersLabel />);
expect(container.firstChild).toBeNull();
}); });
test('snapshot', () => { it('renders users count correctly', () => {
expect(el.snapshot).toMatchSnapshot(); renderWithIntl(<FilteredUsersLabel />);
expect(el.instance).toMatchObject(shallow(formatMessage(messages.visibilityLabel, { expect(screen.getByText((text) => text.includes(userCounts.filteredUsersCount))).toBeInTheDocument();
filteredUsers: <BoldText text={userCounts.filteredUsersCount} />, expect(screen.getByText((text) => text.includes(userCounts.totalUsersCount))).toBeInTheDocument();
totalUsers: <BoldText text={userCounts.totalUsersCount} />,
})));
}); });
}); });
}); });

View File

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

View File

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

View File

@@ -1,10 +1,8 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { getLocale } from '@edx/frontend-platform/i18n'; import { getLocale } from '@edx/frontend-platform/i18n';
import { OverlayTrigger } from '@openedx/paragon';
import LabelReplacements from './LabelReplacements'; import LabelReplacements from './LabelReplacements';
import messages from './messages';
import { renderWithIntl } from '../../../testUtilsExtra';
const { const {
TotalGradeLabelReplacement, TotalGradeLabelReplacement,
@@ -12,52 +10,36 @@ const {
MastersOnlyLabelReplacement, MastersOnlyLabelReplacement,
} = LabelReplacements; } = LabelReplacements;
jest.mock('@openedx/paragon', () => ({ jest.mock('@edx/frontend-platform/i18n', () => ({
Icon: () => 'Icon', ...jest.requireActual('@edx/frontend-platform/i18n'),
OverlayTrigger: () => 'OverlayTrigger', getLocale: jest.fn(),
Tooltip: () => 'Tooltip', isRtl: jest.fn(),
})); }));
describe('LabelReplacements', () => { describe('LabelReplacements', () => {
describe('TotalGradeLabelReplacement', () => { describe('TotalGradeLabelReplacement', () => {
let el; getLocale.mockImplementation(() => 'en');
beforeEach(() => { renderWithIntl(<TotalGradeLabelReplacement />);
el = shallow(<TotalGradeLabelReplacement />); it('displays overlay tooltip', () => {
}); const tooltip = screen.getByText(messages.totalGradePercentage.defaultMessage);
test('snapshot', () => { expect(tooltip).toBeInTheDocument();
expect(el.snapshot).toMatchSnapshot();
});
test('displays overlay tooltip', () => {
expect(el.instance.findByType(OverlayTrigger)[0].props.overlay).toMatchSnapshot();
}); });
}); });
describe('UsernameLabelReplacement', () => { describe('UsernameLabelReplacement', () => {
test('snapshot', () => { it('renders correctly', () => {
expect(shallow(<UsernameLabelReplacement />).snapshot).toMatchSnapshot(); renderWithIntl(<UsernameLabelReplacement />);
expect(screen.getByText(messages.usernameHeading.defaultMessage)).toBeInTheDocument();
}); });
}); });
describe('MastersOnlyLabelReplacement', () => { describe('MastersOnlyLabelReplacement', () => {
test('snapshot', () => { it('renders correctly', () => {
const message = { const message = {
id: 'id', id: 'id',
defaultMessage: 'defaultMessAge', defaultMessage: 'defaultMessAge',
description: 'desCripTion', 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 React from 'react';
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 { Headings } from 'data/constants/grades'; 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 messages from './messages';
import useGradebookTableData from './hooks'; let mockUseAllGrades;
let mockUseGetHeadings;
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',
}));
jest.mock('data/redux/hooks', () => ({ jest.mock('data/redux/hooks', () => ({
selectors: { selectors: {
grades: { useAllGrades: jest.fn() }, grades: { useAllGrades: () => mockUseAllGrades() },
root: { useGetHeadings: jest.fn() }, root: { useGetHeadings: () => mockUseGetHeadings() },
}, },
})); }));
jest.mock('data/redux/transforms', () => ({ jest.mock('data/redux/transforms', () => ({
grades: { roundGrade: jest.fn() }, grades: { roundGrade: jest.fn((val) => val) },
})); }));
const roundGrade = grade => grade * 20; jest.mock('i18n/utils', () => ({ getLocalizedPercentSign: () => '%' }));
transforms.grades.roundGrade.mockImplementation(roundGrade); 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 = [ const subsectionLabels = [
'subsectionLabel1', 'subsectionLabel1',
@@ -85,7 +70,9 @@ const allGrades = [
], ],
}, },
]; ];
const testHeading = 'test-heading-value'; const testHeading = 'test-heading-value';
const headings = [ const headings = [
Headings.totalGrade, Headings.totalGrade,
Headings.username, Headings.username,
@@ -93,100 +80,60 @@ const headings = [
Headings.fullName, Headings.fullName,
testHeading, testHeading,
]; ];
selectors.grades.useAllGrades.mockReturnValue(allGrades);
selectors.root.useGetHeadings.mockReturnValue(headings);
let out; describe('useGradebookTableData hook', () => {
describe('useGradebookTableData', () => { beforeAll(() => {
mockUseAllGrades = jest.fn();
mockUseGetHeadings = jest.fn();
});
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); mockUseAllGrades.mockReset();
out = useGradebookTableData(); mockUseGetHeadings.mockReset();
}); });
describe('behavior', () => { let hookResult;
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled(); const TestComponent = () => {
}); hookResult = hooks.useGradebookTableData();
it('initializes redux hooks', () => { return null;
expect(selectors.grades.useAllGrades).toHaveBeenCalled(); };
expect(selectors.root.useGetHeadings).toHaveBeenCalled();
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', () => { it('nullMethod returns null', () => {
test('total grade heading produces TotalGradeLabelReplacement label', () => { render(<TestComponent />);
const { Header, accessor } = out.columns[0]; expect(hookResult.nullMethod()).toBeNull();
expect(accessor).toEqual(headings[0]); });
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.TotalGradeLabelReplacement />), it('returns expected structure with grades and headings data', () => {
); mockUseAllGrades.mockReturnValue(allGrades);
}); mockUseGetHeadings.mockReturnValue(headings);
test('username heading produces UsernameLabelReplacement', () => { render(<TestComponent />);
const { Header, accessor } = out.columns[1]; expect(hookResult.columns.length).toBe(headings.length);
expect(accessor).toEqual(headings[1]); expect(hookResult.columns[0].accessor).toEqual(headings[0]);
expect(shallow(Header)).toMatchObject( expect(hookResult.data.length).toBe(allGrades.length);
shallow(<LabelReplacements.UsernameLabelReplacement />), expect(hookResult.data[0]).toHaveProperty(Headings.username);
); expect(hookResult.grades).toEqual(allGrades);
}); expect(hookResult.nullMethod()).toBeNull();
test('email heading replaces with email heading message', () => { expect(hookResult.emptyContent).toBe(messages.noResultsFound.defaultMessage);
const { Header, accessor } = out.columns[2];
expect(accessor).toEqual(headings[2]); expect(mockUseAllGrades).toHaveBeenCalled();
expect(shallow(Header)).toMatchObject( expect(mockUseGetHeadings).toHaveBeenCalled();
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));
});
}); });
}); });

View File

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

View File

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

View File

@@ -1,46 +1,26 @@
import React from 'react'; import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n'; import {
import { Form } from '@openedx/paragon'; render, screen, initializeMocks,
} from 'testUtilsExtra';
import NetworkButton from 'components/NetworkButton';
import useImportGradesButtonData from './hooks';
import ImportGradesButton from '.'; import ImportGradesButton from '.';
jest.mock('components/NetworkButton', () => 'NetworkButton'); initializeMocks();
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
let el;
let props;
describe('ImportGradesButton component', () => { describe('ImportGradesButton component', () => {
beforeAll(() => { beforeEach(() => {
props = { jest.clearAllMocks();
fileInputRef: { current: null }, render(<ImportGradesButton />);
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();
});
}); });
describe('render', () => { describe('render', () => {
test('snapshot', () => { test('Form', async () => {
expect(el.snapshot).toMatchSnapshot(); const uploader = screen.getByTestId('file-control');
}); expect(uploader).toBeInTheDocument();
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('import button', () => { 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 React from 'react';
import { render } from '@testing-library/react';
import useImportGradesButtonData from './hooks'; import useImportGradesButtonData from './hooks';
import ImportGradesButton from '.'; import ImportGradesButton from '.';
import { renderWithIntl, screen } from '../../../testUtilsExtra';
jest.unmock('react');
jest.unmock('@openedx/paragon');
jest.mock('components/NetworkButton', () => 'network-button'); jest.mock('components/NetworkButton', () => 'network-button');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() })); jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
@@ -17,11 +14,10 @@ const props = {
}; };
useImportGradesButtonData.mockReturnValue(props); useImportGradesButtonData.mockReturnValue(props);
let el;
describe('ImportGradesButton ref test', () => { describe('ImportGradesButton ref test', () => {
it('loads ref from hook', () => { it('loads ref from hook', () => {
el = render(<ImportGradesButton />); renderWithIntl(<ImportGradesButton />);
const input = el.getByTestId('file-control'); const input = screen.getByTestId('file-control');
expect(input).toEqual(props.fileInputRef.current); 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 setView = jest.fn().mockName('hooks.setView');
const setShowToast = jest.fn().mockName('hooks.setShowImportSuccessToast'); const setShowToast = jest.fn().mockName('hooks.setShowImportSuccessToast');
actions.app.useSetView.mockReturnValue(setView); actions.app.useSetView.mockReturnValue(setView);

View File

@@ -1,39 +1,85 @@
import React from 'react'; 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 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()); jest.mock('./hooks', () => jest.fn());
const hookProps = { initializeMocks();
action: 'test-action',
onClose: jest.fn().mockName('hooks.onClose'),
show: 'test-show',
description: 'test-description',
};
useImportSuccessToastData.mockReturnValue(hookProps);
let el; describe('ImportSuccessToast', () => {
describe('ImportSuccessToast component', () => { beforeEach(() => {
beforeAll(() => { jest.clearAllMocks();
el = shallow(<ImportSuccessToast />);
}); });
describe('behavior', () => {
it('initializes component hook', () => { it('renders with show false', () => {
expect(useImportSuccessToastData).toHaveBeenCalled(); 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 alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
const toastMessage = screen.queryByText('Import Successful! Grades will be updated momentarily.');
expect(toastMessage).toBeNull();
expect(useImportSuccessToastData).toHaveBeenCalled();
}); });
describe('render', () => {
test('snapshot', () => { it('renders with show true', () => {
expect(el.snapshot).toMatchSnapshot(); 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'); render(<ImportSuccessToast />);
expect(el.instance.props.action).toEqual(hookProps.action); const toastMessage = screen.getByText('Import Successful! Grades will be updated momentarily.');
expect(el.instance.props.onClose).toEqual(hookProps.onClose); expect(toastMessage).toBeInTheDocument();
expect(el.instance.props.show).toEqual(hookProps.show); expect(useImportSuccessToastData).toHaveBeenCalled();
expect(el.instance.children[0].el).toEqual(hookProps.description); });
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 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 '.'; import InterventionsReport from '.';
jest.mock('components/NetworkButton', () => 'NetworkButton'); jest.mock('components/NetworkButton', () => 'NetworkButton');
jest.mock('./hooks', () => jest.fn()); jest.mock('./hooks', () => jest.fn());
const hookProps = { show: true, handleClick: jest.fn() }; const useInterventionsReportData = require('./hooks');
useInterventionsReportData.mockReturnValue(hookProps);
let el; initializeMocks();
describe('InterventionsReport component', () => {
describe('InterventionsReport', () => {
beforeEach(() => { beforeEach(() => {
el = shallow(<InterventionsReport />); jest.clearAllMocks();
}); });
describe('behavior', () => {
it('initializes hooks', () => { it('renders without errors when show is true', () => {
expect(useInterventionsReportData).toHaveBeenCalledWith(); useInterventionsReportData.mockReturnValue({
expect(useIntl).toHaveBeenCalledWith(); 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', () => { it('renders nothing when show is false', () => {
useInterventionsReportData.mockReturnValueOnce({ ...hookProps, show: false }); useInterventionsReportData.mockReturnValue({
el = shallow(<InterventionsReport />); show: false,
expect(el.isEmptyRender()).toEqual(true); handleClick: jest.fn(),
}); });
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot(); render(<InterventionsReport />);
const btnProps = el.instance.findByType(NetworkButton)[0].props; expect(screen.queryByRole('heading')).not.toBeInTheDocument();
expect(btnProps.label).toEqual(messages.downloadBtn); expect(screen.queryByText('Interventions Report')).not.toBeInTheDocument();
expect(btnProps.onClick).toEqual(hookProps.handleClick); 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' }; const gradeData = { nextPage: 'test-next-page', prevPage: 'test-prev-page' };
selectors.grades.useGradeData.mockReturnValue(gradeData); selectors.grades.useGradeData.mockReturnValue(gradeData);

View File

@@ -1,7 +1,6 @@
import React from 'react'; 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 usePageButtonsData from './hooks';
import PageButtons from '.'; import PageButtons from '.';
@@ -10,44 +9,53 @@ jest.mock('./hooks', () => jest.fn());
const hookProps = { const hookProps = {
prev: { prev: {
disabled: 'prev-disabled', disabled: false,
onClick: jest.fn().mockName('hooks.prev.onClick'), onClick: jest.fn().mockName('hooks.prev.onClick'),
text: 'prev-text', text: 'prev-text',
}, },
next: { next: {
disabled: 'next-disabled', disabled: false,
onClick: jest.fn().mockName('hooks.next.onClick'), onClick: jest.fn().mockName('hooks.next.onClick'),
text: 'next-text', text: 'next-text',
}, },
}; };
usePageButtonsData.mockReturnValue(hookProps);
let el;
describe('PageButtons component', () => { describe('PageButtons component', () => {
beforeEach(() => { beforeAll(() => {
jest.clearAllMocks(); jest.clearAllMocks();
el = shallow(<PageButtons />); initializeMocks();
}); });
describe('behavior', () => { describe('renders enabled buttons', () => {
it('initializes component hooks', () => { beforeEach(() => {
expect(usePageButtonsData).toHaveBeenCalled(); 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', () => { describe('renders disabled buttons', () => {
expect(el.snapshot).toMatchSnapshot(); beforeAll(() => {
hookProps.prev.disabled = true;
hookProps.next.disabled = true;
}); });
test('prev button', () => { beforeEach(() => {
const button = el.instance.findByType(Button)[0]; usePageButtonsData.mockReturnValue(hookProps);
expect(button.props.disabled).toEqual(hookProps.prev.disabled); render(<PageButtons />);
expect(button.props.onClick).toEqual(hookProps.prev.onClick);
expect(button.children[0].el).toEqual(hookProps.prev.text);
}); });
test('next button', () => { test('prev button disabled', () => {
const button = el.instance.findByType(Button)[1]; expect(screen.getByText(hookProps.next.text)).toBeInTheDocument();
expect(button.props.disabled).toEqual(hookProps.next.disabled); expect(screen.getByText(hookProps.prev.text)).toBeDisabled();
expect(button.props.onClick).toEqual(hookProps.next.onClick); });
expect(button.children[0].el).toEqual(hookProps.next.text); 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 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 { GradeFormats } from 'data/constants/grades'; import userEvent from '@testing-library/user-event';
import { formatMessage } from 'testUtils'; import { ScoreViewInput } from '.';
import { actions, selectors } from 'data/redux/hooks';
import ScoreViewInput from '.';
import messages from './messages';
jest.mock('data/redux/hooks', () => ({ jest.mock('data/redux/hooks', () => ({
actions: { actions: {
grades: { useToggleGradeFormat: jest.fn() }, grades: {
useToggleGradeFormat: jest.fn(),
},
}, },
selectors: { selectors: {
grades: { useGradeData: jest.fn() }, grades: {
useGradeData: jest.fn(),
},
}, },
})); }));
const toggleGradeFormat = jest.fn().mockName('hooks.toggleGradeFormat'); const { actions, selectors } = require('data/redux/hooks');
actions.grades.useToggleGradeFormat.mockReturnValue(toggleGradeFormat);
const gradeFormat = 'test-grade-format'; initializeMocks();
selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
describe('ScoreViewInput', () => {
const mockToggleFormat = jest.fn();
let el;
describe('ScoreViewInput component', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
el = shallow(<ScoreViewInput />);
}); selectors.grades.useGradeData.mockReturnValue({
describe('behavior', () => { gradeFormat: 'percent',
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
}); });
it('initializes redux hooks', () => { actions.grades.useToggleGradeFormat.mockReturnValue(mockToggleFormat);
expect(actions.grades.useToggleGradeFormat).toHaveBeenCalled(); });
expect(selectors.grades.useGradeData).toHaveBeenCalled();
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', () => { describe('form control behavior', () => {
expect(el.snapshot).toMatchSnapshot(); 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]; it('has correct option values', () => {
expect(label.children[0].el).toEqual(`${formatMessage(messages.scoreView)}`); 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; it('has exactly two options', () => {
beforeEach(() => { render(<ScoreViewInput />);
control = el.instance.children;
}); const options = screen.getAllByRole('option');
test('value and onChange from redux hooks', () => { expect(options).toHaveLength(2);
expect(control[1].props.value).toEqual(gradeFormat); });
expect(control[1].props.onChange).toEqual(toggleGradeFormat); });
});
test('absolute and percent options', () => { describe('redux integration', () => {
const { children } = control[1]; it('uses grade data selector hook', () => {
expect(children[0].props.value).toEqual(GradeFormats.percent); render(<ScoreViewInput />);
expect(children[0].children[0].el).toEqual(formatMessage(messages.percent));
expect(children[1].props.value).toEqual(GradeFormats.absolute); expect(selectors.grades.useGradeData).toHaveBeenCalledTimes(1);
expect(children[1].children[0].el).toEqual(formatMessage(messages.absolute)); });
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>
`;

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