Compare commits

...

33 Commits

Author SHA1 Message Date
dependabot[bot]
ad009ef7ab build(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [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/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 13:09:57 +00:00
edX requirements bot
ea2f1f9f6f chore: update browserslist DB (#525)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-11-24 00:31:09 +00:00
edX requirements bot
a4f63de389 chore: update browserslist DB (#523)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-11-17 00:29:35 +00:00
edX requirements bot
6e1688d627 chore: update browserslist DB (#522)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-11-10 00:30:20 +00:00
edX requirements bot
e759221e5f chore: update browserslist DB (#520)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-11-03 00:30:18 +00:00
edX requirements bot
02a3bda10a chore: update browserslist DB (#518)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-10-20 00:30:49 +00:00
edX requirements bot
aa4ddfa977 chore: update browserslist DB (#515)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-10-13 00:29:17 +00:00
edX requirements bot
2b88ef3144 chore: update browserslist DB (#514)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-10-06 00:27:49 +00:00
Feanil Patel
cd6cb71eb5 Merge pull request #513 from openedx/feanil/node24
build: Switch to Node 24 for building and testing.
2025-10-02 13:52:26 -04:00
Feanil Patel
c6d72bcf47 build: Switch to Node 24 for building and testing. 2025-10-02 11:24:12 -04:00
edX requirements bot
a0bdd0c012 chore: update browserslist DB (#512)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-09-29 00:28:42 +00:00
Feanil Patel
73e1421a90 Merge pull request #511 from openedx/feanil/remove-reactifex-packages
build: remove unused reactifex packages
2025-09-26 13:06:13 -04:00
Feanil Patel
d38ec004cb build: remove unused reactifex packages
Remove reactifex and/or @edx/reactifex packages from devDependencies
as they are no longer needed. Translation extraction functionality has
been verified to work correctly without these dependencies.

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

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

* test: coverage on app.test.jsx

* test: improve test cases for toast

* test: improve coverage on gradebook table

* test: missing snap/shallow depr

---------

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

The frontend-build dependency was updated by:

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

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

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

* test: address comments

---------

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

* fix: lint issues

* test: change fireEvent for userEvent

---------

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

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

* test: improve coverage and remove extra word space

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

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

* chore: test utils from dev to dependencies

* chore: removed unused imports

* chore: restore packages to devDep

* chore: renamed header to pass lint

* feat: deprecated shallow in tests and added testing Utils

* chore: removed deprecated tests

* chore: commit to trigger test again

* chore: fix import warning for new testing file

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

* chore: test utils from dev to dependencies

* chore: removed unused imports

* chore: restore packages to devDep

* chore: renamed header to pass lint
2025-08-07 18:12:39 -04:00
edX requirements bot
b88969c9bb chore: update browserslist DB (#495)
Co-authored-by: jansenk <1639231+jansenk@users.noreply.github.com>
2025-08-04 00:47:46 +00:00
Kyle McCormick
427907fba2 chore: Delete CODEOWNERS (#493)
See: https://github.com/openedx/axim-engineering/issues/1511
2025-07-31 16:08:22 -04:00
110 changed files with 3327 additions and 4241 deletions

6
.github/CODEOWNERS vendored
View File

@@ -1,6 +0,0 @@
# Code owners for frontend-app-gradebook, editable gradebook micro-frontend (MFE)
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, they will
# be requested for review when someone opens a pull request.
* @openedx/content-aurora

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Nodejs
uses: actions/setup-node@v4

2
.nvmrc
View File

@@ -1 +1 @@
20
24

View File

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

1184
package-lock.json generated
View File

@@ -1,21 +1,19 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.6.2",
"version": "1.6.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@edx/frontend-app-gradebook",
"version": "1.6.2",
"version": "1.6.3",
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.4.0",
"@edx/frontend-component-header": "^6.6.1",
"@edx/frontend-platform": "^8.3.7",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@edx/reactifex": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
@@ -49,15 +47,16 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/react": "^16.2.0",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"es-check": "^2.3.0",
"fetch-mock": "^12.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"react-dev-utils": "^12.0.1",
"react-test-renderer": "^18.3.1",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.3"
}
},
@@ -65,6 +64,7 @@
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz",
"integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==",
"dev": true,
"license": "MIT"
},
"node_modules/@ampproject/remapping": {
@@ -137,6 +137,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
@@ -2185,6 +2186,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@@ -2207,6 +2209,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
}
@@ -2254,6 +2257,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@edx/browserslist-config/-/browserslist-config-1.5.0.tgz",
"integrity": "sha512-d2ggwi5j4DOBJOwhWZxBWQSDR0DhT4ke/1PbzRauICdFkuOyax+PsFjK8GUh443K2OaQpy9PGfiCzZ1Yg37AUA==",
"dev": true,
"license": "AGPL-3.0"
},
"node_modules/@edx/eslint-config": {
@@ -2312,6 +2316,7 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
@@ -2357,21 +2362,18 @@
}
},
"node_modules/@edx/frontend-component-header": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-6.4.0.tgz",
"integrity": "sha512-RNV3XRXhhN9QlhAoP26CjzoRIPlLSYDp3PZCnK6g6kIHgxC9dCpu2PTZdxV2AVChqVuxtZK5zLbk9yeAtf4U/A==",
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-6.6.1.tgz",
"integrity": "sha512-ETGIpCyXq1YWR/wvc4fGzPUtGsdYfXKDtuH45sgiRx7Zt9spgNm0KYO1tah1TF1UrPjIkQErm+8LFh8me/kJCg==",
"license": "AGPL-3.0",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.6.0",
"@fortawesome/free-brands-svg-icons": "6.6.0",
"@fortawesome/free-regular-svg-icons": "6.6.0",
"@fortawesome/free-solid-svg-icons": "6.6.0",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"axios-mock-adapter": "1.22.0",
"babel-polyfill": "6.26.0",
"classnames": "^2.5.1",
"jest-environment-jsdom": "^29.7.0",
"react-responsive": "8.2.0",
"react-transition-group": "4.4.5"
},
@@ -2385,57 +2387,46 @@
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.6.0"
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-brands-svg-icons": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz",
"integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz",
"integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz",
"integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.6.0"
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.6.0"
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
@@ -2459,6 +2450,7 @@
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.3.7.tgz",
"integrity": "sha512-ya5ObMvtJlfQmoeL36OtzjFBh0hzJgXN/R2ppyIJ+IbCtY2BCfv5NqvmKD7CplwnSGJTBugpv5hQHeGmi+v97w==",
"license": "AGPL-3.0",
"peer": true,
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
"@formatjs/intl-pluralrules": "4.3.3",
@@ -2513,52 +2505,6 @@
"atlas": "atlas"
}
},
"node_modules/@edx/react-unit-test-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@edx/react-unit-test-utils/-/react-unit-test-utils-4.0.0.tgz",
"integrity": "sha512-QlVYhYD9L2bzx1eAtf8BbCJr00ek9rrHrG+/pW2bVSt+t0uvKHQpX1CNdMrDePv18DsMeC7IOB00t8ZIn4mi7w==",
"license": "AGPL-3.0",
"dependencies": {
"@edx/browserslist-config": "^1.1.1",
"@reduxjs/toolkit": "^1.5.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"classnames": "^2.2.6",
"core-js": "3.6.5",
"lodash": "^4.17.21",
"react-dev-utils": "^12.0.1",
"react-test-renderer": "^18.3.1"
},
"peerDependencies": {
"@edx/frontend-platform": "^8.3.1",
"@openedx/frontend-build": "^14.3.0",
"@openedx/paragon": "^22.0.0 || ^23.0.0",
"react": "^18.0.0"
}
},
"node_modules/@edx/reactifex": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-2.2.0.tgz",
"integrity": "sha512-vyGDtx3BwCr6Gjbm4y6gJ8Bzc2TOSNBlBa2hMerz59HoXaot14MihxxiDU+JDNybGLLcKDBiK511bOi/77i1lw==",
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
"yargs": "^17.1.1"
},
"bin": {
"edx_reactifex": "main.js"
}
},
"node_modules/@edx/reactifex/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/@edx/typescript-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@edx/typescript-config/-/typescript-config-1.1.0.tgz",
@@ -2580,9 +2526,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -3072,6 +3018,7 @@
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "^0.2.36"
},
@@ -3186,6 +3133,424 @@
"deprecated": "Use @eslint/object-schema instead",
"license": "BSD-3-Clause"
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
"integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz",
"integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.0"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz",
"integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz",
"integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz",
"integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz",
"integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz",
"integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==",
"cpu": [
"ppc64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz",
"integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz",
"integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz",
"integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz",
"integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz",
"integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.0"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz",
"integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz",
"integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==",
"cpu": [
"ppc64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.0"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz",
"integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.0"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz",
"integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.0"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz",
"integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz",
"integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.0"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz",
"integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.4.4"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz",
"integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz",
"integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz",
"integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -3652,6 +4017,7 @@
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
"integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.11.6",
"@jest/types": "^29.6.3",
@@ -3687,6 +4053,7 @@
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/schemas": "^29.6.3",
"@types/istanbul-lib-coverage": "^2.0.0",
@@ -3963,10 +4330,11 @@
}
},
"node_modules/@openedx/frontend-build": {
"version": "14.5.0",
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.5.0.tgz",
"integrity": "sha512-HY0PdXvXBxrvJHj8HsRA+VNCHDePENFhqOIvbSz9Ke7HDwHpfDjg2CeKk41aU/8iTyj3eESfPwKQr5fTE0A3Ww==",
"version": "14.6.2",
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.6.2.tgz",
"integrity": "sha512-Iu4/GPq90Xr/MSWnonn2qX8VDhI89HN7KOYBZ0/sxmAQgvXXNc7OYNC7kumvzbYzKueJQTyZoUYS7UjKB/n1WA==",
"license": "AGPL-3.0",
"peer": true,
"dependencies": {
"@babel/cli": "7.24.8",
"@babel/core": "7.24.9",
@@ -4011,7 +4379,7 @@
"file-loader": "6.2.0",
"html-webpack-plugin": "5.6.3",
"identity-obj-proxy": "3.0.0",
"image-minimizer-webpack-plugin": "3.8.3",
"image-minimizer-webpack-plugin": "4.1.4",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"mini-css-extract-plugin": "1.6.2",
@@ -4019,13 +4387,13 @@
"postcss": "8.4.49",
"postcss-custom-media": "10.0.8",
"postcss-loader": "7.3.4",
"postcss-rtlcss": "5.1.2",
"postcss-rtlcss": "5.7.1",
"react-dev-utils": "12.0.1",
"react-refresh": "0.16.0",
"resolve-url-loader": "5.0.0",
"sass": "1.85.1",
"sass-loader": "13.3.3",
"sharp": "0.32.6",
"sharp": "0.34.3",
"source-map-loader": "4.0.2",
"style-loader": "3.3.4",
"ts-jest": "29.1.4",
@@ -4235,6 +4603,7 @@
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.9.2"
}
@@ -4244,6 +4613,7 @@
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.4.5.tgz",
"integrity": "sha512-baBTZDO6hdCjI+Jj3oSQaz5GkFTR2TEs94pPMOls7bc/Fsv4zQIN8xDPo4NzAVo/2+3eSuEzUz3xzBeb+94rtw==",
"license": "Apache-2.0",
"peer": true,
"workspaces": [
"example",
"component-generator",
@@ -4776,6 +5146,7 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -4822,6 +5193,7 @@
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.9.2"
}
@@ -5054,6 +5426,7 @@
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@@ -5156,7 +5529,9 @@
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -5172,17 +5547,17 @@
}
},
"node_modules/@testing-library/jest-dom": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
"integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
"license": "MIT",
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz",
"integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==",
"dev": true,
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"lodash": "^4.17.21",
"picocolors": "^1.1.1",
"redent": "^3.0.0"
},
"engines": {
@@ -5191,30 +5566,18 @@
"yarn": ">=1"
}
},
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/react": {
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
"integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
"license": "MIT",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.12.5"
},
@@ -5237,6 +5600,20 @@
}
}
},
"node_modules/@testing-library/user-event": {
"version": "14.6.1",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
"integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12",
"npm": ">=6"
},
"peerDependencies": {
"@testing-library/dom": ">=7.21.4"
}
},
"node_modules/@tokens-studio/sd-transforms": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@tokens-studio/sd-transforms/-/sd-transforms-1.3.0.tgz",
@@ -5295,6 +5672,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
@@ -5676,6 +6054,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz",
"integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -5806,6 +6185,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.62.0",
@@ -5852,6 +6232,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
@@ -6598,6 +6979,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -7134,6 +7516,7 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -7160,19 +7543,6 @@
"axios": "^1"
}
},
"node_modules/axios-mock-adapter": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz",
"integrity": "sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"is-buffer": "^2.0.5"
},
"peerDependencies": {
"axios": ">= 0.17.0"
}
},
"node_modules/axobject-query": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.4.tgz",
@@ -7182,17 +7552,12 @@
"node": ">= 0.4"
}
},
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
"integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
"license": "Apache-2.0"
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
"integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/transform": "^29.7.0",
"@types/babel__core": "^7.1.14",
@@ -7461,78 +7826,6 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/bare-events": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
"license": "Apache-2.0",
"optional": true
},
"node_modules/bare-fs": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.3.tgz",
"integrity": "sha512-OeEZYIg+2qepaWLyphaOXHAHKo3xkM8y3BeGAvHdMN8GNWvEAU1Yw6rYpGzu/wDDbKxgEjVeVDpgGhDzaeMpjg==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"streamx": "^2.21.0"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -7714,6 +8007,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@@ -7884,9 +8178,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001756",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz",
"integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==",
"funding": [
{
"type": "opencollective",
@@ -8093,12 +8387,6 @@
"fsevents": "~2.3.2"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/chroma-js": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz",
@@ -8903,6 +9191,7 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true,
"license": "MIT"
},
"node_modules/cssesc": {
@@ -9174,21 +9463,6 @@
"node": ">=0.10"
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dedent": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
@@ -9209,15 +9483,6 @@
"integrity": "sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug==",
"license": "MIT"
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -9530,6 +9795,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-converter": {
@@ -9786,15 +10052,6 @@
"node": ">= 0.8"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -10104,6 +10361,7 @@
"integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
@@ -10160,6 +10418,7 @@
"resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz",
"integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==",
"license": "MIT",
"peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0",
"object.assign": "^4.1.2",
@@ -10200,6 +10459,7 @@
"resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz",
"integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==",
"license": "MIT",
"peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0"
},
@@ -10552,6 +10812,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
@@ -10606,6 +10867,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz",
"integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.7",
"aria-query": "^5.1.3",
@@ -10642,6 +10904,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz",
"integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==",
"license": "MIT",
"peer": true,
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.flatmap": "^1.3.1",
@@ -10672,6 +10935,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.1.tgz",
"integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -10892,6 +11156,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -11040,15 +11305,6 @@
"node": ">=0.10.0"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/expect": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
@@ -11174,12 +11430,6 @@
"integrity": "sha512-lEJeOH5VL5R09j6AA0D4Uvq7AgsHw0dAImQQ+F3iSyHZuAxyQfWobsagGpTcOPvJr3urmKRHrs+Gs9hV+/Qm/Q==",
"license": "MIT"
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -11803,12 +12053,6 @@
"node": ">= 0.6"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -12023,12 +12267,6 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -12754,16 +12992,16 @@
}
},
"node_modules/image-minimizer-webpack-plugin": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/image-minimizer-webpack-plugin/-/image-minimizer-webpack-plugin-3.8.3.tgz",
"integrity": "sha512-Ex0cjNJc2FUSuwN7WHNyxkIZINP0M9lrN+uWJznMcsehiM5Z7ELwk+SEkSGEookK1GUd2wf+09jy1PEH5a5XmQ==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/image-minimizer-webpack-plugin/-/image-minimizer-webpack-plugin-4.1.4.tgz",
"integrity": "sha512-A2DLYuCyu7icbGdv8OMGFQKPXvsztWAueBkT3yQ7KVW1YGnAJKtgLYELkN7/aUday05DzEdKRaLE5Bnh/9S2UQ==",
"license": "MIT",
"dependencies": {
"schema-utils": "^4.2.0",
"serialize-javascript": "^6.0.1"
"serialize-javascript": "^6.0.2"
},
"engines": {
"node": ">= 12.13.0"
"node": ">= 18.12.0"
},
"funding": {
"type": "opencollective",
@@ -12890,6 +13128,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -13139,29 +13378,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/is-bun-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
@@ -15328,6 +15544,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
@@ -15555,22 +15772,11 @@
"node": ">=6"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -15664,12 +15870,6 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -15722,12 +15922,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/napi-postinstall": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.2.tgz",
@@ -15780,30 +15974,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/node-abi": {
"version": "3.74.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-abi/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -16797,6 +16967,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
@@ -17439,12 +17610,12 @@
}
},
"node_modules/postcss-rtlcss": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-5.1.2.tgz",
"integrity": "sha512-cmcgRoO1wL7IJyVHw0RneWI/5Oe75NLC2NLlQLsNI7hcui+yRcW4RrILfQa4FqKQRLTU4r5eF0YPi1qZpMzQpA==",
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-5.7.1.tgz",
"integrity": "sha512-zE68CuARv5StOG/UQLa0W1Y/raUTzgJlfjtas43yh3/G1BFmoPEaHxPRHgeowXRFFhW33FehrNgsljxRLmPVWw==",
"license": "Apache-2.0",
"dependencies": {
"rtlcss": "4.1.1"
"rtlcss": "4.3.0"
},
"engines": {
"node": ">=18.0.0"
@@ -17503,69 +17674,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prebuild-install/node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/prebuild-install/node_modules/tar-fs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/prebuild-install/node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -17604,6 +17712,7 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1",
@@ -17618,6 +17727,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -17673,6 +17783,7 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -17741,16 +17852,6 @@
"integrity": "sha512-5MZ0I9i5JWVO7SizvOviKvZU2qaBbl2KQX150FAA+fJBwYpwOUId7aNygURWSdPzlsA/xZ/InUKXqBbzM0czTA==",
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -17932,35 +18033,12 @@
"node": ">= 0.8"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -18114,6 +18192,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -18391,6 +18470,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
@@ -18416,6 +18496,7 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.16.0.tgz",
"integrity": "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -18505,6 +18586,7 @@
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.15.0.tgz",
"integrity": "sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@remix-run/router": "1.8.0",
"react-router": "6.15.0"
@@ -18575,6 +18657,7 @@
"version": "16.15.0",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
"integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"object-assign": "^4.1.1",
@@ -18632,6 +18715,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz",
"integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-is": "^18.3.1",
@@ -18646,6 +18730,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/react-transition-group": {
@@ -18664,16 +18749,6 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/reactifex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/reactifex/-/reactifex-1.1.1.tgz",
"integrity": "sha512-HH2N/b5tRxh7ypIgCRsiBl/CTxRkTEPf9DhIstaM6hne4WiwM5/bBbWuvVlRZc/i3FdqZED3pZ//6n4mtxma4w==",
"dev": true,
"license": "MIT",
"bin": {
"reactifex": "main.js"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -18746,6 +18821,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"indent-string": "^4.0.0",
@@ -18769,6 +18845,7 @@
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"symbol-observable": "^1.2.0"
@@ -18779,6 +18856,7 @@
"resolved": "https://registry.npmjs.org/redux-beacon/-/redux-beacon-2.1.0.tgz",
"integrity": "sha512-KMbopAM7YLnQdospn5ClG3o8c4uhYkLBDEWSmxNbvQb4MGAJJh3sbbPKoEB00qU5rTXbwKuaaqZlWdgrY1Twxw==",
"license": "MIT",
"peer": true,
"dependencies": {
"array-flatten": "2.1.1"
},
@@ -19162,9 +19240,9 @@
}
},
"node_modules/rtlcss": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz",
"integrity": "sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz",
"integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==",
"license": "MIT",
"dependencies": {
"escalade": "^3.1.1",
@@ -19435,6 +19513,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -19713,26 +19792,45 @@
"license": "MIT"
},
"node_modules/sharp": {
"version": "0.32.6",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
"integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.2",
"node-addon-api": "^6.1.0",
"prebuild-install": "^7.1.1",
"semver": "^7.5.4",
"simple-get": "^4.0.1",
"tar-fs": "^3.0.4",
"tunnel-agent": "^0.6.0"
"detect-libc": "^2.0.4",
"semver": "^7.7.2"
},
"engines": {
"node": ">=14.15.0"
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-libvips-darwin-arm64": "1.2.0",
"@img/sharp-libvips-darwin-x64": "1.2.0",
"@img/sharp-libvips-linux-arm": "1.2.0",
"@img/sharp-libvips-linux-arm64": "1.2.0",
"@img/sharp-libvips-linux-ppc64": "1.2.0",
"@img/sharp-libvips-linux-s390x": "1.2.0",
"@img/sharp-libvips-linux-x64": "1.2.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0",
"@img/sharp-libvips-linuxmusl-x64": "1.2.0",
"@img/sharp-linux-arm": "0.34.3",
"@img/sharp-linux-arm64": "0.34.3",
"@img/sharp-linux-ppc64": "0.34.3",
"@img/sharp-linux-s390x": "0.34.3",
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-linuxmusl-arm64": "0.34.3",
"@img/sharp-linuxmusl-x64": "0.34.3",
"@img/sharp-wasm32": "0.34.3",
"@img/sharp-win32-arm64": "0.34.3",
"@img/sharp-win32-ia32": "0.34.3",
"@img/sharp-win32-x64": "0.34.3"
}
},
"node_modules/sharp/node_modules/detect-libc": {
@@ -19744,16 +19842,10 @@
"node": ">=8"
}
},
"node_modules/sharp/node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
"license": "MIT"
},
"node_modules/sharp/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -19873,51 +19965,6 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -20202,19 +20249,6 @@
"component-emitter": "^2.0.0"
}
},
"node_modules/streamx": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
"license": "MIT",
"dependencies": {
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
},
"optionalDependencies": {
"bare-events": "^2.2.0"
}
},
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
@@ -20417,6 +20451,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"min-indent": "^1.0.0"
@@ -20443,6 +20478,7 @@
"integrity": "sha512-+xU0IA1StzqAqFs/QtXkK+XJa7wpS4X5H+JQccRKsRCElgeLGocFU1U/UMvMUylKFw6vwGV+Y/a2wb2pm5rFFQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@bundled-es-modules/deepmerge": "^4.3.1",
"@bundled-es-modules/glob": "^10.4.2",
@@ -20970,31 +21006,6 @@
"node": ">=6"
}
},
"node_modules/tar-fs": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
}
},
"node_modules/tar-stream": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/terser": {
"version": "5.39.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
@@ -21127,15 +21138,6 @@
"node": ">=8"
}
},
"node_modules/text-decoder": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -21219,6 +21221,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -21486,7 +21489,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/tsutils": {
"version": "3.21.0",
@@ -21509,18 +21513,6 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -21547,6 +21539,7 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -22134,6 +22127,7 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz",
"integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
@@ -22249,6 +22243,7 @@
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",
@@ -22326,6 +22321,7 @@
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",

View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.6.2",
"version": "1.6.3",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": {
"type": "git",
@@ -31,11 +31,9 @@
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.4.0",
"@edx/frontend-component-header": "^6.6.1",
"@edx/frontend-platform": "^8.3.7",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@edx/reactifex": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
@@ -69,15 +67,16 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/react": "^16.2.0",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"es-check": "^2.3.0",
"fetch-mock": "^12.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"react-dev-utils": "^12.0.1",
"react-test-renderer": "^18.3.1",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.3"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,45 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementAlerts component no errer, no upload success snapshot - bulkImportError closed, success closed 1`] = `
<Fragment>
<Alert
dismissible={false}
show={false}
variant="danger"
/>
<Alert
dismissible={false}
show={false}
variant="success"
>
<FormattedMessage
defaultMessage="CSV processing. File uploads may take several minutes to complete."
description="Success Dialog message in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementHistoryView.successDialog"
/>
</Alert>
</Fragment>
`;
exports[`BulkManagementAlerts component no errer, no upload success snapshot - danger alert open with bulkImportError, success alert open with messages.successDialog 1`] = `
<Fragment>
<Alert
dismissible={false}
show={true}
variant="danger"
>
Oh noooooo
</Alert>
<Alert
dismissible={false}
show={true}
variant="success"
>
<FormattedMessage
defaultMessage="CSV processing. File uploads may take several minutes to complete."
description="Success Dialog message in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementHistoryView.successDialog"
/>
</Alert>
</Fragment>
`;

View File

@@ -1,118 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistoryTable component snapshot history table data (from bulkManagementHistory.map(this.formatHistoryRow) snapshot: maps resultsSummay to ResultsSummary, wraps filename and user, forwards the rest 1`] = `
[
{
"filename": <span
className="wrap-text-in-cell"
>
blue.png
</span>,
"resultsSummary": <ResultsSummary
courseId="Da Bu Dee"
rowId={12}
text="Da ba daa"
/>,
"timeUploaded": "65",
"user": <span
className="wrap-text-in-cell"
>
Eifel
</span>,
},
{
"filename": <span
className="wrap-text-in-cell"
>
allStar.jpg
</span>,
"resultsSummary": <ResultsSummary
courseId="rockstar"
rowId={2}
text="all that glitters is gold"
/>,
"timeUploaded": "2000s?",
"user": <span
className="wrap-text-in-cell"
>
Smashmouth
</span>,
},
]
`;
exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = `
<DataTable
className="table-striped"
columns={
[
{
"Header": "Gradebook",
"accessor": "filename",
"columnSortable": false,
"width": "col-5",
},
{
"Header": "Download Summary",
"accessor": "resultsSummary",
"columnSortable": false,
"width": "col",
},
{
"Header": "Who",
"accessor": "user",
"columnSortable": false,
"width": "col-1",
},
{
"Header": "When",
"accessor": "timeUploaded",
"columnSortable": false,
"width": "col",
},
]
}
data={
[
{
"filename": <span
className="wrap-text-in-cell"
>
blue.png
</span>,
"resultsSummary": <ResultsSummary
courseId="Da Bu Dee"
rowId={12}
text="Da ba daa"
/>,
"timeUploaded": "65",
"user": <span
className="wrap-text-in-cell"
>
Eifel
</span>,
},
{
"filename": <span
className="wrap-text-in-cell"
>
allStar.jpg
</span>,
"resultsSummary": <ResultsSummary
courseId="rockstar"
rowId={2}
text="all that glitters is gold"
/>,
"timeUploaded": "2000s?",
"user": <span
className="wrap-text-in-cell"
>
Smashmouth
</span>,
},
]
}
hasFixedColumnWidths={true}
itemCount={2}
/>
`;

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResultsSummary component snapshot - safe hyperlink with bulkGradesUrl with course and row id, download icon, results text 1`] = `
<Hyperlink
destination="www.edx.org"
href={
{
"url": {
"rowId": 42,
},
}
}
rel="noopener noreferrer"
showLaunchIcon={false}
target="_blank"
>
<Icon
className="d-inline-block"
src="DownloadIcon"
/>
texty
</Hyperlink>
`;

View File

@@ -1,26 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementHistoryView component snapshot snapshot - loads heading from messages.BulkManagementHistoryView.heading, <BulkManagementAlerts />, <HistoryTable /> 1`] = `
<div
className="bulk-management-history-view"
>
<h4>
<FormattedMessage
defaultMessage="Bulk Management History"
description="Heading text for BulkManagement History Tab"
id="gradebook.BulkManagementHistoryView.heading"
/>
</h4>
<p
className="help-text"
>
<FormattedMessage
defaultMessage="Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override."
description="Bulk Management History View help text"
id="gradebook.BulkManagementHistoryView"
/>
</p>
<BulkManagementAlerts />
<HistoryTable />
</div>
`;

View File

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

View File

@@ -8,7 +8,7 @@ const messages = defineMessages({
},
helpText: {
id: 'gradebook.BulkManagementHistoryView',
defaultMessage: 'Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override.',
defaultMessage: 'Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override.',
description: 'Bulk Management History View help text',
},
successDialog: {

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';
import Header from '.';
jest.mock('@edx/frontend-platform', () => ({
...jest.requireActual('@edx/frontend-platform'),
getConfig: jest.fn(),
}));
describe('Header', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('has edx link with logo url', () => {
const url = 'www.ourLogo.url';
const baseUrl = 'www.lms.url';
getConfig.mockReturnValue({ LOGO_URL: url, LMS_BASE_URL: baseUrl });
render(
<IntlProvider messages={{}} locale="en">
<Header />
</IntlProvider>,
);
const link = screen.getByRole('link');
const logo = screen.getByAltText('edX logo');
expect(link).toHaveAttribute('href', `${baseUrl}/dashboard`);
expect(logo).toHaveAttribute('src', url);
});
});

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header snapshot - has edx link with logo url 1`] = `
<div
className="mb-3"
>
<header
className="d-flex justify-content-center align-items-center p-3 border-bottom-blue"
>
<Hyperlink
destination="undefined/dashboard"
>
<img
alt="edX logo"
height="30"
src="www.ourLogo.url"
width="60"
/>
</Hyperlink>
<div />
</header>
</div>
`;

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { getConfig } from '@edx/frontend-platform';
import Header from '.';
jest.mock('@openedx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
}));
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
describe('Header', () => {
test('snapshot - has edx link with logo url', () => {
const url = 'www.ourLogo.url';
getConfig.mockReturnValue({ LOGO_URL: url });
expect(shallow(<Header />).snapshot).toMatchSnapshot();
});
});

View File

@@ -1,52 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AssignmentFilter component render snapshot 1`] = `
<div
className="student-filters"
>
<SelectGroup
disabled={false}
id="assignment"
label="Assignment"
onChange={[MockFunction]}
options={
[
<option
value=""
>
All
</option>,
<option
value="label1"
>
label1
:
sLabel1
</option>,
<option
value="label2"
>
label2
:
sLabel2
</option>,
<option
value="label3"
>
label3
:
sLabel3
</option>,
<option
value="label4"
>
label4
:
sLabel4
</option>,
]
}
value="test-label"
/>
</div>
`;

View File

@@ -1,12 +1,9 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import SelectGroup from '../SelectGroup';
import useAssignmentFilterData from './hooks';
import AssignmentFilter from '.';
jest.mock('../SelectGroup', () => 'SelectGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const handleChange = jest.fn();
@@ -25,29 +22,16 @@ useAssignmentFilterData.mockReturnValue({
const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilter component', () => {
beforeAll(() => {
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useAssignmentFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
initializeMocks();
render(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('filter options', () => {
const { options } = el.instance.findByType(SelectGroup)[0].props;
expect(options.length).toEqual(5);
const testOption = assignmentFilterOptions[0];
const optionProps = options[1].props;
expect(optionProps.value).toEqual(testOption.label);
expect(optionProps.children.join(''))
.toEqual(`${testOption.label}: ${testOption.subsectionLabel}`);
expect(screen.getByRole('combobox', { name: 'Assignment' })).toBeInTheDocument();
expect(screen.getAllByRole('option')).toHaveLength(assignmentFilterOptions.length + 1); // +1 for the default option
expect(screen.getAllByRole('option')[assignmentFilterOptions.length]).toHaveTextContent(assignmentFilterOptions[assignmentFilterOptions.length - 1].label);
});
});
});

View File

@@ -1,67 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AssignmentFilter component render with selected assignment snapshot 1`] = `
<div
className="grade-filter-inputs"
>
<PercentGroup
disabled={false}
id="assignmentGradeMin"
label="Min Grade"
onChange={[MockFunction]}
value={23}
/>
<PercentGroup
disabled={false}
id="assignmentGradeMax"
label="Max Grade"
onChange={[MockFunction]}
value={300}
/>
<div
className="grade-filter-action"
>
<Button
disabled={false}
name="assignmentGradeMinMax"
type="submit"
variant="outline-secondary"
>
Apply
</Button>
</div>
</div>
`;
exports[`AssignmentFilter component render without selected assignment snapshot 1`] = `
<div
className="grade-filter-inputs"
>
<PercentGroup
disabled={true}
id="assignmentGradeMin"
label="Min Grade"
onChange={[MockFunction]}
value={23}
/>
<PercentGroup
disabled={true}
id="assignmentGradeMax"
label="Max Grade"
onChange={[MockFunction]}
value={300}
/>
<div
className="grade-filter-action"
>
<Button
disabled={true}
name="assignmentGradeMinMax"
type="submit"
variant="outline-secondary"
>
Apply
</Button>
</div>
</div>
`;

View File

@@ -1,17 +1,15 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
/* eslint-disable import/no-extraneous-dependencies */
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PercentGroup from '../PercentGroup';
import useAssignmentGradeFilterData from './hooks';
import AssignmentFilter from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('../PercentGroup', () => 'PercentGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const hookData = {
handleChange: jest.fn(),
handleSubmit: jest.fn(),
handleSetMax: jest.fn(),
handleSetMin: jest.fn(),
selectedAssignment: 'test-assignment',
@@ -22,37 +20,39 @@ useAssignmentGradeFilterData.mockReturnValue(hookData);
const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilter component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
expect(useAssignmentGradeFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
describe('with selected assignment', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
beforeEach(() => {
jest.clearAllMocks();
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
it('renders a PercentGroup for both Max and Min filters', () => {
let { props } = el.instance.findByType(PercentGroup)[0];
expect(props.value).toEqual(hookData.assignmentGradeMin);
expect(props.disabled).toEqual(false);
expect(props.onChange).toEqual(hookData.handleSetMin);
props = el.instance.findByType(PercentGroup)[1].props;
expect(props.value).toEqual(hookData.assignmentGradeMax);
expect(props.disabled).toEqual(false);
expect(props.onChange).toEqual(hookData.handleSetMax);
it('renders a PercentGroup for both Max and Min filters', async () => {
const user = userEvent.setup();
const minGradeInput = screen.getByRole('spinbutton', { name: /Min Grade/i });
const maxGradeInput = screen.getByRole('spinbutton', { name: /Max Grade/i });
expect(minGradeInput).toBeInTheDocument();
expect(maxGradeInput).toBeInTheDocument();
expect(minGradeInput).toBeEnabled();
expect(maxGradeInput).toBeEnabled();
await user.type(minGradeInput, '25');
expect(hookData.handleSetMin).toHaveBeenCalled();
await user.type(maxGradeInput, '50');
expect(hookData.handleSetMax).toHaveBeenCalled();
});
it('renders a submit button', () => {
const { props } = el.instance.findByType(Button)[0];
expect(props.disabled).toEqual(false);
expect(props.onClick).toEqual(hookData.handleSubmit);
it('renders a submit button', async () => {
const user = userEvent.setup();
const submitButton = screen.getByRole('button', { name: /Apply/ });
expect(submitButton).toBeInTheDocument();
expect(submitButton).not.toHaveAttribute('disabled');
await user.click(submitButton);
expect(hookData.handleSubmit).toHaveBeenCalled();
});
});
describe('without selected assignment', () => {
@@ -61,16 +61,13 @@ describe('AssignmentFilter component', () => {
...hookData,
selectedAssignment: null,
});
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
it('disables controls', () => {
let { props } = el.instance.findByType(PercentGroup)[0];
expect(props.disabled).toEqual(true);
props = el.instance.findByType(PercentGroup)[1].props;
expect(props.disabled).toEqual(true);
const minGrade = screen.getByRole('spinbutton', { name: /Min Grade/ });
const maxGrade = screen.getByRole('spinbutton', { name: /Max Grade/ });
expect(minGrade).toHaveAttribute('disabled');
expect(maxGrade).toHaveAttribute('disabled');
});
});
});

View File

@@ -1,44 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AssignmentFilterType component render snapshot 1`] = `
<div
className="student-filters"
>
<SelectGroup
disabled={true}
id="assignment-types"
label="Assignment Types"
onChange={[MockFunction]}
options={
[
<option
value=""
>
All
</option>,
<option
value="test-type"
>
test-type
</option>,
<option
value="type1"
>
type1
</option>,
<option
value="type2"
>
type2
</option>,
<option
value="type3"
>
type3
</option>,
]
}
value="test-type"
/>
</div>
`;

View File

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

View File

@@ -1,63 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseFilter component render if disabled snapshot 1`] = `
<Fragment>
<div
className="grade-filter-inputs"
>
<PercentGroup
id="minimum-grade"
label="Min Grade"
onChange={[MockFunction]}
value={23}
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
onChange={[MockFunction]}
value={300}
/>
</div>
<div
className="grade-filter-action"
>
<Button
disabled={true}
variant="outline-secondary"
>
Apply
</Button>
</div>
</Fragment>
`;
exports[`CourseFilter component render with selected assignment snapshot 1`] = `
<Fragment>
<div
className="grade-filter-inputs"
>
<PercentGroup
id="minimum-grade"
label="Min Grade"
onChange={[MockFunction]}
value={23}
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
onChange={[MockFunction]}
value={300}
/>
</div>
<div
className="grade-filter-action"
>
<Button
disabled={false}
variant="outline-secondary"
>
Apply
</Button>
</div>
</Fragment>
`;

View File

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

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import PercentGroup from './PercentGroup';
@@ -12,6 +11,7 @@ describe('PercentGroup', () => {
};
beforeEach(() => {
initializeMocks();
props = {
...props,
onChange: jest.fn().mockName('props.onChange'),
@@ -19,15 +19,17 @@ describe('PercentGroup', () => {
});
describe('Component', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<PercentGroup {...props} />);
expect(el.snapshot).toMatchSnapshot();
});
test('disabled', () => {
const el = shallow(<PercentGroup {...props} disabled />);
expect(el.snapshot).toMatchSnapshot();
});
test('is displayed', () => {
render(<PercentGroup {...props} />);
expect(screen.getByRole('spinbutton', { name: 'Group Label' })).toBeInTheDocument();
expect(screen.getByText('Group Label')).toBeVisible();
expect(screen.getByText('%')).toBeVisible();
});
test('disabled', () => {
render(<PercentGroup {...props} disabled />);
expect(screen.getByRole('spinbutton', { name: 'Group Label' })).toBeDisabled();
expect(screen.getByText('Group Label')).toBeVisible();
expect(screen.getByText('%')).toBeVisible();
});
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import SelectGroup from './SelectGroup';
@@ -24,15 +24,14 @@ describe('SelectGroup', () => {
});
describe('Component', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<SelectGroup {...props} />);
expect(el.snapshot).toMatchSnapshot();
});
test('disabled', () => {
const el = shallow(<SelectGroup {...props} disabled />);
expect(el.snapshot).toMatchSnapshot();
});
test('rendered with all options and label', () => {
render(<SelectGroup {...props} />);
expect(screen.getAllByRole('option')).toHaveLength(props.options.length);
expect(screen.getByLabelText(props.label)).toBeInTheDocument();
});
test('disabled', () => {
render(<SelectGroup {...props} disabled />);
expect(screen.getByRole('combobox')).toBeDisabled();
});
});
});

View File

@@ -1,72 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StudentGroupsFilter component render snapshot 1`] = `
<Fragment>
<SelectGroup
id="Tracks"
label="Tracks"
onChange={[MockFunction]}
options={
[
<option
value="Track-All"
>
Track-All
</option>,
<option
value="v1"
>
n1
</option>,
<option
value="v2"
>
n2
</option>,
<option
value="v3"
>
n3
</option>,
<option
value="v4"
>
n4
</option>,
]
}
value="test-track"
/>
<SelectGroup
disabled={false}
id="Cohorts"
label="Cohorts"
onChange={[MockFunction]}
options={
[
<option
value="Cohort-All"
>
Cohort-All
</option>,
<option
value="v1"
>
n1
</option>,
<option
value="v2"
>
n2
</option>,
<option
value="v3"
>
n3
</option>,
]
}
value="test-cohort"
/>
</Fragment>
`;

View File

@@ -1,84 +1,164 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import SelectGroup from '../SelectGroup';
import { StudentGroupsFilter } from './index';
import useStudentGroupsFilterData from './hooks';
import StudentGroupsFilter from '.';
jest.mock('../SelectGroup', () => 'SelectGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
jest.mock('../SelectGroup', () => jest.fn(() => <div data-testid="select-group">SelectGroup</div>));
jest.mock('./hooks', () => jest.fn());
const props = {
cohorts: {
value: 'test-cohort',
initializeMocks();
describe('StudentGroupsFilter', () => {
const mockUpdateQueryParams = jest.fn();
const mockTracksData = {
value: 'test-track-value',
entries: [
{ value: 'v1', name: 'n1' },
{ value: 'v2', name: 'n2' },
{ value: 'v3', name: 'n3' },
{ value: 'track1', name: 'Track 1' },
{ value: 'track2', name: 'Track 2' },
],
handleChange: jest.fn(),
};
const mockCohortsData = {
value: 'test-cohort-value',
entries: [
{ value: 'cohort1', name: 'Cohort 1' },
{ value: 'cohort2', name: 'Cohort 2' },
],
handleChange: jest.fn(),
isDisabled: false,
},
tracks: {
value: 'test-track',
entries: [
{ value: 'v1', name: 'n1' },
{ value: 'v2', name: 'n2' },
{ value: 'v3', name: 'n3' },
{ value: 'v4', name: 'n4' },
],
handleChange: jest.fn(),
},
};
useStudentGroupsFilterData.mockReturnValue(props);
const updateQueryParams = jest.fn();
};
let el;
describe('StudentGroupsFilter component', () => {
beforeAll(() => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<StudentGroupsFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
useStudentGroupsFilterData.mockReturnValue({
tracks: mockTracksData,
cohorts: mockCohortsData,
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
it('calls useStudentGroupsFilterData hook with updateQueryParams', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({
updateQueryParams: mockUpdateQueryParams,
});
test('track options', () => {
const {
options,
onChange,
value,
} = el.instance.findByType(SelectGroup)[0].props;
expect(value).toEqual(props.tracks.value);
expect(onChange).toEqual(props.tracks.handleChange);
expect(options.length).toEqual(5);
const testEntry = props.tracks.entries[0];
const optionProps = options[1].props;
expect(optionProps.value).toEqual(testEntry.value);
expect(optionProps.children).toEqual(testEntry.name);
});
it('renders two SelectGroup components', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
expect(SelectGroup).toHaveBeenCalledTimes(2);
expect(screen.getAllByTestId('select-group')).toHaveLength(2);
});
describe('tracks SelectGroup', () => {
it('renders tracks SelectGroup with correct props', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const tracksCall = SelectGroup.mock.calls[0][0];
expect(tracksCall.id).toBe('Tracks');
expect(tracksCall.value).toBe(mockTracksData.value);
expect(tracksCall.onChange).toBe(mockTracksData.handleChange);
});
test('cohort options', () => {
const {
options,
onChange,
disabled,
value,
} = el.instance.findByType(SelectGroup)[1].props;
expect(value).toEqual(props.cohorts.value);
expect(disabled).toEqual(false);
expect(onChange).toEqual(props.cohorts.handleChange);
expect(options.length).toEqual(4);
const testEntry = props.cohorts.entries[0];
const optionProps = options[1].props;
expect(optionProps.value).toEqual(testEntry.value);
expect(optionProps.children).toEqual(testEntry.name);
it('includes trackAll option in tracks SelectGroup', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const tracksCall = SelectGroup.mock.calls[0][0];
const { options } = tracksCall;
expect(options).toHaveLength(3);
expect(options[0].props.value).toBeDefined();
expect(options[0].props.children).toBeDefined();
});
it('includes track entries in tracks SelectGroup options', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const tracksCall = SelectGroup.mock.calls[0][0];
const { options } = tracksCall;
expect(options[1].props.value).toBe('track1');
expect(options[1].props.children).toBe('Track 1');
expect(options[2].props.value).toBe('track2');
expect(options[2].props.children).toBe('Track 2');
});
});
describe('cohorts SelectGroup', () => {
it('renders cohorts SelectGroup with correct props', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
expect(cohortsCall.id).toBe('Cohorts');
expect(cohortsCall.value).toBe(mockCohortsData.value);
expect(cohortsCall.onChange).toBe(mockCohortsData.handleChange);
expect(cohortsCall.disabled).toBe(mockCohortsData.isDisabled);
});
it('includes cohortAll option in cohorts SelectGroup', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
const { options } = cohortsCall;
expect(options).toHaveLength(3);
expect(options[0].props.value).toBeDefined();
expect(options[0].props.children).toBeDefined();
});
it('includes cohort entries in cohorts SelectGroup options', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
const { options } = cohortsCall;
expect(options[1].props.value).toBe('cohort1');
expect(options[1].props.children).toBe('Cohort 1');
expect(options[2].props.value).toBe('cohort2');
expect(options[2].props.children).toBe('Cohort 2');
});
it('passes disabled state to cohorts SelectGroup', () => {
useStudentGroupsFilterData.mockReturnValue({
tracks: mockTracksData,
cohorts: { ...mockCohortsData, isDisabled: true },
});
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
expect(cohortsCall.disabled).toBe(true);
});
});
describe('with empty entries', () => {
it('handles empty tracks entries', () => {
useStudentGroupsFilterData.mockReturnValue({
tracks: { ...mockTracksData, entries: [] },
cohorts: mockCohortsData,
});
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const tracksCall = SelectGroup.mock.calls[0][0];
expect(tracksCall.options).toHaveLength(1);
});
it('handles empty cohorts entries', () => {
useStudentGroupsFilterData.mockReturnValue({
tracks: mockTracksData,
cohorts: { ...mockCohortsData, entries: [] },
});
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
expect(cohortsCall.options).toHaveLength(1);
});
});
});

View File

@@ -1,57 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PercentGroup Component snapshots basic snapshot 1`] = `
<div
className="percent-group"
>
<Form.Group
controlId="group id"
>
<Form.Label>
Group Label
</Form.Label>
<Form.Control
disabled={false}
max={100}
min={0}
onChange={[MockFunction props.onChange]}
step={1}
type="number"
value="group VALUE"
/>
</Form.Group>
<span
className="input-percent-label"
>
%
</span>
</div>
`;
exports[`PercentGroup Component snapshots disabled 1`] = `
<div
className="percent-group"
>
<Form.Group
controlId="group id"
>
<Form.Label>
Group Label
</Form.Label>
<Form.Control
disabled={true}
max={100}
min={0}
onChange={[MockFunction props.onChange]}
step={1}
type="number"
value="group VALUE"
/>
</Form.Group>
<span
className="input-percent-label"
>
%
</span>
</div>
`;

View File

@@ -1,79 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectGroup Component snapshots basic snapshot 1`] = `
<div
className="student-filters"
>
<Form.Group
controlId="group id"
>
<Form.Label>
Group Label
</Form.Label>
<Form.Control
as="select"
disabled={false}
onChange={[MockFunction props.onChange]}
value="group VALUE"
>
<option
key="opt1"
value="opt1"
>
Option 1
</option>
<option
key="opt2"
value="opt2"
>
Option 2
</option>
<option
key="opt3"
value="opt3"
>
Option 3
</option>
</Form.Control>
</Form.Group>
</div>
`;
exports[`SelectGroup Component snapshots disabled 1`] = `
<div
className="student-filters"
>
<Form.Group
controlId="group id"
>
<Form.Label>
Group Label
</Form.Label>
<Form.Control
as="select"
disabled={true}
onChange={[MockFunction props.onChange]}
value="group VALUE"
>
<option
key="opt1"
value="opt1"
>
Option 1
</option>
<option
key="opt2"
value="opt2"
>
Option 2
</option>
<option
key="opt3"
value="opt3"
>
Option 3
</option>
</Form.Control>
</Form.Group>
</div>
`;

View File

@@ -1,70 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookFilters render snapshot 1`] = `
<Fragment>
<div
className="filter-sidebar-header"
>
<h2>
<Icon
className="fa fa-filter"
/>
</h2>
<IconButton
alt="Close Filters"
aria-label="Close Filters"
className="p-1"
iconAs="Icon"
onClick={[MockFunction hook.closeMenu]}
src="Close"
/>
</div>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Assignments"
>
<div>
<AssignmentTypeFilter
updateQueryParams={[MockFunction]}
/>
<AssignmentFilter
updateQueryParams={[MockFunction]}
/>
<AssignmentGradeFilter
updateQueryParams={[MockFunction]}
/>
</div>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Overall Grade"
>
<CourseGradeFilter
updateQueryParams={[MockFunction]}
/>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Student Groups"
>
<StudentGroupsFilter
updateQueryParams={[MockFunction]}
/>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Include Course Team Members"
>
<Form.Checkbox
checked={true}
onChange={[MockFunction hook.handleChange]}
>
Include Course Team Members
</Form.Checkbox>
</Collapsible>
</Fragment>
`;

View File

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

View File

@@ -1,139 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookHeader component render default view shapshot 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
</div>
</div>
`;
exports[`GradebookHeader component render frozen grades snapshot: show frozen warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
</div>
<div
className="alert alert-warning"
role="alert"
>
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
</div>
`;
exports[`GradebookHeader component render show bulk management snapshot: show toggle view message button with handleToggleViewClick method 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
<Button
onClick={[MockFunction hooks.handleToggleViewClick]}
variant="tertiary"
>
toggle-view-message
</Button>
</div>
</div>
`;
exports[`GradebookHeader component render user cannot view gradebook snapshot: show unauthorized warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
</div>
<div
className="alert alert-warning"
role="alert"
>
You are not authorized to view the gradebook for this course.
</div>
</div>
`;

View File

@@ -1,77 +1,300 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import userEvent from '@testing-library/user-event';
import { formatMessage } from 'testUtils';
import { instructorDashboardUrl } from 'data/services/lms/urls';
import { GradebookHeader } from './index';
import useGradebookHeaderData from './hooks';
import GradebookHeader from '.';
import messages from './messages';
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
jest.mock('data/services/lms/urls', () => ({
instructorDashboardUrl: jest.fn(),
}));
jest.mock('./hooks', () => jest.fn());
instructorDashboardUrl.mockReturnValue('test-dashboard-url');
initializeMocks();
const hookProps = {
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'test-course-id',
handleToggleViewClick: jest.fn().mockName('hooks.handleToggleViewClick'),
showBulkManagement: false,
toggleViewMessage: { defaultMessage: 'toggle-view-message' },
};
useGradebookHeaderData.mockReturnValue(hookProps);
describe('GradebookHeader', () => {
const mockHandleToggleViewClick = jest.fn();
let el;
describe('GradebookHeader component', () => {
beforeAll(() => {
el = shallow(<GradebookHeader />);
beforeEach(() => {
jest.clearAllMocks();
instructorDashboardUrl.mockReturnValue('https://example.com/dashboard');
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useGradebookHeaderData).toHaveBeenCalledWith();
expect(useIntl).toHaveBeenCalledWith();
describe('basic rendering', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
it('renders the main header container', () => {
render(<GradebookHeader />);
const header = screen.getByText('Gradebook').closest('.gradebook-header');
expect(header).toHaveClass('gradebook-header');
});
it('renders back to dashboard link', () => {
render(<GradebookHeader />);
const dashboardLink = screen.getByRole('link');
expect(dashboardLink).toHaveAttribute(
'href',
'https://example.com/dashboard',
);
expect(dashboardLink).toHaveClass('mb-3');
expect(dashboardLink).toHaveTextContent('Back to Dashboard');
});
it('renders gradebook title', () => {
render(<GradebookHeader />);
const title = screen.getByRole('heading', { level: 1 });
expect(title).toHaveTextContent('Gradebook');
});
it('renders course ID subtitle', () => {
render(<GradebookHeader />);
const subtitle = screen.getByRole('heading', { level: 2 });
expect(subtitle).toHaveTextContent('course-v1:TestU+CS101+2024');
expect(subtitle).toHaveClass('text-break');
});
it('renders subtitle row with correct classes', () => {
render(<GradebookHeader />);
const subtitleRow = screen.getByRole('heading', {
level: 2,
}).parentElement;
expect(subtitleRow).toHaveClass(
'subtitle-row',
'd-flex',
'justify-content-between',
'align-items-center',
);
});
it('calls instructorDashboardUrl to get dashboard URL', () => {
render(<GradebookHeader />);
expect(instructorDashboardUrl).toHaveBeenCalled();
});
it('calls useGradebookHeaderData hook', () => {
render(<GradebookHeader />);
expect(useGradebookHeaderData).toHaveBeenCalled();
});
});
describe('render', () => {
describe('default view', () => {
test('shapshot', () => {
expect(el.snapshot).toMatchSnapshot();
describe('bulk management toggle button', () => {
describe('when showBulkManagement is true', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: true,
toggleViewMessage: messages.toActivityLog,
});
});
it('renders toggle view button', () => {
render(<GradebookHeader />);
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('displays correct button text from toggleViewMessage', () => {
render(<GradebookHeader />);
const toggleButton = screen.getByRole('button');
expect(toggleButton).toHaveTextContent('View Bulk Management History');
});
it('calls handleToggleViewClick when button is clicked', async () => {
render(<GradebookHeader />);
const user = userEvent.setup();
const toggleButton = screen.getByRole('button');
await user.click(toggleButton);
expect(mockHandleToggleViewClick).toHaveBeenCalledTimes(1);
});
it('displays correct message from toggleViewMessage', () => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: true,
toggleViewMessage: messages.toGradesView,
});
render(<GradebookHeader />);
const toggleButton = screen.getByRole('button');
expect(toggleButton).toHaveTextContent('Return to Gradebook');
});
});
describe('show bulk management', () => {
describe('when showBulkManagement is false', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true });
el = shallow(<GradebookHeader />);
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
test('snapshot: show toggle view message button with handleToggleViewClick method', () => {
expect(el.snapshot).toMatchSnapshot();
const { onClick } = el.instance.findByType(Button)[0].props;
expect(onClick).toEqual(hookProps.handleToggleViewClick);
expect(el.instance.findByType(Button)[0].children[0].el).toEqual(formatMessage(hookProps.toggleViewMessage));
it('does not render toggle view button', () => {
render(<GradebookHeader />);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});
describe('frozen grades', () => {
});
describe('frozen grades warning', () => {
describe('when areGradesFrozen is true', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true });
el = shallow(<GradebookHeader />);
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: true,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
test('snapshot: show frozen warning', () => {
expect(el.snapshot).toMatchSnapshot();
it('renders frozen warning alert', () => {
render(<GradebookHeader />);
const alert = screen.getByRole('alert');
expect(alert).toHaveClass('alert', 'alert-warning');
expect(alert).toHaveTextContent(
'The grades for this course are now frozen. Editing of grades is no longer allowed.',
);
});
});
describe('user cannot view gradebook', () => {
describe('when areGradesFrozen is false', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false });
el = shallow(<GradebookHeader />);
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
test('snapshot: show unauthorized warning', () => {
expect(el.snapshot).toMatchSnapshot();
it('does not render frozen warning alert', () => {
render(<GradebookHeader />);
expect(
screen.queryByText(
'The grades for this course are now frozen. Editing of grades is no longer allowed.',
),
).not.toBeInTheDocument();
});
});
});
describe('unauthorized warning', () => {
describe('when canUserViewGradebook is false', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: false,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
it('renders unauthorized warning alert', () => {
render(<GradebookHeader />);
const alert = screen.getByRole('alert');
expect(alert).toHaveClass('alert', 'alert-warning');
expect(alert).toHaveTextContent(
'You are not authorized to view the gradebook for this course.',
);
});
});
describe('when canUserViewGradebook is true', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
it('does not render unauthorized warning alert', () => {
render(<GradebookHeader />);
expect(
screen.queryByText(
'You are not authorized to view the gradebook for this course.',
),
).not.toBeInTheDocument();
});
});
});
describe('multiple warnings', () => {
it('renders both frozen and unauthorized warnings when both conditions are true', () => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: true,
canUserViewGradebook: false,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
render(<GradebookHeader />);
const alerts = screen.getAllByRole('alert');
expect(alerts).toHaveLength(2);
expect(
screen.getByText(
'The grades for this course are now frozen. Editing of grades is no longer allowed.',
),
).toBeInTheDocument();
expect(
screen.getByText(
'You are not authorized to view the gradebook for this course.',
),
).toBeInTheDocument();
});
});
describe('complete integration', () => {
it('renders all elements when showBulkManagement is true', () => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: true,
toggleViewMessage: messages.toActivityLog,
});
render(<GradebookHeader />);
expect(screen.getByRole('link')).toBeInTheDocument();
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
});
});

View File

@@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = `
<div
className="d-flex"
>
<NetworkButton
label={
{
"defaultMessage": "Download Grades",
"description": "A labeled button that allows an admin user to download course grades all at once (in bulk).",
"id": "gradebook.GradesView.BulkManagementControls.bulkManagementLabel",
}
}
onClick={[MockFunction]}
/>
<ImportGradesButton />
</div>
`;

View File

@@ -1,32 +1,160 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import NetworkButton from 'components/NetworkButton';
import ImportGradesButton from '../ImportGradesButton';
import { BulkManagementControls } from './index';
import useBulkManagementControlsData from './hooks';
import BulkManagementControls from '.';
jest.mock('../ImportGradesButton', () => 'ImportGradesButton');
jest.mock('components/NetworkButton', () => 'NetworkButton');
import messages from './messages';
jest.mock('components/NetworkButton', () => jest.fn(() => <div data-testid="network-button">NetworkButton</div>));
jest.mock('../ImportGradesButton', () => jest.fn(() => (
<div data-testid="import-grades-button">ImportGradesButton</div>
)));
jest.mock('./hooks', () => jest.fn());
const hookProps = {
show: true,
handleClickExportGrades: jest.fn(),
};
useBulkManagementControlsData.mockReturnValue(hookProps);
initializeMocks();
describe('BulkManagementControls', () => {
describe('behavior', () => {
shallow(<BulkManagementControls />);
expect(useBulkManagementControlsData).toHaveBeenCalledWith();
const mockHandleClickExportGrades = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('render', () => {
test('snapshot - show - network and import buttons', () => {
expect(shallow(<BulkManagementControls />).snapshot).toMatchSnapshot();
describe('when show is false', () => {
beforeEach(() => {
useBulkManagementControlsData.mockReturnValue({
show: false,
handleClickExportGrades: mockHandleClickExportGrades,
});
});
test('snapshot - empty if show is not truthy', () => {
useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false });
expect(shallow(<BulkManagementControls />).isEmptyRender()).toEqual(true);
it('renders nothing when show is false', () => {
render(<BulkManagementControls />);
expect(screen.queryByTestId('network-button')).not.toBeInTheDocument();
expect(
screen.queryByTestId('import-grades-button'),
).not.toBeInTheDocument();
});
it('does not render NetworkButton when show is false', () => {
render(<BulkManagementControls />);
expect(NetworkButton).not.toHaveBeenCalled();
});
it('does not render ImportGradesButton when show is false', () => {
render(<BulkManagementControls />);
expect(ImportGradesButton).not.toHaveBeenCalled();
});
});
describe('when show is true', () => {
beforeEach(() => {
useBulkManagementControlsData.mockReturnValue({
show: true,
handleClickExportGrades: mockHandleClickExportGrades,
});
});
it('renders the container div with correct class when show is true', () => {
render(<BulkManagementControls />);
const containerDiv = screen.getByTestId('network-button').parentElement;
expect(containerDiv).toHaveClass('d-flex');
});
it('renders NetworkButton with correct props', () => {
render(<BulkManagementControls />);
expect(NetworkButton).toHaveBeenCalledWith(
{
label: messages.downloadGradesBtn,
onClick: mockHandleClickExportGrades,
},
{},
);
expect(screen.getByTestId('network-button')).toBeInTheDocument();
});
it('renders ImportGradesButton', () => {
render(<BulkManagementControls />);
expect(ImportGradesButton).toHaveBeenCalledWith({}, {});
expect(screen.getByTestId('import-grades-button')).toBeInTheDocument();
});
it('calls handleClickExportGrades when NetworkButton is clicked', () => {
render(<BulkManagementControls />);
const networkButtonCall = NetworkButton.mock.calls[0][0];
const { onClick } = networkButtonCall;
onClick();
expect(mockHandleClickExportGrades).toHaveBeenCalledTimes(1);
});
it('passes correct label to NetworkButton', () => {
render(<BulkManagementControls />);
const networkButtonCall = NetworkButton.mock.calls[0][0];
expect(networkButtonCall.label).toBe(messages.downloadGradesBtn);
});
it('renders both buttons in the correct order', () => {
render(<BulkManagementControls />);
expect(NetworkButton).toHaveBeenCalled();
expect(ImportGradesButton).toHaveBeenCalled();
const networkButton = screen.getByTestId('network-button');
const importButton = screen.getByTestId('import-grades-button');
expect(networkButton).toBeInTheDocument();
expect(importButton).toBeInTheDocument();
});
});
describe('hook integration', () => {
it('calls useBulkManagementControlsData hook', () => {
useBulkManagementControlsData.mockReturnValue({
show: true,
handleClickExportGrades: mockHandleClickExportGrades,
});
render(<BulkManagementControls />);
expect(useBulkManagementControlsData).toHaveBeenCalledTimes(1);
});
it('uses the show value from hook to determine rendering', () => {
useBulkManagementControlsData.mockReturnValue({
show: false,
handleClickExportGrades: mockHandleClickExportGrades,
});
render(<BulkManagementControls />);
expect(screen.queryByTestId('network-button')).not.toBeInTheDocument();
useBulkManagementControlsData.mockReturnValue({
show: true,
handleClickExportGrades: mockHandleClickExportGrades,
});
render(<BulkManagementControls />);
expect(screen.getByTestId('network-button')).toBeInTheDocument();
});
it('passes handleClickExportGrades from hook to NetworkButton', () => {
const customHandler = jest.fn();
useBulkManagementControlsData.mockReturnValue({
show: true,
handleClickExportGrades: customHandler,
});
render(<BulkManagementControls />);
const networkButtonCall = NetworkButton.mock.calls[0][0];
expect(networkButtonCall.onClick).toBe(customHandler);
});
});
});

View File

@@ -1,17 +1,100 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import HistoryHeader from './HistoryHeader';
initializeMocks();
describe('HistoryHeader', () => {
const props = {
id: 'water',
label: 'Brita',
value: 'hydration',
const defaultProps = {
id: 'test-id',
label: 'Test Label',
value: 'Test Value',
};
describe('Component', () => {
test('snapshot', () => {
expect(shallow(<HistoryHeader {...props} />).snapshot).toMatchSnapshot();
});
it('renders header with label and value', () => {
render(<HistoryHeader {...defaultProps} />);
expect(screen.getByText('Test Label:')).toBeInTheDocument();
expect(screen.getByText('Test Value')).toBeInTheDocument();
});
it('renders header element with correct classes', () => {
render(<HistoryHeader {...defaultProps} />);
const headerElement = screen.getByText('Test Label:');
expect(headerElement).toHaveClass('grade-history-header');
expect(headerElement).toHaveClass('grade-history-test-id');
});
it('renders with string value', () => {
const props = {
...defaultProps,
value: 'String Value',
};
render(<HistoryHeader {...props} />);
expect(screen.getByText('String Value')).toBeInTheDocument();
});
it('renders with number value', () => {
const props = {
...defaultProps,
value: 85,
};
render(<HistoryHeader {...props} />);
expect(screen.getByText('85')).toBeInTheDocument();
});
it('renders with null value (default prop)', () => {
const props = {
id: 'test-id',
label: 'Test Label',
};
render(<HistoryHeader {...props} />);
expect(screen.getByText('Test Label:')).toBeInTheDocument();
const valueDiv = screen.getByText('Test Label:').nextSibling;
expect(valueDiv).toBeInTheDocument();
expect(valueDiv).toBeEmptyDOMElement();
});
it('renders with React node as label', () => {
const props = {
...defaultProps,
label: <strong>Bold Label</strong>,
};
render(<HistoryHeader {...props} />);
const strongElement = screen.getByText('Bold Label');
expect(strongElement.tagName).toBe('STRONG');
});
it('generates correct class name based on id', () => {
const props = {
...defaultProps,
id: 'assignment-name',
};
render(<HistoryHeader {...props} />);
const headerElement = screen.getByText('Test Label:');
expect(headerElement).toHaveClass('grade-history-assignment-name');
});
it('renders container structure correctly', () => {
render(<HistoryHeader {...defaultProps} />);
const headerElement = screen.getByText('Test Label:');
const valueElement = screen.getByText('Test Value');
expect(headerElement).toBeInTheDocument();
expect(valueElement).toBeInTheDocument();
expect(headerElement).toHaveClass(
'grade-history-header',
'grade-history-test-id',
);
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,12 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn((val) => ({ current: val, useRef: true })),
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
}));
const modalData = { reasonForChange: 'test-reason-for-change' };
const setModalState = jest.fn();
selectors.app.useModalData.mockReturnValue(modalData);
@@ -25,6 +31,7 @@ const ref = { current: { focus: jest.fn() }, useRef: true };
React.useRef.mockReturnValue(ref);
let out;
describe('useReasonInputData hook', () => {
beforeEach(() => {
jest.clearAllMocks();

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
import { selectors } from 'data/redux/hooks';
@@ -16,6 +15,18 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn(() => ({
formatMessage: (message) => message.defaultMessage,
})),
}));
selectors.grades.useHasOverrideErrors.mockReturnValue(false);
const gradeOverrides = ['some', 'override', 'data'];
const gradeData = { gradeOverrideHistoryResults: gradeOverrides };
@@ -44,22 +55,22 @@ describe('useOverrideTableData', () => {
describe('columns', () => {
test('date column', () => {
const { Header, accessor } = out.columns[0];
expect(Header).toEqual(formatMessage(messages.dateHeader));
expect(Header).toEqual(messages.dateHeader.defaultMessage);
expect(accessor).toEqual(columns.date);
});
test('grader column', () => {
const { Header, accessor } = out.columns[1];
expect(Header).toEqual(formatMessage(messages.graderHeader));
expect(Header).toEqual(messages.graderHeader.defaultMessage);
expect(accessor).toEqual(columns.grader);
});
test('reason column', () => {
const { Header, accessor } = out.columns[2];
expect(Header).toEqual(formatMessage(messages.reasonHeader));
expect(Header).toEqual(messages.reasonHeader.defaultMessage);
expect(accessor).toEqual(columns.reason);
});
test('adjustedGrade column', () => {
const { Header, accessor } = out.columns[3];
expect(Header).toEqual(formatMessage(messages.adjustedGradeHeader));
expect(Header).toEqual(messages.adjustedGradeHeader.defaultMessage);
expect(accessor).toEqual(columns.adjustedGrade);
});
});

View File

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

View File

@@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistoryHeader Component snapshot 1`] = `
<div>
<div
className="grade-history-header grade-history-water"
>
Brita
:
</div>
<div>
hydration
</div>
</div>
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,18 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn(() => ({
formatMessage: (message) => message.defaultMessage,
})),
}));
const setView = jest.fn().mockName('hooks.setView');
const setShowToast = jest.fn().mockName('hooks.setShowImportSuccessToast');
actions.app.useSetView.mockReturnValue(setView);

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,18 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn(() => ({
formatMessage: (message) => message.defaultMessage,
})),
}));
const gradeData = { nextPage: 'test-next-page', prevPage: 'test-prev-page' };
selectors.grades.useGradeData.mockReturnValue(gradeData);

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,18 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn(() => ({
formatMessage: (message) => message.defaultMessage,
})),
}));
const searchValue = 'test-search-value';
selectors.app.useSearchValue.mockReturnValue(searchValue);
const setSearchValue = jest.fn();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Alert } from '@openedx/paragon';
import { render, screen } from '@testing-library/react';
import useStatusAlertsData from './hooks';
import StatusAlerts from '.';
@@ -11,21 +9,20 @@ jest.mock('./hooks', () => jest.fn());
const hookProps = {
successBanner: {
onClose: jest.fn().mockName('hooks.successBanner.onClose'),
show: 'hooks.show-success-banner',
show: true,
text: 'hooks.success-banner-text',
},
gradeFilter: {
show: 'hooks.show-grade-filter',
show: true,
text: 'hooks.grade-filter-text',
},
};
useStatusAlertsData.mockReturnValue(hookProps);
let el;
describe('StatusAlerts component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<StatusAlerts />);
render(<StatusAlerts />);
});
describe('behavior', () => {
it('initializes component hooks', () => {
@@ -33,21 +30,17 @@ describe('StatusAlerts component', () => {
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
it('success banner', () => {
const alerts = screen.getAllByRole('alert');
const successAlert = alerts[0];
expect(successAlert).toHaveTextContent(hookProps.successBanner.text);
expect(successAlert).toHaveClass('alert-success');
});
test('success banner', () => {
const alert = el.instance.findByType(Alert)[0];
const { props } = alert;
expect(props.onClose).toEqual(hookProps.successBanner.onClose);
expect(props.show).toEqual(hookProps.successBanner.show);
expect(alert.children[0].el).toEqual(hookProps.successBanner.text);
});
test('grade filter banner', () => {
const alert = el.instance.findByType(Alert)[1];
const { props } = alert;
expect(props.show).toEqual(hookProps.gradeFilter.show);
expect(alert.children[0].el).toEqual(hookProps.gradeFilter.text);
it('grade filter banner', () => {
const alerts = screen.getAllByRole('alert');
const gradeFilter = alerts[1];
expect(gradeFilter).toHaveTextContent(hookProps.gradeFilter.text);
expect(gradeFilter).toHaveClass('alert-danger');
});
});
});

View File

@@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SpinnerIcon component snapshot - displays spinner overlay with spinner icon 1`] = `
<div
className="spinner-overlay"
>
<Icon
className="fa fa-spinner fa-spin fa-5x color-black"
/>
</div>
`;

View File

@@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradesView component render snapshot 1`] = `
<Fragment>
<SpinnerIcon />
<InterventionsReport />
<h3
className="step-message-1"
>
filter-step-heading
</h3>
<div
className="d-flex justify-content-between flex-wrap"
>
<FilterMenuToggle />
<SearchControls />
</div>
<FilterBadges
handleClose={[MockFunction hooks.handleFilterBadgeClose]}
/>
<StatusAlerts />
<h3>
gradebook-step-heading
</h3>
<div
className="d-flex justify-content-between align-items-center mb-2"
>
<ScoreViewInput />
<BulkManagementControls />
</div>
<FilteredUsersLabel />
<GradebookTable />
<PageButtons />
<p>
*
test-masters-hint
</p>
<EditModal />
<ImportSuccessToast />
</Fragment>
`;

View File

@@ -1,6 +1,5 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { actions, thunkActions } from 'data/redux/hooks';
import useGradesViewData from './hooks';
@@ -15,6 +14,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 fetchGrades = jest.fn();
thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades);
const resetFilters = jest.fn();
@@ -39,11 +50,11 @@ describe('useGradesViewData', () => {
});
describe('output', () => {
test('stepHeadings', () => {
expect(out.stepHeadings.filter).toEqual(formatMessage(messages.filterStepHeading));
expect(out.stepHeadings.gradebook).toEqual(formatMessage(messages.gradebookStepHeading));
expect(out.stepHeadings.filter).toEqual(messages.filterStepHeading.defaultMessage);
expect(out.stepHeadings.gradebook).toEqual(messages.gradebookStepHeading.defaultMessage);
});
test('mastersHint', () => {
expect(out.mastersHint).toEqual(formatMessage(messages.mastersHint));
expect(out.mastersHint).toEqual(messages.mastersHint.defaultMessage);
});
describe('handleFilterBadgeClose', () => {
it('resets filters locally and in query params, and fetches grades', () => {

View File

@@ -1,24 +1,11 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import FilterBadges from './FilterBadges';
// eslint-disable-next-line import/no-extraneous-dependencies
import userEvent from '@testing-library/user-event';
import { render, initializeMocks } from 'testUtilsExtra';
import useGradesViewData from './hooks';
import GradesView from '.';
jest.mock('./BulkManagementControls', () => 'BulkManagementControls');
jest.mock('./EditModal', () => 'EditModal');
jest.mock('./FilterBadges', () => 'FilterBadges');
jest.mock('./FilteredUsersLabel', () => 'FilteredUsersLabel');
jest.mock('./FilterMenuToggle', () => 'FilterMenuToggle');
jest.mock('./GradebookTable', () => 'GradebookTable');
jest.mock('./ImportSuccessToast', () => 'ImportSuccessToast');
jest.mock('./InterventionsReport', () => 'InterventionsReport');
jest.mock('./PageButtons', () => 'PageButtons');
jest.mock('./ScoreViewInput', () => 'ScoreViewInput');
jest.mock('./SearchControls', () => 'SearchControls');
jest.mock('./SpinnerIcon', () => 'SpinnerIcon');
jest.mock('./StatusAlerts', () => 'StatusAlerts');
jest.mock('./hooks', () => jest.fn());
const hookProps = {
@@ -35,23 +22,21 @@ const updateQueryParams = jest.fn().mockName('props.updateQueryParams');
let el;
describe('GradesView component', () => {
beforeAll(() => {
initializeMocks();
});
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<GradesView updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes component hooks', () => {
expect(useGradesViewData).toHaveBeenCalled();
});
el = render(<GradesView updateQueryParams={updateQueryParams} />);
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
test('component to be rendered', () => {
expect(el.container).toBeInTheDocument();
});
test('filterBadges load close behavior from hook', () => {
expect(el.instance.findByType(FilterBadges)[0].props.handleClose).toEqual(
hookProps.handleFilterBadgeClose,
);
test('filterBadges load close behavior from hook', async () => {
const user = userEvent.setup();
await user.click(el.getAllByRole('button', { name: 'close' })[0]); // All the buttons use the same handler
expect(hookProps.handleFilterBadgeClose).toHaveBeenCalled();
});
});
});

View File

@@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NetworkButton component snapshots snapshot 1`] = `
<StatefulButton
className="ml-2 test-class"
disabledStates={
[
"pending",
]
}
icons={
{
"default": <Icon
className="fa mr-2 fa-download"
/>,
"pending": <Icon
className="fa mr-2 fa-spinner fa-spin"
/>,
}
}
labels={
{
"default": <FormattedMessage
defaultMessage="test button label"
description="test button label description"
id="label-id"
showSpinner={false}
/>,
"pending": <FormattedMessage
defaultMessage="test button label"
description="test button label description"
id="label-id"
showSpinner={false}
/>,
}
}
onClick={[MockFunction]}
state="default"
variant="outline-primary"
/>
`;

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