Compare commits

...

144 Commits

Author SHA1 Message Date
Brian Smith
8a2c6aae3b fix(deps): regenerate package-lock.json (#478)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-02-13 16:56:52 -05:00
Anton Melser
d4c6d93c26 docs: include 'standard' dev instructions 2026-01-28 12:00:10 -03:00
Anton Melser
0ff61e11d5 docs: Generify currently supported node version 2026-01-28 12:00:10 -03:00
Jansen Kantor
d9e5ae0c80 Merge pull request #471 from openedx/jkantor/prompt
feat: add prompt to grading screen
2026-01-08 13:17:54 -07:00
Jansen Kantor
3c03358d4e fix: failing tests 2026-01-08 15:14:48 -05:00
Jansen Kantor
f7e6e30d99 style: formatting 2026-01-08 14:57:56 -05:00
Jansen Kantor
ae365b6951 test: add test coverage 2025-11-05 15:49:26 -05:00
Jansen Kantor
729cb40c66 feat: add prompt to grading screen 2025-11-05 15:10:01 -05:00
Muhammad Anas
bc4abcdeef chore: update frontend-component-header to v8 (#468) 2025-11-03 16:38:29 -05:00
Feanil Patel
508c91d487 Merge pull request #470 from openedx/feanil/node24
build: Upgrade to node 24 and update package-lock.json
2025-10-02 13:54:04 -04:00
Feanil Patel
28634843d0 build: Upgrade to node 24 and update package-lock.json
Resolves https://github.com/openedx/frontend-app-ora-grading/issues/438
2025-10-02 13:51:27 -04:00
Feanil Patel
b7b9b9d81d Merge pull request #469 from openedx/feanil/remove-reactifex-packages
build: remove unused reactifex packages
2025-09-26 13:21:03 -04:00
Adolfo R. Brandes
bab0962b6d Merge branch 'master' into feanil/remove-reactifex-packages 2025-09-26 14:18:18 -03:00
Feanil Patel
c564150cb5 build: remove unused reactifex packages
Remove reactifex and @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 11:02:18 -04:00
vladislavkeblysh
f438360fdb feat: Ora mobile responsive (#336)
Co-authored-by: ihor-romaniuk <ihor.romaniuk@raccoongang.com>
2025-09-24 10:14:42 -03:00
vladislavkeblysh
4ce7209230 fix: image rendering and blockquote styles (#333) 2025-09-23 11:19:36 -03:00
Samuel Allan
266589bca6 chore(deps): update frontend-build to fix install issues (#464)
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

Co-authored-by: Braden MacDonald <mail@bradenm.com>
2025-09-05 17:34:04 +00:00
Diana Villalvazo
a579455e58 test: Remove unwanted mocks, unmocks and deprecate react-unit-test-utils package (#465)
* test: remove last snapshot

* test: remove unwanted mocks, remove unmocks, refactor renderWithIntl

* test: refactor renderWithIntl with small improvements

* test: remove react-unit-test-utils

* test: change fireEvent for userEvent
2025-09-05 12:34:28 -04:00
Victor Navarro
b10aa63723 test: deprecate react-unit-test-utils part-5 (#439)
* test: deprecate react-unit-test-utils part-5

* test: fixes due to rebase

* test: change fireEvent to userEvent

* test: improve tests and address feedback

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
2025-09-04 10:16:52 -04:00
Victor Navarro
377bb6bbc3 test: deprecate react-unit-test-utils part-4 (#462)
* test: deprecate react-unit-test-utils part-4

* test: address feedback

* test: address feedback

* test: improve tests and address feedback

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
2025-09-03 14:39:49 -04:00
Victor Navarro
ac03594943 test: deprecate react-unit-test-utils part-8 (#444)
* test: deprecate react-unit-test-utils part-8

* test: change fireEvent to userEvent

* test: rebase fixes

* test: change fireEvent to userEvent

* test: fixes and adress review

* test: improve description test

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
2025-09-02 11:46:50 -04:00
Victor Navarro
8e7bba5365 test: deprecate react-unit-test-utils part-6 (#440) 2025-09-02 11:42:37 -04:00
Victor Navarro
e4c0b1843d test: deprecate react-unit-test-utils part-7 (#442)
* test: deprecate react-unit-test-utils part-7

* test: change fireEvent for userEvent

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
2025-08-29 20:27:15 -04:00
Victor Navarro
480262a7a2 test: deprecate react-unit-test-utils part-10 (#447)
* test: deprecate react-unit-test-utils part-10

* test: change fireEvent to userEvent

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
2025-08-29 16:22:02 -04:00
Victor Navarro
66d5b01a6e test: deprecate react-unit-test-utils part-9 (#446)
* test: deprecate react-unit-test-utils part-9

* test: check metadata on test

* test: remove debug

---------

Co-authored-by: diana-villalvazo-wgu <diana.villalvazo@wgu.edu>
2025-08-29 15:57:02 -04:00
Jacobo Dominguez
57022ed294 refactor: replacing injectIntl with useIntl() part 2 (#459) 2025-08-22 12:24:39 -04:00
Jacobo Dominguez
3115fc275c refactor: replacing injectIntl with useIntl() part 3 (#460) 2025-08-14 10:48:34 -04:00
Jacobo Dominguez
f49c6a55f2 refactor: replacing injectIntl with useIntl() (#458) 2025-08-13 16:47:19 -04:00
Kyle McCormick
d71edbd2f2 chore: Delete CODEOWNERS (#461)
See: https://github.com/openedx/axim-engineering/issues/1511
2025-07-31 16:08:25 -04:00
Brian Smith
715cc60c1c feat!: add design tokens support (#441)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 16:05:04 -04:00
Victor Navarro
ec3c25f54a test: deprecate react-unit-test-utils part-3 (#430) 2025-06-13 08:31:43 -03:00
Victor Navarro
b54e8ffc85 test: deprecate react-unit-test-utils part-2 (#428) 2025-06-11 09:28:54 -03:00
Adolfo R. Brandes
5a383479ff Merge pull request #426 from vmnavarro94/depr-p1
test: deprecate react-unit-test-utils part-1
2025-06-11 09:23:18 -03:00
ayesha waris
cc4b1c8169 feat: removed ora staff notification settings banner (#421)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.10.31>
2025-06-09 14:01:15 -06:00
ayesha waris
fc7370c593 feat: removed ora staff notification settings banner (#421)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.10.31>
2025-06-02 14:09:11 -04:00
renovate[bot]
01bc2cb545 fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 10:21:26 -04:00
Brian Smith
4825c3d68c feat: import FooterSlot from component package instead of slot package (#414) 2025-04-24 12:33:01 -04:00
renovate[bot]
4e27a35e10 fix(deps): update dependency @edx/frontend-component-header to v6.4.0 (#415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 20:17:57 +00:00
Stanislav Lunyachek
ad0e2af8c8 feat: ORA visual improvements 2025-04-16 16:10:15 +03:30
Brian Smith
d03e7c40d8 feat: upgrade to react 18 (#406) 2025-04-09 15:22:36 -04:00
Régis Behmo
5a7063c123 chore: remove husky 🪓🐶 (#405) 2025-04-09 13:56:30 +00:00
Régis Behmo
9348c4bb4c feat: lighter build by avoiding full lodash import
Incorrect lodash imports are causing MFEs to import the entire lodash
library.
2025-04-03 12:42:31 +03:30
Brian Smith
5c5ff1190b chore(deps): update @openedx dependencies to versions that support React 18 (#399) 2025-03-27 16:17:00 -04:00
Jansen Kantor
7aee8562a8 Merge pull request #390 from openedx/jkantor/conflictError
fix: error code expecting wrong data shape
2025-03-05 14:49:36 -05:00
Jansen Kantor
ebca59a38f Merge branch 'master' into jkantor/conflictError 2025-03-05 14:42:07 -05:00
salman2013
f87d521bc0 chore: update catalog-info file for release data 2025-02-20 19:07:39 +03:30
Stanislav Lunyachek
5280cef554 feat: Add header styles inclusion 2025-02-20 18:19:39 +03:30
Stanislav Lunyachek
08430571ed fix: Fix for the Paragon modal shadow, which prevents clicking on an element on the grading page 2025-02-19 18:45:39 +03:30
Jansen Kantor
3de6821c5d Merge branch 'master' into jkantor/conflictError 2025-01-14 10:45:33 -05:00
Paulo Viadanna
fb06133a27 fix: enable scroll in grading modal 2025-01-14 19:00:42 +03:30
Jansen Kantor
12f1c72b7e Merge branch 'master' into jkantor/conflictError 2025-01-10 14:11:49 -05:00
Jansen Kantor
ca8d08c8a0 fixup! fix: error code expecting wrong data shape 2025-01-10 14:08:32 -05:00
Jansen Kantor
30a4ca17ac fix: error code expecting wrong data shape 2025-01-10 14:07:01 -05:00
Awais Ansari
25d76c0e59 fix: updated notifications preferences url (#389)
* fix: updated notifiations preferences url

* fix: updated test cases
2025-01-10 17:09:14 +05:00
Emad Rad
6527f505f1 chore: npm publish action removed (#366)
We don't push MFEs to the npm registry.
Achieves part of https://github.com/openedx/public-engineering/issues/284
2024-12-18 13:48:18 -05:00
milad-emami
400950cff8 feat: update react-pdf to v7 and fix worker configuration 2024-12-18 13:25:38 +03:30
Adolfo R. Brandes
0219f5cd25 chore: remove extraneous file
Remove a file that was previously added by mistake.
2024-12-06 10:54:46 -03:00
Adolfo R. Brandes
fdcab456e8 fix: broken download tests
Declaring `browserslist` in package.json exposed a bug in the
download.js tests that wasn't causing failures before (but arguably,
should): one can't use arrow functions to mock constructors because
calling `new` on them doesn't work.  See the NOTE under:

https://jestjs.io/docs/es6-class-mocks#-module-factory-function-must-return-a-function
2024-12-06 10:54:46 -03:00
Adolfo R. Brandes
5283e7c7c6 fix: Use browserslist-config
We were installing browserslist-config but not declaring it.  This had
the effect that webpack - and likely others - were not using it.
2024-12-06 10:54:46 -03:00
Dima Alipov
e39533c56a fix: incorrect message for locking 2024-11-30 15:22:18 +03:30
milad-emami
212014fed9 chore: update redux-devtools-extension to @redux-devtools/extension@3.0.0 2024-11-25 12:14:34 +03:30
milad-emami
9600301a62 chore(deps): update dependency redux-mock-store to 1.5.5
Updates redux-mock-store to version 1.5.5 in both package.json and package-lock.json to ensure dependency consistency
2024-11-25 12:08:07 +03:30
Feanil Patel
1d5f64e1db docs: Update catalog-info.yaml
Correct the owner.
2024-11-20 13:14:08 -05:00
milad-emami
f5208c58aa fix: remove fixed height from review modal body
Remove height:100% property from modal body content to prevent content from sticking to bottom of page and allow proper spacing
2024-11-20 17:38:01 +03:30
milad-emami
c15680cb8c fix: prevent radio criterion shrinking and improve alignment
- Add flexShrink: 0 style to prevent radio button compression
- Add align-items-center class for better vertical alignment
2024-11-20 17:36:46 +03:30
Diana Catalina Olarte
76f41439e9 fix: apply getPath to PUBLIC_PATH to allow use with CDN 2024-11-19 16:19:19 +03:30
Feanil Patel
4581cf8698 Merge pull request #371 from CodeWithEmad/chore/catalog-info
chore: owner changed
2024-11-12 09:01:55 -05:00
Emad Rad
95fa32eaaa chore: owner changed 2024-11-09 11:25:09 +03:30
Asad Ali
1f729becbe feat: remove CTA (#347) 2024-11-05 12:00:31 -05:00
Arslan Ashraf
7ad1df8bd0 test: Remove support for Node 18 (#365)
Co-authored-by: Muhammad Anas <muhammad.anas@arbisoft.com>
2024-11-04 13:46:17 -05:00
Asad Ali
c9d0abe968 fix: convert notification banner to text if accounts url is not set (#362)
* fix: convert banner to text if ACCOUNT_SETTINGS_URL is not set

* refactor: refactoring

* refactor: rename notificationsBannerLinkMessage to notificationsBannerPreferencesCenterMessage

* refactor: remove lodash usage

* refactor: remove lodash usage
2024-11-04 12:22:50 -05:00
Brian Smith
91e33748ab feat(deps): update header to 5.6.0 (#363) 2024-10-22 19:19:55 -04:00
Feanil Patel
8809f4cf16 Merge pull request #353 from openedx/feanil/ubuntu_upgrade
build: Switch to ubuntu-latest for builds
2024-09-20 10:28:36 -04:00
Feanil Patel
7482765aa4 build: Switch to ubuntu-latest for builds
This code does not have any dependencies that are specific to any specific
version of ubuntu.  So instead of testing on a specific version and then needing
to do work to keep the versions up-to-date, we switch to the ubuntu-latest
target which should be sufficient for testing purposes.

This work is being done as a part of https://github.com/openedx/platform-roadmap/issues/377

closes https://github.com/openedx/frontend-app-ora-grading/issues/345
2024-09-20 10:28:31 -04:00
Kristin Aoki
7003b6102d Merge pull request #354 from openedx/jkantor/headingMessage
fix: incorrect key breaks page
2024-09-13 09:19:34 -04:00
Jansen Kantor
d608daccb1 Merge branch 'master' into jkantor/headingMessage 2024-09-12 16:03:50 -04:00
Jansen Kantor
2208737a69 fix: incorrect key crashes page 2024-09-12 16:01:02 -04:00
Muhammad Anas
567a020061 build: Upgrade to Node 20 (#349)
* build: Upgrade to Node 20

* refactor: updated package-lock

* refactor: updated the lockfile version workflow
2024-09-06 12:23:49 -04:00
Muhammad Anas
c5d9bfb2f6 test: Add Node 20 to CI matrix (#348)
* test: Add Node 20 to CI matrix

* fix: pinning node 18 verson
2024-09-03 14:30:54 -04:00
Feanil Patel
f433d33f9d Merge pull request #328 from openedx/bilalqamar95/jest-v29-upgrade
feat: updated frontend-build & frontend-platform major versions
2024-08-07 10:01:12 -04:00
Bilal Qamar
c0816e0818 Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2024-07-30 14:12:20 +05:00
Feanil Patel
c83e86bf06 Merge pull request #343 from openedx/feanil/update_maintainer
docs: Make it clear that this is unmaintained.
2024-07-24 10:52:01 -04:00
Feanil Patel
b6f1a9739e docs: Make it clear that this is unmaintained. 2024-07-19 16:06:30 -04:00
Bilal Qamar
f2b6cd4cac Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2024-07-04 16:02:16 +05:00
Adolfo R. Brandes
3092bd3980 build: Update codecov and use token (#342)
Update codecov to the latest version and start using the org-wide token for uploads.

See https://github.com/openedx/wg-frontend/issues/179
2024-06-17 12:14:02 -03:00
Bilal Qamar
20dc736278 refactor: installed jest-environment-jsdom package, updated package-lock 2024-06-03 16:08:02 +05:00
Bilal Qamar
d72beeb2d4 Merge branch 'master' of github.com:openedx/frontend-app-ora-grading into bilalqamar95/jest-v29-upgrade 2024-06-03 16:06:23 +05:00
Brian Smith
64b57df976 feat: use frontend-plugin-framework to provide a FooterSlot 2024-05-23 11:28:25 -03:00
Bilal Qamar
346b371ba2 Merge branch 'master' of github.com:openedx/frontend-app-ora-grading into bilalqamar95/jest-v29-upgrade 2024-05-16 16:04:53 +05:00
Bilal Qamar
650d708a1c refactor: major version upgrade for react-unit-test-utils 2024-05-16 16:04:13 +05:00
Awais Ansari
1d924b4812 Merge pull request #329 from openedx/aansari/INF-1375
feat: added info banner for ORA notifications
2024-04-30 21:09:29 +05:00
Awais Ansari
31ed9410a4 test: added test cases for NotificationsBanner 2024-04-30 20:07:23 +05:00
Awais Ansari
0ba3cac532 feat: added info banner for ORA notifications 2024-04-30 19:59:26 +05:00
Bilal Qamar
b23effdb7f feat: updated frontend-build & frontend-platform major versions 2024-04-24 13:54:40 +05:00
Bilal Qamar
5bcb6fe6f3 refactor: updated snapshots for failing tests 2024-04-15 15:44:02 +05:00
Bilal Qamar
a67f201f4d feat: updated frontend-build major version, bumps jest to v29 2024-04-15 15:43:48 +05:00
alipov_d
9cfab58663 fix: request error for empty user list responses 2024-04-03 15:25:01 -03:00
Samir Sabri
59a7d0751b feat!: remove Transifex calls for OEP-58 2024-03-18 15:01:30 -04:00
Jeremy Ristau
9d673e803e Merge pull request #320 from openedx/ownership-update
chore: update catalog-info owner
2024-03-15 12:54:11 -04:00
Jeremy Ristau
8438915f72 fix: use correct yaml 2024-03-15 09:25:01 -04:00
Jeremy Ristau
cc6dd20f12 chore: update catalog-info owner
Continuing maintainership updates for 2U 2024.
2024-03-11 21:34:25 -04:00
Mashal Malik
f67ffdd480 refactor: replace @edx/paragon and @edx/frontend-build (#294)
* refactor: replace @edx/paragon and @edx/frontend-build

* refactor: updated edx packages

* fix: fixed failing test cases by remmoving paragon mock

* fix: updated lock file to fix build issues

---------

Co-authored-by: mashal-m <mashal.malik@arbisoft.com>
Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com>
Co-authored-by: Muhammad Abdullah Waheed <abdullah.waheed@arbisoft.com>
2024-02-28 13:04:23 -03:00
renovate[bot]
836df49829 fix(deps): update dependency axios to ^0.28.0 [security] 2024-02-21 22:18:09 -05:00
Syed Ali Abbas Zaidi
5bed90b659 feat: migrate enzyme to edx/react-unit-test-utils (#295)
* feat: migrate enzyme to edx/react-unit-test-utils

* refactor: remove shallowWrapper usage

* refactor: remove unnecessary _instance usage
2024-02-10 00:35:02 +05:00
Feanil Patel
3b39c79fbf Merge pull request #262 from openedx/abdullahwaheed/react-intl-to-formatjs
feat: babel-plugin-react-intl to babel-plugin-formatjs migration
2024-02-08 14:58:54 -05:00
Abdullah Waheed
df7a189bcd fix: upgraded frontend-build to fix security issue 2024-02-08 14:43:21 -05:00
Abdullah Waheed
08c51b6492 fix: reverted install package functionality 2024-02-08 14:43:21 -05:00
Abdullah Waheed
3bb3d90f3a feat: babel-plugin-react-intl to babel-plugin-formatjs migration 2024-02-08 14:43:19 -05:00
Feanil Patel
2f48cc5767 Merge pull request #247 from openedx/mashal-m/update_lockfile
refactor: updated lock file version check to use new workflow
2024-02-08 14:19:55 -05:00
mashal-m
ea43ebb031 refactor: update lock file version 2024-02-08 14:10:50 -05:00
Omar Al-Ithawi
4a708da50c feat: tutor-mfe compatiblilty for atlas pull (#312)
- install atlas
 - remove `--filter` to pull all languages by default
 - use ATLAS_OPTIONS to allow custom `--filter`
 - include frontend-platform in `atlas pull`

Refs: FC-0012 OEP-58
2024-02-05 16:58:28 -05:00
Jenkins
f135f9a111 chore(i18n): update translations 2024-02-04 10:46:33 -05:00
Jhon Vente
44d6dc616c fix: test problems jest dom and paragon (#311) 2024-02-02 12:13:12 -05:00
renovate[bot]
a9771fbf49 chore(deps): update dependency @testing-library/jest-dom to v6 2024-01-26 07:48:36 -05:00
renovate[bot]
8f58b72919 fix(deps): update react-router monorepo to v6.21.3 2024-01-26 05:59:50 -05:00
renovate[bot]
8da2a60b37 fix(deps): update dependency moment to v2.30.1 2024-01-26 03:17:56 -05:00
renovate[bot]
ff3e56c3f7 fix(deps): update dependency classnames to v2.5.1 2024-01-25 22:31:21 -05:00
renovate[bot]
204586e79b fix(deps): update dependency @edx/frontend-component-header to v4.11.1 2024-01-25 20:25:00 -05:00
renovate[bot]
31be6daac3 fix(deps): update dependency @edx/frontend-component-footer to v12.7.1 2024-01-25 17:43:59 -05:00
renovate[bot]
9f1c950080 chore(deps): update dependency node to 18.19 2024-01-25 14:50:00 -05:00
renovate[bot]
66b793a1d4 fix(deps): update dependency whatwg-fetch to v3.6.20 2024-01-25 10:24:19 -05:00
renovate[bot]
72db51b65c fix(deps): update dependency regenerator-runtime to v0.14.1 2024-01-25 06:08:35 -05:00
renovate[bot]
9121b3f1e7 fix(deps): update dependency @zip.js/zip.js to v2.7.32 2024-01-25 04:41:05 -05:00
renovate[bot]
f7e51fd1d0 fix(deps): update dependency @testing-library/user-event to v14.5.2 2024-01-25 01:58:02 -05:00
renovate[bot]
77b4f9b47e fix(deps): update dependency core-js to v3.35.1 2024-01-25 01:25:50 -05:00
Jenkins
3ef24a626b chore(i18n): update translations 2023-12-17 10:46:17 -05:00
renovate[bot]
d810913038 fix(deps): update dependency @edx/brand to v1.2.3 2023-10-29 15:44:21 -04:00
renovate[bot]
a6436997bb fix(deps): update dependency core-js to v3.33.1 2023-10-23 15:16:43 -04:00
renovate[bot]
7959a39267 fix(deps): update react-router monorepo to v6.17.0 2023-10-23 13:51:41 -04:00
Feanil Patel
3a1dbfdee5 chore: Update to the new version of brand-openedx in the new scope. (#277)
Part of https://github.com/openedx/axim-engineering/issues/23

This updates the `@edx/brand` alias to point to the `brand-openedx` package at
the `openedx` scope. This does not impact imports because this package is used
via an alias.
2023-10-20 17:26:09 -04:00
renovate[bot]
9b326f1ee8 fix(deps): update dependency core-js to v3.33.0 2023-10-19 03:14:33 -04:00
renovate[bot]
3a87ebda1a fix(deps): update dependency @testing-library/user-event to v14.5.1 2023-10-19 00:43:26 -04:00
renovate[bot]
25389ff296 fix(deps): update dependency @edx/frontend-component-header to v4.8.0 2023-10-18 20:59:19 -04:00
renovate[bot]
80f782b87f fix(deps): update dependency @edx/frontend-component-footer to v12.5.0 2023-10-18 18:57:41 -04:00
renovate[bot]
8a2d767263 chore(deps): update dependency node to 18.18 2023-10-18 15:38:22 -04:00
renovate[bot]
264bed987e chore(deps): update dependency jest to v29.7.0 2023-10-18 11:37:15 -04:00
renovate[bot]
738d460505 chore(deps): update dependency axios-mock-adapter to v1.22.0 2023-10-18 11:34:18 -04:00
Syed Ali Abbas Zaidi
b8f43b92a1 feat: upgrade react router to v6 (#174)
Co-authored-by: Matthew Carter <mcarter@edx.org>
2023-10-18 11:33:42 -04:00
renovate[bot]
7afffa4509 fix(deps): update dependency whatwg-fetch to v3.6.19 2023-10-18 08:44:56 -04:00
renovate[bot]
b9ad13e354 fix(deps): update dependency @zip.js/zip.js to v2.7.30 2023-10-18 05:50:38 -04:00
renovate[bot]
8ceb9e308f fix(deps): update dependency @reduxjs/toolkit to v1.9.7 2023-10-18 03:25:31 -04:00
renovate[bot]
b58cab1249 fix(deps): update dependency @edx/paragon to v20.46.3 2023-10-17 20:47:07 -04:00
renovate[bot]
f6d8c324d9 fix(deps): update dependency @edx/frontend-platform to v4.6.3 2023-10-17 17:42:46 -04:00
renovate[bot]
0c8d2017db chore(deps): update dependency @edx/frontend-build to v12.9.17 2023-10-17 15:28:45 -04:00
222 changed files with 16302 additions and 22854 deletions

3
.env
View File

@@ -32,3 +32,6 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='' ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
APP_ID='' APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''
ACCOUNT_SETTINGS_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -7,7 +7,6 @@ LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token' CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -38,3 +37,6 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer' ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
APP_ID='' APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''
ACCOUNT_SETTINGS_URL=http://localhost:1997
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -7,7 +7,6 @@ LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token' CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -36,3 +35,5 @@ ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_SOURCE='example.com' ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral' ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer' ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
ACCOUNT_SETTINGS_URL=http://localhost:1997
PARAGON_THEME_URLS={}

View File

@@ -1,4 +1,5 @@
const { createConfig } = require('@edx/frontend-build'); // eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('eslint', { const config = createConfig('eslint', {
rules: { rules: {
@@ -6,20 +7,28 @@ const config = createConfig('eslint', {
'import/no-named-as-default-member': 'off', 'import/no-named-as-default-member': 'off',
'import/no-import-module-exports': 'off', 'import/no-import-module-exports': 'off',
'import/no-self-import': 'off', 'import/no-self-import': 'off',
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }], 'spaced-comment': ['error', 'always', { block: { exceptions: ['*'] } }],
'react-hooks/rules-of-hooks': 'off', 'react-hooks/rules-of-hooks': 'off',
"react/forbid-prop-types": ["error", { "forbid": ["any", "array"] }], // arguable object proptype is use when I do not care about the shape of the object 'react/forbid-prop-types': ['error', { forbid: ['any', 'array'] }], // arguable object proptype is use when I do not care about the shape of the object
'no-import-assign': 'off', 'no-import-assign': 'off',
'no-promise-executor-return': 'off', 'no-promise-executor-return': 'off',
'import/no-cycle': 'off', 'import/no-cycle': 'off',
}, },
overrides: [
{
files: ['**/*.test.{js,jsx,ts,tsx}'],
rules: {
'react/prop-types': 'off',
},
},
],
}); });
config.settings = { config.settings = {
"import/resolver": { 'import/resolver': {
node: { node: {
paths: ["src", "node_modules"], paths: ['src', 'node_modules'],
extensions: [".js", ".jsx"], extensions: ['.js', '.jsx'],
}, },
}, },
}; };

6
.github/CODEOWNERS vendored
View File

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

View File

@@ -10,17 +10,18 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Nodejs Env - name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v3 uses: actions/setup-node@v4
# Because of node 18 bug (https://github.com/nodejs/node/issues/47563), Pinning node version 18.15 until the next release of node # Because of node 18 bug (https://github.com/nodejs/node/issues/47563), Pinning node version 18.15 until the next release of node
with: with:
node-version: 18.15 node-version-file: '.nvmrc'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -38,7 +39,10 @@ jobs:
run: npm run build run: npm run build
- name: Run Coverage - name: Run Coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Send failure notification - name: Send failure notification
if: ${{ failure() }} if: ${{ failure() }}

View File

@@ -1,32 +0,0 @@
name: Release CI
on:
push:
tags:
- "*"
jobs:
release:
name: Release
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 12
- name: Install dependencies
run: npm ci
- name: Create Build
run: npm run build
- name: Release Package
env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
run: npm semantic-release

2
.gitignore vendored
View File

@@ -25,3 +25,5 @@ module.config.js
### transifex ### ### transifex ###
src/i18n/transifex_input.json src/i18n/transifex_input.json
temp temp
src/i18n/messages

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint

2
.nvmrc
View File

@@ -1 +1 @@
18.17 24

View File

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

View File

@@ -1,9 +0,0 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-ora-grading]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -2,16 +2,13 @@ npm-install-%: ## install specified % npm package
npm install $* --save-dev npm install $* --save-dev
git add package.json git add package.json
transifex_resource = frontend-app-ora-grading
transifex_langs = "ar,de_DE,es_419,fa_IR,fr,fr_CA,hi,it_IT,pt_PT,uk,ru,zh_CN"
intl_imports = ./node_modules/.bin/intl-imports.js intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json transifex_input = $(i18n)/transifex_input.json
# This directory must match .babelrc . # This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl transifex_temp = ./temp/babel-plugin-formatjs
NPM_TESTS=build i18n_extract lint test NPM_TESTS=build i18n_extract lint test
@@ -43,35 +40,18 @@ detect_changed_source_translations:
# Checking for changed translations... # Checking for changed translations...
git diff --exit-code $(i18n) git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations: pull_translations:
rm -rf src/i18n/messages rm -rf src/i18n/messages
mkdir src/i18n/messages mkdir src/i18n/messages
cd src/i18n/messages \ cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \ && atlas pull $(ATLAS_OPTIONS) \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \ translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \ translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/paragon/src/i18n/messages:paragon \ translations/paragon/src/i18n/messages:paragon \
translations/frontend-app-ora-grading/src/i18n/messages:frontend-app-ora-grading translations/frontend-app-ora-grading/src/i18n/messages:frontend-app-ora-grading
$(intl_imports) frontend-component-footer frontend-component-header paragon frontend-app-ora-grading $(intl_imports) frontend-component-footer frontend-component-header frontend-platform paragon frontend-app-ora-grading
endif
# This target is used by CI. # This target is used by CI.
validate-no-uncommitted-package-lock-changes: validate-no-uncommitted-package-lock-changes:

View File

@@ -26,18 +26,20 @@ Getting Started
Prerequisites Prerequisites
============= =============
The `devstack`_ is currently recommended as a development environment for your `Tutor`_ is currently recommended as a development environment for your
new MFE. If you start it with ``make dev.up.lms`` that should give you new MFE. Please refer
everything you need as a companion to this frontend.
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
to the `relevant tutor-mfe documentation`_ to get started using it. to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Devstack: https://github.com/openedx/devstack
.. _Tutor: https://github.com/overhangio/tutor .. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development .. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Plugins
=======
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
Developing Developing
========== ==========
@@ -54,9 +56,9 @@ First, clone the repo, install code prerequisites, and start the app.
``git clone git@github.com:openedx/frontend-app-ora-grading.git`` ``git clone git@github.com:openedx/frontend-app-ora-grading.git``
2. Use node v18.x. 2. Use the version of Node specified in the ``.nvmrc`` file.
The current version of the micro-frontend build scripts support node 18. The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
Using other major versions of node *may* work, but this is unsupported. For Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes an .nvmrc file to help in setting the convenience, this repository includes an .nvmrc file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_. correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.

View File

@@ -13,7 +13,9 @@ metadata:
- url: "https://ora-grading.stage.edx.org" - url: "https://ora-grading.stage.edx.org"
title: "Stage Site" title: "Stage Site"
icon: "Web" icon: "Web"
annotations:
openedx.org/release: "master"
spec: spec:
owner: group:content-aurora owner: "user:codewithemad"
type: 'website' type: 'website'
lifecycle: 'production' lifecycle: 'production'

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@edx/frontend-build'); const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('jest', { module.exports = createConfig('jest', {
setupFilesAfterEnv: [ setupFilesAfterEnv: [
@@ -6,9 +6,6 @@ module.exports = createConfig('jest', {
'<rootDir>/src/setupTest.js', '<rootDir>/src/setupTest.js',
], ],
modulePaths: ['<rootDir>/src/'], modulePaths: ['<rootDir>/src/'],
snapshotSerializers: [
'enzyme-to-json/serializer',
],
coveragePathIgnorePatterns: [ coveragePathIgnorePatterns: [
'src/segment.js', 'src/segment.js',
'src/postcss.config.js', 'src/postcss.config.js',

View File

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

28016
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,16 +6,19 @@
"type": "git", "type": "git",
"url": "git+https://github.com/edx/frontend-app-ora-grading.git" "url": "git+https://github.com/edx/frontend-app-ora-grading.git"
}, },
"browserslist": [
"extends @edx/browserslist-config"
],
"scripts": { "scripts": {
"build": "fedx-scripts webpack", "build": "fedx-scripts webpack",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null", "i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .jsx,.js src/", "lint": "fedx-scripts eslint --ext .jsx,.js src/",
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/", "lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"semantic-release": "semantic-release", "semantic-release": "semantic-release",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/ora-grading/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests", "test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
"watch-tests": "jest --watch", "watch-tests": "jest --watch"
"prepare": "husky install"
}, },
"author": "edX", "author": "edX",
"license": "AGPL-3.0", "license": "AGPL-3.0",
@@ -24,26 +27,26 @@
"access": "public" "access": "public"
}, },
"dependencies": { "dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@^1.2.0", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "12.2.1", "@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "4.6.0", "@edx/frontend-component-header": "^8.0.0",
"@edx/frontend-platform": "4.6.1", "@edx/frontend-platform": "^8.3.1",
"@edx/paragon": "^20.44.0", "@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4", "@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/paragon": "^23.4.5",
"@redux-beacon/segment": "^1.1.0", "@redux-beacon/segment": "^1.1.0",
"@redux-devtools/extension": "3.0.0",
"@reduxjs/toolkit": "^1.6.1", "@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^14.0.0", "@testing-library/user-event": "^14.0.0",
"@zip.js/zip.js": "^2.4.6", "@zip.js/zip.js": "^2.4.6",
"axios": "^0.27.0", "axios": "^0.28.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"core-js": "3.32.1", "core-js": "3.35.1",
"dompurify": "^2.3.1", "dompurify": "^2.3.1",
"email-prop-type": "^3.0.1", "email-prop-type": "^3.0.1",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"filesize": "^8.0.6", "filesize": "^8.0.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
@@ -53,18 +56,17 @@
"moment": "^2.29.3", "moment": "^2.29.3",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "7.1.3", "query-string": "7.1.3",
"react": "^17.0.2", "react": "^18.3.1",
"react-dom": "^17.0.2", "react-dom": "^18.3.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-intl": "^5.20.9", "react-intl": "6.4.7",
"react-pdf": "^5.5.0", "react-pdf": "^7.0.0",
"react-redux": "^7.2.9", "react-redux": "^7.2.9",
"react-router": "5.3.4", "react-router": "6.21.3",
"react-router-dom": "5.3.4", "react-router-dom": "6.21.3",
"react-router-redux": "^5.0.0-alpha.9", "react-router-redux": "^5.0.0-alpha.9",
"redux": "4.2.1", "redux": "4.2.1",
"redux-beacon": "^2.1.0", "redux-beacon": "^2.1.0",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6", "redux-logger": "3.0.6",
"redux-thunk": "2.4.2", "redux-thunk": "2.4.2",
"regenerator-runtime": "^0.14.0", "regenerator-runtime": "^0.14.0",
@@ -73,22 +75,18 @@
"whatwg-fetch": "^3.6.2" "whatwg-fetch": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "^1.2.0", "@edx/browserslist-config": "^1.3.0",
"@edx/frontend-build": "^12.7.0", "@openedx/frontend-build": "^14.6.2",
"@edx/reactifex": "^2.1.1", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^16.2.0",
"@testing-library/react": "12.1.5",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"axios-mock-adapter": "^1.20.0", "axios-mock-adapter": "^1.20.0",
"fetch-mock": "^9.11.0", "fetch-mock": "^9.11.0",
"husky": "^7.0.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "29.6.4", "jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-expect-message": "^1.0.2", "jest-expect-message": "^1.0.2",
"react-dev-utils": "^12.0.1", "react-dev-utils": "^12.0.1",
"react-test-renderer": "^17.0.2", "react-test-renderer": "^18.3.1",
"reactifex": "1.1.1", "redux-mock-store": "^1.5.5"
"redux-mock-store": "^1.5.4",
"semantic-release": "^19.0.3"
} }
} }

View File

@@ -3,13 +3,12 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import Footer from '@edx/frontend-component-footer'; import { FooterSlot } from '@edx/frontend-component-footer';
import { LearningHeader as Header } from '@edx/frontend-component-header'; import { LearningHeader as Header } from '@edx/frontend-component-header';
import { selectors } from 'data/redux'; import { selectors } from 'data/redux';
import DemoWarning from 'containers/DemoWarning'; import DemoWarning from 'containers/DemoWarning';
import CTA from 'containers/CTA';
import ListView from 'containers/ListView'; import ListView from 'containers/ListView';
import './App.scss'; import './App.scss';
@@ -23,13 +22,13 @@ export const App = ({ courseMetadata, isEnabled }) => (
courseTitle={courseMetadata.title} courseTitle={courseMetadata.title}
courseNumber={courseMetadata.number} courseNumber={courseMetadata.number}
courseOrg={courseMetadata.org} courseOrg={courseMetadata.org}
data-testid="header"
/> />
{!isEnabled && <DemoWarning />} {!isEnabled && <DemoWarning />}
<CTA /> <main data-testid="main">
<main>
<ListView /> <ListView />
</main> </main>
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} /> <FooterSlot />
</div> </div>
</Router> </Router>
); );

View File

@@ -1,15 +1,13 @@
// frontend-app-*/src/index.scss // frontend-app-*/src/index.scss
@import "~@edx/brand/paragon/fonts"; @use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
@import "~@edx/brand/paragon/variables";
@import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
$fa-font-path: "~font-awesome/fonts"; $fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome"; @import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work $input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
@import "~@edx/frontend-component-footer/dist/_footer"; @import "~@edx/frontend-component-footer/dist/_footer";
@import "~@edx/frontend-component-header/dist/index";
#root { #root {
display: flex; display: flex;
@@ -48,7 +46,22 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
right: 1rem !important; right: 1rem !important;
} }
} }
.confirm-modal .pgn__modal-body { .confirm-modal .pgn__modal-body {
overflow: hidden; overflow: hidden;
} }
.pgn__modal-body-content {
& img {
object-fit: contain;
max-width: 100%;
height: auto;
}
& blockquote > p {
border-left: 2px solid var(--pgn-color-gray-200);
margin-left: 1.5rem;
padding-left: 1rem;
}
}
} }

View File

@@ -1,77 +1,101 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import { selectors } from 'data/redux';
import Footer from '@edx/frontend-component-footer'; import { renderWithIntl } from './testUtils';
import { LearningHeader as Header } from '@edx/frontend-component-header'; import { App, mapStateToProps } from './App';
import ListView from 'containers/ListView'; // we want to scope these tests to the App component, so we mock some child components to reduce complexity
import { App } from './App'; jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
getLoginRedirectUrl: jest.fn(),
}));
jest.mock('@edx/frontend-component-footer', () => ({
FooterSlot: () => <div data-testid="footer">Footer</div>,
}));
jest.mock('containers/ListView', () => function ListView() {
return <div data-testid="list-view">List View</div>;
});
jest.mock('containers/DemoWarning', () => function DemoWarning() {
return <div role="alert" data-testid="demo-warning">Demo Warning</div>;
});
jest.mock('@edx/frontend-component-header', () => ({
LearningHeader: ({ courseTitle, courseNumber, courseOrg }) => (
<div data-testid="header">
Header - {courseTitle} {courseNumber} {courseOrg}
</div>
),
}));
jest.mock('data/redux', () => ({ jest.mock('data/redux', () => ({
app: { selectors: {
selectors: { app: {
courseMetadata: (state) => ({ courseMetadata: state }), courseMetadata: jest.fn((state) => state.courseMetadata || {
isEnabled: (state) => ({ isEnabled: state }), org: 'test-org',
number: 'test-101',
title: 'Test Course',
}),
isEnabled: jest.fn((state) => (state.isEnabled !== undefined ? state.isEnabled : true)),
}, },
}, },
})); }));
jest.mock('@edx/frontend-component-header', () => ({ describe('App component', () => {
LearningHeader: 'Header', const defaultProps = {
}));
jest.mock('@edx/frontend-component-footer', () => 'Footer');
jest.mock('containers/DemoWarning', () => 'DemoWarning');
jest.mock('containers/CTA', () => 'CTA');
jest.mock('containers/ListView', () => 'ListView');
jest.mock('components/Head', () => 'Head');
const logo = 'fakeLogo.png';
let el;
let router;
describe('App router component', () => {
const props = {
courseMetadata: { courseMetadata: {
org: 'course-org', org: 'test-org',
number: 'course-number', number: 'test-101',
title: 'course-title', title: 'Test Course',
}, },
isEnabled: true, isEnabled: true,
}; };
test('snapshot: enabled', () => {
expect(shallow(<App {...props} />)).toMatchSnapshot();
});
test('snapshot: disabled (show demo warning)', () => {
expect(shallow(<App {...props} isEnabled={false} />)).toMatchSnapshot();
});
describe('component', () => {
beforeEach(() => {
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
el = shallow(<App {...props} />);
router = el.childAt(0);
});
describe('Router', () => {
test('Routing - ListView is only route', () => {
expect(router.find('main')).toEqual(shallow(
<main><ListView /></main>,
));
});
});
test('Footer logo drawn from env variable', () => {
expect(router.find(Footer).props().logo).toEqual(logo);
});
test('Header to use courseMetadata props', () => { beforeEach(() => {
const { jest.clearAllMocks();
courseTitle, });
courseNumber,
courseOrg, it('renders header with course metadata', () => {
} = router.find(Header).props(); renderWithIntl(<App {...defaultProps} />);
expect(courseTitle).toEqual(props.courseMetadata.title); const org = screen.getByText((text) => text.includes('test-org'));
expect(courseNumber).toEqual(props.courseMetadata.number); expect(org).toBeInTheDocument();
expect(courseOrg).toEqual(props.courseMetadata.org); const title = screen.getByText((content) => content.includes('Test Course'));
expect(title).toBeInTheDocument();
});
it('renders main content', () => {
renderWithIntl(<App {...defaultProps} />);
const main = screen.getByTestId('main');
expect(main).toBeInTheDocument();
});
it('does not render demo warning when enabled', () => {
renderWithIntl(<App {...defaultProps} />);
const demoWarning = screen.queryByRole('alert');
expect(demoWarning).not.toBeInTheDocument();
});
it('renders demo warning when disabled', () => {
renderWithIntl(<App {...defaultProps} isEnabled={false} />);
const demoWarning = screen.getByRole('alert');
expect(demoWarning).toBeInTheDocument();
});
describe('mapStateToProps', () => {
it('maps state properties correctly', () => {
const testState = { arbitraryState: 'some data' };
const mapped = mapStateToProps(testState);
expect(selectors.app.courseMetadata).toHaveBeenCalledWith(testState);
expect(selectors.app.isEnabled).toHaveBeenCalledWith(testState);
expect(mapped.courseMetadata).toEqual(selectors.app.courseMetadata(testState));
expect(mapped.isEnabled).toEqual(selectors.app.isEnabled(testState));
}); });
}); });
}); });

View File

@@ -1,42 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App router component snapshot: disabled (show demo warning) 1`] = `
<BrowserRouter>
<div>
<Head />
<Header
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
/>
<DemoWarning />
<CTA />
<main>
<ListView />
</main>
<Footer
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
/>
</div>
</BrowserRouter>
`;
exports[`App router component snapshot: enabled 1`] = `
<BrowserRouter>
<div>
<Head />
<Header
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
/>
<CTA />
<main>
<ListView />
</main>
<Footer
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
/>
</div>
</BrowserRouter>
`;

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
<ErrorPage
message="test-error-message"
/>
`;
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<AppProvider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
>
<App />
</AppProvider>
`;

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { AlertModal, ActionRow, Button } from '@edx/paragon'; import { AlertModal, ActionRow, Button } from '@openedx/paragon';
import { nullMethod } from 'hooks'; import { nullMethod } from 'hooks';
export const ConfirmModal = ({ export const ConfirmModal = ({

View File

@@ -1,5 +1,6 @@
import { shallow } from 'enzyme'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { ConfirmModal } from './ConfirmModal'; import { ConfirmModal } from './ConfirmModal';
describe('ConfirmModal', () => { describe('ConfirmModal', () => {
@@ -12,10 +13,48 @@ describe('ConfirmModal', () => {
onCancel: jest.fn().mockName('this.props.onCancel'), onCancel: jest.fn().mockName('this.props.onCancel'),
onConfirm: jest.fn().mockName('this.props.onConfirm'), onConfirm: jest.fn().mockName('this.props.onConfirm'),
}; };
test('snapshot: closed', () => {
expect(shallow(<ConfirmModal {...props} />)).toMatchSnapshot(); beforeEach(() => {
jest.clearAllMocks();
}); });
test('snapshot: open', () => {
expect(shallow(<ConfirmModal {...props} isOpen />)).toMatchSnapshot(); it('should not render content when modal is closed', () => {
render(
<IntlProvider locale="en">
<ConfirmModal {...props} />
</IntlProvider>,
);
expect(screen.queryByText(props.content)).toBeNull();
});
it('should display content when modal is open', () => {
render(
<IntlProvider locale="en">
<ConfirmModal {...props} isOpen />
</IntlProvider>,
);
expect(screen.getByText(props.content)).toBeInTheDocument();
});
it('should call onCancel when cancel button is clicked', async () => {
render(
<IntlProvider locale="en">
<ConfirmModal {...props} isOpen />
</IntlProvider>,
);
const user = userEvent.setup();
await user.click(screen.getByText(props.cancelText));
expect(props.onCancel).toHaveBeenCalledTimes(1);
});
it('should call onConfirm when confirm button is clicked', async () => {
render(
<IntlProvider locale="en">
<ConfirmModal {...props} isOpen />
</IntlProvider>,
);
const user = userEvent.setup();
await user.click(screen.getByText(props.confirmText));
expect(props.onConfirm).toHaveBeenCalledTimes(1);
}); });
}); });

View File

@@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DemoAlert component snapshot 1`] = `
<AlertModal
footerNode={
<ActionRow>
<Button
onClick={[MockFunction props.onClose]}
variant="primary"
>
Confirm
</Button>
</ActionRow>
}
isOpen={true}
onClose={[MockFunction props.onClose]}
title="Demo submit prevented"
>
<p>
Grade submission is disabled in the Demo mode of the new ORA Staff Grader.
</p>
</AlertModal>
`;

View File

@@ -1,39 +1,40 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { import {
ActionRow, ActionRow,
AlertModal, AlertModal,
Button, Button,
} from '@edx/paragon'; } from '@openedx/paragon';
import messages from './messages'; import messages from './messages';
export const DemoAlert = ({ export const DemoAlert = ({
intl: { formatMessage },
isOpen, isOpen,
onClose, onClose,
}) => ( }) => {
<AlertModal const { formatMessage } = useIntl();
title={formatMessage(messages.title)} return (
isOpen={isOpen} <AlertModal
onClose={onClose} title={formatMessage(messages.title)}
footerNode={( isOpen={isOpen}
<ActionRow> onClose={onClose}
<Button variant="primary" onClick={onClose}> footerNode={(
{formatMessage(messages.confirm)} <ActionRow>
</Button> <Button variant="primary" onClick={onClose}>
</ActionRow> {formatMessage(messages.confirm)}
</Button>
</ActionRow>
)} )}
> >
<p>{formatMessage(messages.warningMessage)}</p> <p>{formatMessage(messages.warningMessage)}</p>
</AlertModal> </AlertModal>
); );
};
DemoAlert.propTypes = { DemoAlert.propTypes = {
intl: intlShape.isRequired,
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
export default injectIntl(DemoAlert); export default DemoAlert;

View File

@@ -1,16 +1,32 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import userEvent from '@testing-library/user-event';
import { renderWithIntl } from '../../testUtils';
import { formatMessage } from 'testUtils'; import messages from './messages';
import { DemoAlert } from '.'; import { DemoAlert } from '.';
describe('DemoAlert component', () => { describe('DemoAlert component', () => {
test('snapshot', () => { const props = {
const props = { isOpen: true,
intl: { formatMessage }, onClose: jest.fn().mockName('props.onClose'),
isOpen: true, };
onClose: jest.fn().mockName('props.onClose'),
}; it('does not render when isOpen is false', () => {
expect(shallow(<DemoAlert {...props} />)).toMatchSnapshot(); renderWithIntl(<DemoAlert {...props} isOpen={false} />);
expect(screen.queryByText(messages.title.defaultMessage)).toBeNull();
});
it('renders with correct title and message when isOpen is true', () => {
renderWithIntl(<DemoAlert {...props} />);
expect(screen.getByText(messages.title.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.warningMessage.defaultMessage)).toBeInTheDocument();
});
it('calls onClose when confirmation button is clicked', async () => {
renderWithIntl(<DemoAlert {...props} />);
const user = userEvent.setup();
const confirmButton = screen.getByText(messages.confirm.defaultMessage);
await user.click(confirmButton);
expect(props.onClose).toHaveBeenCalled();
}); });
}); });

View File

@@ -1,89 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilePopoverContent component snapshot default 1`] = `
<Fragment>
<div
className="help-popover-option"
>
<strong>
<FormattedMessage
defaultMessage="File Name"
description="Popover title for file name"
id="ora-grading.FilePopoverContent.filePopoverNameTitle"
/>
</strong>
<br />
some file name
</div>
<div
className="help-popover-option"
>
<strong>
<FormattedMessage
defaultMessage="File Description"
description="Popover title for file description"
id="ora-grading.FilePopoverCellContent.filePopoverDescriptionTitle"
/>
</strong>
<br />
long descriptive text...
</div>
<div
className="help-popover-option"
>
<strong>
<FormattedMessage
defaultMessage="File Size"
description="Popover title for file size"
id="ora-grading.FilePopoverCellContent.fileSizeTitle"
/>
</strong>
<br />
filesize(6000)
</div>
</Fragment>
`;
exports[`FilePopoverContent component snapshot invalid size 1`] = `
<Fragment>
<div
className="help-popover-option"
>
<strong>
<FormattedMessage
defaultMessage="File Name"
description="Popover title for file name"
id="ora-grading.FilePopoverContent.filePopoverNameTitle"
/>
</strong>
<br />
some file name
</div>
<div
className="help-popover-option"
>
<strong>
<FormattedMessage
defaultMessage="File Description"
description="Popover title for file description"
id="ora-grading.FilePopoverCellContent.filePopoverDescriptionTitle"
/>
</strong>
<br />
long descriptive text...
</div>
<div
className="help-popover-option"
>
<strong>
<FormattedMessage
defaultMessage="File Size"
description="Popover title for file size"
id="ora-grading.FilePopoverCellContent.fileSizeTitle"
/>
</strong>
<br />
Unknown
</div>
</Fragment>
`;

View File

@@ -1,7 +1,7 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import filesize from 'filesize'; import filesize from 'filesize';
import { renderWithIntl } from '../../testUtils';
import FilePopoverContent from '.'; import FilePopoverContent from '.';
jest.mock('filesize', () => (size) => `filesize(${size})`); jest.mock('filesize', () => (size) => `filesize(${size})`);
@@ -14,25 +14,26 @@ describe('FilePopoverContent', () => {
downloadURL: 'this-url-is.working', downloadURL: 'this-url-is.working',
size: 6000, size: 6000,
}; };
let el;
beforeEach(() => {
el = shallow(<FilePopoverContent {...props} />);
});
describe('snapshot', () => {
test('default', () => expect(el).toMatchSnapshot());
test('invalid size', () => {
el.setProps({
size: null,
});
expect(el).toMatchSnapshot();
});
});
describe('behavior', () => { describe('behavior', () => {
test('content', () => { it('renders file name correctly', () => {
expect(el.text()).toContain(props.name); renderWithIntl(<FilePopoverContent {...props} />);
expect(el.text()).toContain(props.description); expect(screen.getByText(props.name)).toBeInTheDocument();
expect(el.text()).toContain(filesize(props.size)); });
it('renders file description correctly', () => {
renderWithIntl(<FilePopoverContent {...props} />);
expect(screen.getByText(props.description)).toBeInTheDocument();
});
it('renders file size correctly', () => {
renderWithIntl(<FilePopoverContent {...props} />);
expect(screen.getByText(filesize(props.size))).toBeInTheDocument();
});
it('renders "Unknown" when size is null', () => {
renderWithIntl(<FilePopoverContent {...props} size={null} />);
expect(screen.getByText('Unknown')).toBeInTheDocument();
}); });
}); });
}); });

View File

@@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Alert, Button } from '@edx/paragon'; import { Alert, Button } from '@openedx/paragon';
import { Info } from '@edx/paragon/icons'; import { Info } from '@openedx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';
const messageShape = PropTypes.shape({ const messageShape = PropTypes.shape({

View File

@@ -1,8 +1,6 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import { renderWithIntl } from '../../../testUtils';
import ErrorBanner from './ErrorBanner'; import ErrorBanner from './ErrorBanner';
import messages from '../messages'; import messages from '../messages';
describe('Error Banner component', () => { describe('Error Banner component', () => {
@@ -25,35 +23,29 @@ describe('Error Banner component', () => {
children, children,
}; };
let el; describe('behavior', () => {
beforeEach(() => { it('renders children content', () => {
el = shallow(<ErrorBanner {...props} />); renderWithIntl(<ErrorBanner {...props} />);
}); const childText = screen.getByText('Abitary Child');
expect(childText).toBeInTheDocument();
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
describe('component', () => {
test('children node', () => {
expect(el.containsMatchingElement(children)).toEqual(true);
}); });
test('verify actions', () => { it('renders the correct number of action buttons', () => {
const actions = el.find('Alert').prop('actions'); renderWithIntl(<ErrorBanner {...props} />);
expect(actions).toHaveLength(props.actions.length); const buttons = screen.getAllByText(messages.retryButton.defaultMessage);
expect(buttons).toHaveLength(2);
actions.forEach((action, index) => {
expect(action.type).toEqual('Button');
expect(action.props.onClick).toEqual(props.actions[index].onClick);
// action message
expect(action.props.children.props).toEqual(props.actions[index].message);
});
}); });
test('verify heading', () => { it('renders error heading with correct message', () => {
const heading = el.find('FormattedMessage'); renderWithIntl(<ErrorBanner {...props} />);
expect(heading.props()).toEqual(props.headingMessage); const heading = screen.getAllByText(messages.unknownError.defaultMessage)[0];
expect(heading).toBeInTheDocument();
});
it('renders with danger variant', () => {
renderWithIntl(<ErrorBanner {...props} />);
const alert = screen.getByRole('alert');
expect(alert).toHaveClass('alert-danger');
}); });
}); });
}); });

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Alert, Spinner } from '@edx/paragon'; import { Alert, Spinner } from '@openedx/paragon';
export const LoadingBanner = () => ( export const LoadingBanner = () => (
<Alert variant="info"> <Alert variant="info">

View File

@@ -1,11 +1,19 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import LoadingBanner from './LoadingBanner'; import LoadingBanner from './LoadingBanner';
describe('Loading Banner component', () => { describe('Loading Banner component', () => {
test('snapshot', () => { describe('behavior', () => {
const el = shallow(<LoadingBanner />); it('renders an info alert', () => {
expect(el).toMatchSnapshot(); render(<LoadingBanner />);
const alert = screen.getByRole('alert');
expect(alert).toHaveClass('alert-info');
});
it('renders a spinner', () => {
const { container } = render(<LoadingBanner />);
const spinner = container.querySelector('.pgn__spinner');
expect(spinner).toBeInTheDocument();
expect(spinner).toHaveClass('spinner-border');
});
}); });
}); });

View File

@@ -1,42 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Error Banner component snapshot 1`] = `
<Alert
actions={
Array [
<Button
onClick={[MockFunction action1.onClick]}
variant="outline-primary"
>
<FormattedMessage
defaultMessage="Retry"
description="Retry button for error in file renderer"
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
/>
</Button>,
<Button
onClick={[MockFunction action2.onClick]}
variant="outline-primary"
>
<FormattedMessage
defaultMessage="Retry"
description="Retry button for error in file renderer"
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
/>
</Button>,
]
}
variant="danger"
>
<Alert.Heading>
<FormattedMessage
defaultMessage="Unknown errors"
description="Unknown errors message"
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
/>
</Alert.Heading>
<p>
Abitary Child
</p>
</Alert>
`;

View File

@@ -1,12 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Loading Banner component snapshot 1`] = `
<Alert
variant="info"
>
<Spinner
animation="border"
className="d-flex m-auto"
/>
</Alert>
`;

View File

@@ -1,21 +1,40 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { render, screen } from '@testing-library/react';
import ImageRenderer from './ImageRenderer'; import ImageRenderer from './ImageRenderer';
describe('Image Renderer Component', () => { describe('Image Renderer Component', () => {
const props = { const props = {
url: 'some_url.jpg', url: 'some_url.jpg',
fileName: 'test-image.jpg',
onError: jest.fn().mockName('this.props.onError'),
onSuccess: jest.fn().mockName('this.props.onSuccess'),
}; };
props.onError = jest.fn().mockName('this.props.onError'); it('renders an image with the correct src and alt attributes', () => {
props.onSuccess = jest.fn().mockName('this.props.onSuccess'); render(<ImageRenderer {...props} />);
const imgElement = screen.getByRole('img');
let el; expect(imgElement).toBeInTheDocument();
beforeEach(() => { expect(imgElement).toHaveAttribute('src', props.url);
el = shallow(<ImageRenderer {...props} />); expect(imgElement).toHaveAttribute('alt', props.fileName);
expect(imgElement).toHaveClass('image-renderer');
}); });
test('snapshot', () => {
expect(el).toMatchSnapshot(); it('calls onSuccess when image loads successfully', () => {
render(<ImageRenderer {...props} />);
const imgElement = screen.getByRole('img');
imgElement.dispatchEvent(new Event('load'));
expect(props.onSuccess).toHaveBeenCalled();
});
it('calls onError when image fails to load', () => {
render(<ImageRenderer {...props} />);
const imgElement = screen.getByRole('img');
imgElement.dispatchEvent(new Event('error'));
expect(props.onError).toHaveBeenCalled();
}); });
}); });

View File

@@ -1,17 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Document, Page, pdfjs } from 'react-pdf';
import { pdfjs, Document, Page } from 'react-pdf';
import { import {
Icon, Form, ActionRow, IconButton, Icon, Form, ActionRow, IconButton,
} from '@edx/paragon'; } from '@openedx/paragon';
import { ChevronLeft, ChevronRight } from '@edx/paragon/icons'; import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import { rendererHooks } from './pdfHooks'; import { rendererHooks } from './pdfHooks';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker; pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
/** /**
* <PDFRenderer /> * <PDFRenderer />

View File

@@ -1,16 +1,22 @@
import React from 'react'; import { Document, Page } from 'react-pdf';
import { shallow } from 'enzyme'; import { render } from '@testing-library/react';
import PropTypes from 'prop-types';
import PDFRenderer from './PDFRenderer'; import PDFRenderer from './PDFRenderer';
import * as hooks from './pdfHooks'; import * as hooks from './pdfHooks';
jest.mock('react-pdf', () => ({ jest.mock('react-pdf', () => ({
pdfjs: { GlobalWorkerOptions: {} }, pdfjs: { GlobalWorkerOptions: {} },
Document: () => 'Document', Document: jest.fn(),
Page: () => 'Page', Page: jest.fn(),
})); }));
Document.mockImplementation((props) => <div data-testid="pdf-document">{props.children}</div>);
Document.propTypes = {
children: PropTypes.node,
};
Page.mockImplementation(() => <div data-testid="pdf-page">Page Content</div>);
jest.mock('./pdfHooks', () => ({ jest.mock('./pdfHooks', () => ({
rendererHooks: jest.fn(), rendererHooks: jest.fn(),
})); }));
@@ -33,25 +39,45 @@ describe('PDF Renderer Component', () => {
onNextPageButtonClick: jest.fn().mockName('hooks.onNextPageButtonClick'), onNextPageButtonClick: jest.fn().mockName('hooks.onNextPageButtonClick'),
onPrevPageButtonClick: jest.fn().mockName('hooks.onPrevPageButtonClick'), onPrevPageButtonClick: jest.fn().mockName('hooks.onPrevPageButtonClick'),
hasNext: true, hasNext: true,
hasPref: false, hasPrev: false,
}; };
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
describe('snapshots', () => {
test('first page, prev is disabled', () => { describe('rendering', () => {
it('should render the PDF document with navigation controls', () => {
hooks.rendererHooks.mockReturnValue(hookProps); hooks.rendererHooks.mockReturnValue(hookProps);
expect(shallow(<PDFRenderer {...props} />)).toMatchSnapshot(); const { getByTestId, getAllByText, container } = render(<PDFRenderer {...props} />);
expect(getByTestId('pdf-document')).toBeInTheDocument();
expect(getByTestId('pdf-page')).toBeInTheDocument();
expect(container.querySelector('input[type="number"]')).toBeInTheDocument();
expect(getAllByText(/Page/).length).toBeGreaterThan(0);
expect(getAllByText(`of ${hookProps.numPages}`).length).toBeGreaterThan(0);
}); });
test('on last page, next is disabled', () => {
it('should have disabled previous button when on the first page', () => {
hooks.rendererHooks.mockReturnValue({
...hookProps,
hasPrev: false,
});
const { container } = render(<PDFRenderer {...props} />);
const prevButton = container.querySelector('button[aria-label="previous pdf page"]');
expect(prevButton).toBeDisabled();
});
it('should have disabled next button when on the last page', () => {
hooks.rendererHooks.mockReturnValue({ hooks.rendererHooks.mockReturnValue({
...hookProps, ...hookProps,
pageNumber: hookProps.numPages,
hasNext: false, hasNext: false,
hasPrev: true, hasPrev: true,
}); });
expect(shallow(<PDFRenderer {...props} />)).toMatchSnapshot();
const { container } = render(<PDFRenderer {...props} />);
const nextButton = container.querySelector('button[aria-label="next pdf page"]');
expect(nextButton).toBeDisabled();
}); });
}); });
}); });

View File

@@ -1,23 +1,38 @@
import React from 'react'; import { render } from '@testing-library/react';
import { shallow } from 'enzyme';
import TXTRenderer from './TXTRenderer'; import TXTRenderer from './TXTRenderer';
jest.mock('./textHooks', () => { jest.mock('./textHooks', () => {
const content = 'test-content'; const mockRendererHooks = jest.fn().mockReturnValue({ content: 'test-content' });
return { return {
content, rendererHooks: mockRendererHooks,
rendererHooks: (args) => ({ content, rendererHooks: args }),
}; };
}); });
const textHooks = require('./textHooks');
describe('TXT Renderer Component', () => { describe('TXT Renderer Component', () => {
const props = { const props = {
url: 'some_url.txt', url: 'some_url.txt',
onError: jest.fn().mockName('this.props.onError'), onError: jest.fn().mockName('this.props.onError'),
onSuccess: jest.fn().mockName('this.props.onSuccess'), onSuccess: jest.fn().mockName('this.props.onSuccess'),
}; };
test('snapshot', () => {
expect(shallow(<TXTRenderer {...props} />)).toMatchSnapshot(); beforeEach(() => {
textHooks.rendererHooks.mockClear();
});
it('renders the text content in a pre element', () => {
const { getByText, container } = render(<TXTRenderer {...props} />);
expect(getByText('test-content')).toBeInTheDocument();
expect(container.querySelector('pre')).toHaveClass('txt-renderer');
});
it('passes the correct props to rendererHooks', () => {
render(<TXTRenderer {...props} />);
expect(textHooks.rendererHooks).toHaveBeenCalledWith({
url: props.url,
onError: props.onError,
onSuccess: props.onSuccess,
});
}); });
}); });

View File

@@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Image Renderer Component snapshot 1`] = `
<img
alt=""
className="image-renderer"
onError={[MockFunction this.props.onError]}
onLoad={[MockFunction this.props.onSuccess]}
src="some_url.jpg"
/>
`;

View File

@@ -1,137 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = `
<div
className="pdf-renderer"
>
<Document
file="some_url.pdf"
onLoadError={[MockFunction hooks.onDocumentLoadError]}
onLoadSuccess={[MockFunction hooks.onDocumentLoadSuccess]}
>
<div
className="page-wrapper"
style={
Object {
"height": 200,
}
}
>
<Page
onLoadSuccess={[MockFunction hooks.onLoadPageSuccess]}
pageNumber={1}
/>
</div>
</Document>
<ActionRow
className="d-flex justify-content-center m-0"
>
<IconButton
alt="previous pdf page"
disabled={true}
iconAs="Icon"
onClick={[MockFunction hooks.onPrevPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronLeft]}
/>
<Form.Group
className="d-flex align-items-center m-0"
>
<Form.Label
isInline={true}
>
Page
</Form.Label>
<Form.Control
max={10}
min={0}
onChange={[MockFunction hooks.onInputPageChange]}
type="number"
value={1}
/>
<Form.Label
isInline={true}
>
of
10
</Form.Label>
</Form.Group>
<IconButton
alt="next pdf page"
disabled={false}
iconAs="Icon"
onClick={[MockFunction hooks.onNextPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronRight]}
/>
</ActionRow>
</div>
`;
exports[`PDF Renderer Component snapshots on last page, next is disabled 1`] = `
<div
className="pdf-renderer"
>
<Document
file="some_url.pdf"
onLoadError={[MockFunction hooks.onDocumentLoadError]}
onLoadSuccess={[MockFunction hooks.onDocumentLoadSuccess]}
>
<div
className="page-wrapper"
style={
Object {
"height": 200,
}
}
>
<Page
onLoadSuccess={[MockFunction hooks.onLoadPageSuccess]}
pageNumber={10}
/>
</div>
</Document>
<ActionRow
className="d-flex justify-content-center m-0"
>
<IconButton
alt="previous pdf page"
disabled={false}
iconAs="Icon"
onClick={[MockFunction hooks.onPrevPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronLeft]}
/>
<Form.Group
className="d-flex align-items-center m-0"
>
<Form.Label
isInline={true}
>
Page
</Form.Label>
<Form.Control
max={10}
min={0}
onChange={[MockFunction hooks.onInputPageChange]}
type="number"
value={10}
/>
<Form.Label
isInline={true}
>
of
10
</Form.Label>
</Form.Group>
<IconButton
alt="next pdf page"
disabled={true}
iconAs="Icon"
onClick={[MockFunction hooks.onNextPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronRight]}
/>
</ActionRow>
</div>
`;

View File

@@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TXT Renderer Component snapshot 1`] = `
<pre
className="txt-renderer"
>
test-content
</pre>
`;

View File

@@ -1,16 +1,11 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { pdfjs } from 'react-pdf';
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css'; import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import { ErrorStatuses } from 'data/constants/requests'; import { ErrorStatuses } from 'data/constants/requests';
import { StrictDict } from 'utils'; import { StrictDict } from 'utils';
import * as module from './pdfHooks'; import * as module from './pdfHooks';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export const errors = StrictDict({ export const errors = StrictDict({
missingPDF: 'MissingPDFException', missingPDF: 'MissingPDFException',
}); });

View File

@@ -12,6 +12,11 @@ jest.mock('react-pdf', () => ({
Page: () => 'Page', Page: () => 'Page',
})); }));
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn((val) => ({ current: val, useRef: true })),
}));
const state = new MockUseState(hooks); const state = new MockUseState(hooks);
const hookKeys = keyStore(hooks); const hookKeys = keyStore(hooks);

View File

@@ -18,7 +18,7 @@ export const fetchFile = async ({
onSuccess(); onSuccess();
setContent(data); setContent(data);
}) })
.catch((e) => onError(e.response.status)); .catch((e) => onError(e.response?.status));
export const rendererHooks = ({ url, onError, onSuccess }) => { export const rendererHooks = ({ url, onError, onSuccess }) => {
const [content, setContent] = module.state.content(''); const [content, setContent] = module.state.content('');

View File

@@ -10,6 +10,11 @@ jest.mock('axios', () => ({
get: jest.fn(), get: jest.fn(),
})); }));
jest.mock('react', () => ({
...jest.requireActual('react'),
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
}));
const hookKeys = keyStore(hooks); const hookKeys = keyStore(hooks);
const state = new MockUseState(hooks); const state = new MockUseState(hooks);

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Card, Collapsible } from '@edx/paragon'; import { Card, Collapsible } from '@openedx/paragon';
import FilePopoverContent from 'components/FilePopoverContent'; import FilePopoverContent from 'components/FilePopoverContent';
import FileInfo from './FileInfo'; import FileInfo from './FileInfo';
@@ -17,7 +17,7 @@ export const FileCard = ({ file, children }) => (
defaultOpen defaultOpen
title={<h3 className="file-card-title">{file.name}</h3>} title={<h3 className="file-card-title">{file.name}</h3>}
> >
<div className="preview-panel"> <div className="preview-panel" data-testid="preview-panel">
<FileInfo><FilePopoverContent {...file} /></FileInfo> <FileInfo><FilePopoverContent {...file} /></FileInfo>
{children} {children}
</div> </div>

View File

@@ -1,7 +1,5 @@
@import "@edx/paragon/scss/core/core";
.file-card { .file-card {
margin: map-get($spacers, 1) 0; margin: var(--pgn-spacing-spacer-1) 0;
.file-card-title { .file-card-title {
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -26,8 +24,8 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
@include media-breakpoint-down(sm) { @media (--pgn-size-breakpoint-max-width-sm) {
.file-card-title { .file-card-title {
width: calc(map-get($container-max-widths, "sm")/2); width: calc(var(--pgn-size-container-max-width-sm)/2);
} }
} }

View File

@@ -1,10 +1,4 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import { Collapsible } from '@edx/paragon';
import FilePopoverContent from 'components/FilePopoverContent';
import FileInfo from './FileInfo';
import FileCard from './FileCard'; import FileCard from './FileCard';
jest.mock('components/FilePopoverContent', () => 'FilePopoverContent'); jest.mock('components/FilePopoverContent', () => 'FilePopoverContent');
@@ -19,24 +13,27 @@ describe('File Preview Card component', () => {
}, },
}; };
const children = (<h1>some children</h1>); const children = (<h1>some children</h1>);
let el;
beforeEach(() => {
el = shallow(<FileCard {...props}>{children}</FileCard>);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
describe('Component', () => { describe('Component', () => {
test('collapsible title is name header', () => { it('renders with the file name in the title', () => {
const title = el.find(Collapsible).prop('title'); render(<FileCard {...props}>{children}</FileCard>);
expect(title).toEqual(<h3 className="file-card-title">{props.file.name}</h3>); expect(screen.getByText(props.file.name)).toBeInTheDocument();
expect(screen.getByText(props.file.name)).toHaveClass('file-card-title');
}); });
test('forwards children into preview-panel', () => {
const previewPanelChildren = el.find('.preview-panel').children(); it('renders the preview panel with file info', () => {
expect(previewPanelChildren.at(0).equals( render(<FileCard {...props}>{children}</FileCard>);
<FileInfo><FilePopoverContent file={props.file} /></FileInfo>, const previewPanel = screen.getByTestId('preview-panel');
)); expect(previewPanel).toBeInTheDocument();
expect(previewPanelChildren.at(1).equals(children)).toEqual(true); expect(document.querySelector('FileInfo')).toBeInTheDocument();
expect(document.querySelector('FilePopoverContent')).toBeInTheDocument();
});
it('renders children in the preview panel', () => {
render(<FileCard {...props}>{children}</FileCard>);
const previewPanel = screen.getByTestId('preview-panel');
expect(previewPanel).toBeInTheDocument();
expect(screen.getByText('some children')).toBeInTheDocument();
}); });
}); });
}); });

View File

@@ -5,8 +5,8 @@ import {
Button, Button,
OverlayTrigger, OverlayTrigger,
Popover, Popover,
} from '@edx/paragon'; } from '@openedx/paragon';
import { InfoOutline } from '@edx/paragon/icons'; import { InfoOutline } from '@openedx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { nullMethod } from 'hooks'; import { nullMethod } from 'hooks';
import messages from './messages'; import messages from './messages';

View File

@@ -1,25 +1,29 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import userEvent from '@testing-library/user-event';
import { renderWithIntl } from '../../testUtils';
import { Popover } from '@edx/paragon';
import FileInfo from './FileInfo'; import FileInfo from './FileInfo';
import messages from './messages';
describe('File Preview Card component', () => { describe('FileInfo component', () => {
const children = (<h1>some Children</h1>); const children = (<h1>some Children</h1>);
const props = { onClick: jest.fn().mockName('this.props.onClick') }; const props = { onClick: jest.fn().mockName('this.props.onClick') };
let el;
beforeEach(() => { beforeEach(() => {
el = shallow(<FileInfo {...props}>{children}</FileInfo>); jest.clearAllMocks();
}); });
test('snapshot', () => {
expect(el).toMatchSnapshot(); describe('Component rendering', () => {
}); it('renders the FileInfo button with correct text', () => {
describe('Component', () => { renderWithIntl(<FileInfo {...props}>{children}</FileInfo>);
test('overlay with passed children', () => { expect(screen.getByText(messages.fileInfo.defaultMessage)).toBeInTheDocument();
const { overlay } = el.at(0).props(); });
expect(overlay.type).toEqual(Popover);
expect(overlay.props.children).toEqual(<Popover.Content>{children}</Popover.Content>); it('calls onClick when button is clicked', async () => {
renderWithIntl(<FileInfo {...props}>{children}</FileInfo>);
const user = userEvent.setup();
await user.click(screen.getByText(messages.fileInfo.defaultMessage));
expect(props.onClick).toHaveBeenCalledTimes(1);
}); });
}); });
}); });

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import FileCard from './FileCard'; import FileCard from './FileCard';
import { ErrorBanner, LoadingBanner } from './Banners'; import { ErrorBanner, LoadingBanner } from './Banners';
@@ -12,8 +12,8 @@ import { renderHooks } from './hooks';
*/ */
export const FileRenderer = ({ export const FileRenderer = ({
file, file,
intl,
}) => { }) => {
const intl = useIntl();
const { const {
Renderer, Renderer,
isLoading, isLoading,
@@ -39,8 +39,6 @@ FileRenderer.propTypes = {
name: PropTypes.string, name: PropTypes.string,
downloadUrl: PropTypes.string, downloadUrl: PropTypes.string,
}).isRequired, }).isRequired,
// injected
intl: intlShape.isRequired,
}; };
export default injectIntl(FileRenderer); export default FileRenderer;

View File

@@ -1,53 +1,79 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { keyStore } from 'utils'; import { keyStore } from 'utils';
import { ErrorStatuses } from 'data/constants/requests'; import { ErrorStatuses } from 'data/constants/requests';
import { renderWithIntl } from '../../testUtils';
import { FileRenderer } from './FileRenderer'; import { FileRenderer } from './FileRenderer';
import * as hooks from './hooks'; import * as hooks from './hooks';
jest.mock('./FileCard', () => 'FileCard');
jest.mock('./Banners', () => ({
ErrorBanner: () => 'ErrorBanner',
LoadingBanner: () => 'LoadingBanner',
}));
const hookKeys = keyStore(hooks); const hookKeys = keyStore(hooks);
const props = { const props = {
file: { file: {
downloadUrl: 'file download url', downloadUrl: 'file download url',
name: 'filename.txt', name: 'filename.txt',
description: 'A text file',
}, },
intl: { formatMessage },
}; };
describe('FileRenderer', () => { describe('FileRenderer', () => {
describe('component', () => { describe('component', () => {
describe('snapshot', () => { it('renders loading banner when isLoading is true', () => {
test('isLoading, no Error', () => { const hookProps = {
const hookProps = { Renderer: () => <div data-testid="mock-renderer">Renderer Component</div>,
Renderer: () => 'Renderer', isLoading: true,
isloading: true, errorStatus: null,
errorStatus: null, error: null,
error: null, rendererProps: { prop: 'hooks.rendererProps' },
rendererProps: { prop: 'hooks.rendererProps' }, };
}; jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps); renderWithIntl(<FileRenderer {...props} />);
expect(shallow(<FileRenderer {...props} />)).toMatchSnapshot();
}); expect(screen.getByText('filename.txt')).toBeInTheDocument();
test('is not loading, with error', () => { expect(screen.getByTestId('mock-renderer')).toBeInTheDocument();
const hookProps = { const spinner = document.querySelector('.spinner-border');
Renderer: () => 'Renderer', expect(spinner).toBeInTheDocument();
isloading: false, });
errorStatus: ErrorStatuses.serverError, it('renders error banner when there is an error status', () => {
error: { prop: 'hooks.errorProps' }, const errorProps = {
rendererProps: { prop: 'hooks.rendererProps' }, headingMessage: { id: 'error.heading', defaultMessage: 'Error Heading' },
}; children: 'Error Message',
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps); actions: [{ id: 'retry', onClick: jest.fn(), message: { id: 'retry', defaultMessage: 'Retry' } }],
expect(shallow(<FileRenderer {...props} />)).toMatchSnapshot(); };
});
const hookProps = {
Renderer: () => <div data-testid="mock-renderer">Renderer Component</div>,
isLoading: false,
errorStatus: ErrorStatuses.serverError,
error: errorProps,
rendererProps: { prop: 'hooks.rendererProps' },
};
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
renderWithIntl(<FileRenderer {...props} />);
expect(screen.getByText('filename.txt')).toBeInTheDocument();
expect(screen.getByText('Error Message')).toBeInTheDocument();
expect(document.querySelector('.alert-heading')).toBeInTheDocument();
expect(document.querySelector('.btn.btn-outline-primary')).toBeInTheDocument();
});
it('renders renderer component when not loading and no error', () => {
const hookProps = {
Renderer: () => <div data-testid="mock-renderer">Renderer Component</div>,
isLoading: false,
errorStatus: null,
error: null,
rendererProps: { prop: 'hooks.rendererProps' },
};
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
renderWithIntl(<FileRenderer {...props} />);
expect(screen.getByText('filename.txt')).toBeInTheDocument();
expect(screen.getByTestId('mock-renderer')).toBeInTheDocument();
expect(screen.getByText('Renderer Component')).toBeInTheDocument();
const spinner = document.querySelector('.spinner-border');
expect(spinner).not.toBeInTheDocument();
}); });
}); });
}); });

View File

@@ -1,35 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`File Preview Card component snapshot 1`] = `
<Card
className="file-card"
key="test-file-name.pdf"
>
<Collapsible
className="file-collapsible"
defaultOpen={true}
title={
<h3
className="file-card-title"
>
test-file-name.pdf
</h3>
}
>
<div
className="preview-panel"
>
<FileInfo>
<FilePopoverContent
description="test-file description"
downloadUrl="destination/test-file-name.pdf"
name="test-file-name.pdf"
/>
</FileInfo>
<h1>
some children
</h1>
</div>
</Collapsible>
</Card>
`;

View File

@@ -1,34 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`File Preview Card component snapshot 1`] = `
<OverlayTrigger
flip={true}
overlay={
<Popover
className="overlay-help-popover"
id="file-popover"
>
<Popover.Content>
<h1>
some Children
</h1>
</Popover.Content>
</Popover>
}
placement="right-end"
trigger="focus"
>
<Button
iconAfter={[MockFunction icons.InfoOutline]}
onClick={[MockFunction this.props.onClick]}
size="sm"
variant="tertiary"
>
<FormattedMessage
defaultMessage="File info"
description="Popover trigger button text for file preview card"
id="ora-grading.InfoPopover.fileInfo"
/>
</Button>
</OverlayTrigger>
`;

View File

@@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileRenderer component snapshot is not loading, with error 1`] = `
<FileCard
file={
Object {
"downloadUrl": "file download url",
"name": "filename.txt",
}
}
key="file download url"
>
<ErrorBanner
prop="hooks.errorProps"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot isLoading, no Error 1`] = `
<FileCard
file={
Object {
"downloadUrl": "file download url",
"name": "filename.txt",
}
}
key="file download url"
>
<Renderer
prop="hooks.rendererProps"
/>
</FileCard>
`;

View File

@@ -79,7 +79,7 @@ export const renderHooks = ({
message: messages.retryButton, message: messages.retryButton,
}; };
const error = { const error = {
headerMessage: errorMessage, headingMessage: errorMessage,
children: intl.formatMessage(errorMessage), children: intl.formatMessage(errorMessage),
actions: [errorAction], actions: [errorAction],
}; };

View File

@@ -55,7 +55,7 @@ describe('FilePreview hooks', () => {
}); });
describe('error', () => { describe('error', () => {
it('loads message from current error status, if valid, else from serverError', () => { it('loads message from current error status, if valid, else from serverError', () => {
expect(hook.error.headerMessage).toEqual( expect(hook.error.headingMessage).toEqual(
hooks.ERROR_STATUSES[ErrorStatuses.serverError], hooks.ERROR_STATUSES[ErrorStatuses.serverError],
); );
expect(hook.error.children).toEqual( expect(hook.error.children).toEqual(
@@ -63,7 +63,7 @@ describe('FilePreview hooks', () => {
); );
state.mockVal(state.keys.errorStatus, ErrorStatuses.notFound); state.mockVal(state.keys.errorStatus, ErrorStatuses.notFound);
hook = hooks.renderHooks({ intl: { formatMessage }, file }); hook = hooks.renderHooks({ intl: { formatMessage }, file });
expect(hook.error.headerMessage).toEqual( expect(hook.error.headingMessage).toEqual(
hooks.ERROR_STATUSES[ErrorStatuses.notFound], hooks.ERROR_STATUSES[ErrorStatuses.notFound],
); );
expect(hook.error.children).toEqual( expect(hook.error.children).toEqual(

View File

@@ -1,14 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Head snapshot 1`] = `
<Helmet>
<title>
ORA staff grading | site-name
</title>
<link
href="favicon-url"
rel="shortcut icon"
type="image/x-icon"
/>
</Helmet>
`;

View File

@@ -1,25 +1,45 @@
import React from 'react'; import { render } from '@testing-library/react';
import { getConfig } from '@edx/frontend-platform'; import { Helmet } from 'react-helmet';
import { shallow } from 'enzyme';
import Head from '.'; import Head from '.';
jest.mock('react-helmet', () => ({ jest.mock('@edx/frontend-platform/i18n', () => ({
Helmet: 'Helmet', useIntl: () => ({
formatMessage: (message, values) => {
if (message.defaultMessage && values) {
return message.defaultMessage.replace('{siteName}', values.siteName);
}
return message.defaultMessage || message.id;
},
}),
defineMessages: (messages) => messages,
})); }));
jest.mock('react-helmet', () => ({
Helmet: jest.fn(),
}));
Helmet.mockImplementation(({ children }) => <div data-testid="helmet-mock">{children}</div>);
jest.mock('@edx/frontend-platform', () => ({ jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({ getConfig: jest.fn().mockReturnValue({
SITE_NAME: 'site-name', SITE_NAME: 'site-name',
FAVICON_URL: 'favicon-url', FAVICON_URL: 'favicon-url',
}), }),
})); }));
describe('Head', () => { describe('Head', () => {
it('snapshot', () => { it('should render page title with site name from config', () => {
const el = shallow(<Head />); const { container } = render(<Head />);
expect(el).toMatchSnapshot(); const titleElement = container.querySelector('title');
expect(titleElement).toBeInTheDocument();
expect(titleElement.textContent).toContain('ORA staff grading | site-name');
});
expect(el.find('title').text()).toContain(getConfig().SITE_NAME); it('should render favicon link with URL from config', () => {
expect(el.find('link').prop('href')).toEqual(getConfig().FAVICON_URL); const { container } = render(<Head />);
const faviconLink = container.querySelector('link[rel="shortcut icon"]');
expect(faviconLink).toBeInTheDocument();
expect(faviconLink.getAttribute('href')).toEqual('favicon-url');
expect(faviconLink.getAttribute('type')).toEqual('image/x-icon');
}); });
}); });

View File

@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Info Popover Component snapshot 1`] = `
<OverlayTrigger
flip={true}
overlay={
<Popover
className="overlay-help-popover"
id="info-popover"
>
<Popover.Content>
<div>
Children component
</div>
</Popover.Content>
</Popover>
}
placement="right-end"
trigger="focus"
>
<IconButton
alt="Display more info"
className="esg-help-icon"
iconAs="Icon"
onClick={[MockFunction this.props.onClick]}
src={[MockFunction icons.InfoOutline]}
/>
</OverlayTrigger>
`;

View File

@@ -6,9 +6,9 @@ import {
Popover, Popover,
Icon, Icon,
IconButton, IconButton,
} from '@edx/paragon'; } from '@openedx/paragon';
import { InfoOutline } from '@edx/paragon/icons'; import { InfoOutline } from '@openedx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { nullMethod } from 'hooks'; import { nullMethod } from 'hooks';
@@ -17,26 +17,35 @@ import messages from './messages';
/** /**
* <InfoPopover /> * <InfoPopover />
*/ */
export const InfoPopover = ({ onClick, children, intl }) => ( export const InfoPopover = (
<OverlayTrigger {
trigger="focus" onClick,
placement="right-end" children,
flip },
overlay={( ) => {
<Popover id="info-popover" className="overlay-help-popover"> const intl = useIntl();
<Popover.Content>{children}</Popover.Content> return (
</Popover> <OverlayTrigger
)} trigger="focus"
> placement="left-end"
<IconButton flip
className="esg-help-icon" overlay={(
src={InfoOutline} <Popover id="info-popover" className="overlay-help-popover">
alt={intl.formatMessage(messages.altText)} <Popover.Content>{children}</Popover.Content>
iconAs={Icon} </Popover>
onClick={onClick} )}
/> >
</OverlayTrigger> <IconButton
); className="esg-help-icon"
data-testid="esg-help-icon"
src={InfoOutline}
alt={intl.formatMessage(messages.altText)}
iconAs={Icon}
onClick={onClick}
/>
</OverlayTrigger>
);
};
InfoPopover.defaultProps = { InfoPopover.defaultProps = {
onClick: nullMethod, onClick: nullMethod,
@@ -47,7 +56,6 @@ InfoPopover.propTypes = {
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),
PropTypes.node, PropTypes.node,
]).isRequired, ]).isRequired,
intl: intlShape.isRequired,
}; };
export default injectIntl(InfoPopover); export default InfoPopover;

View File

@@ -1,23 +1,31 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import userEvent from '@testing-library/user-event';
import { renderWithIntl } from '../../testUtils';
import { formatMessage } from 'testUtils';
import { InfoPopover } from '.'; import { InfoPopover } from '.';
describe('Info Popover Component', () => { describe('Info Popover Component', () => {
const child = <div>Children component</div>; const child = <div>Children component</div>;
const onClick = jest.fn().mockName('this.props.onClick'); const onClick = jest.fn().mockName('this.props.onClick');
let el;
beforeEach(() => {
el = shallow(<InfoPopover onClick={onClick} intl={{ formatMessage }}>{child}</InfoPopover>);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
describe('Component', () => { describe('Component', () => {
test('Test component render', () => { it('renders the help icon button', () => {
expect(el.length).toEqual(1); renderWithIntl(
expect(el.find('.esg-help-icon').length).toEqual(1); <InfoPopover onClick={onClick}>
{child}
</InfoPopover>,
);
expect(screen.getByTestId('esg-help-icon')).toBeInTheDocument();
});
it('calls onClick when the help icon is clicked', async () => {
renderWithIntl(
<InfoPopover onClick={onClick}>
{child}
</InfoPopover>,
);
const user = userEvent.setup();
await user.click(screen.getByTestId('esg-help-icon'));
expect(onClick).toHaveBeenCalled();
}); });
}); });
}); });

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Spinner } from '@edx/paragon'; import { Spinner } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';
/** /**

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Badge } from '@edx/paragon'; import { Badge } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils'; import { StrictDict } from 'utils';

View File

@@ -1,34 +1,36 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import { gradingStatuses } from 'data/services/lms/constants'; import { gradingStatuses } from 'data/services/lms/constants';
import messages from '../data/services/lms/messages';
import { renderWithIntl } from '../testUtils';
import { StatusBadge } from './StatusBadge'; import { StatusBadge } from './StatusBadge';
const className = 'test-className'; const className = 'test-className';
describe('StatusBadge component', () => { describe('StatusBadge component', () => {
const render = (status) => shallow(<StatusBadge className={className} status={status} />);
describe('behavior', () => { describe('behavior', () => {
it('does not render if status does not have configured variant', () => { it('does not render if status does not have configured variant', () => {
const el = render('arbitrary'); const { container } = renderWithIntl(<StatusBadge className={className} status="arbitrary" />);
expect(el).toMatchSnapshot(); expect(container.firstChild).toBeNull();
expect(el.isEmptyRender()).toEqual(true);
}); });
describe('status snapshots: loads badge with configured variant and message.', () => { describe('status rendering: loads badge with configured variant and message', () => {
test('`ungraded` shows primary button variant and message', () => { it('`ungraded` shows primary button variant and message', () => {
const el = render(gradingStatuses.ungraded); renderWithIntl(<StatusBadge className={className} status={gradingStatuses.ungraded} />);
expect(el).toMatchSnapshot(); const badge = screen.getByText(messages.ungraded.defaultMessage);
expect(badge).toHaveClass('badge-primary');
}); });
test('`locked` shows light button variant and message', () => { it('`locked` shows light button variant and message', () => {
const el = render(gradingStatuses.locked); renderWithIntl(<StatusBadge className={className} status={gradingStatuses.locked} />);
expect(el).toMatchSnapshot(); const badge = screen.getByText(messages.locked.defaultMessage);
expect(badge).toHaveClass('badge-light');
}); });
test('`graded` shows success button variant and message', () => { it('`graded` shows success button variant and message', () => {
const el = render(gradingStatuses.graded); renderWithIntl(<StatusBadge className={className} status={gradingStatuses.graded} />);
expect(el).toMatchSnapshot(); const badge = screen.getByText(messages.graded.defaultMessage);
expect(badge).toHaveClass('badge-success');
}); });
test('`inProgress` shows warning button variant and message', () => { it('`inProgress` shows warning button variant and message', () => {
const el = render(gradingStatuses.inProgress); renderWithIntl(<StatusBadge className={className} status={gradingStatuses.inProgress} />);
expect(el).toMatchSnapshot(); const badge = screen.getByText(messages['in-progress'].defaultMessage);
expect(badge).toHaveClass('badge-warning');
}); });
}); });
}); });

View File

@@ -1,59 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmModal snapshot: closed 1`] = `
<AlertModal
className="confirm-modal"
footerNode={
<ActionRow>
<Button
onClick={[MockFunction this.props.onCancel]}
variant="tertiary"
>
test-cancel-text
</Button>
<Button
onClick={[MockFunction this.props.onConfirm]}
variant="primary"
>
test-confirm-text
</Button>
</ActionRow>
}
isOpen={false}
onClose={[MockFunction hooks.nullMethod]}
title="test-title"
>
<p>
test-content
</p>
</AlertModal>
`;
exports[`ConfirmModal snapshot: open 1`] = `
<AlertModal
className="confirm-modal"
footerNode={
<ActionRow>
<Button
onClick={[MockFunction this.props.onCancel]}
variant="tertiary"
>
test-cancel-text
</Button>
<Button
onClick={[MockFunction this.props.onConfirm]}
variant="primary"
>
test-confirm-text
</Button>
</ActionRow>
}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
title="test-title"
>
<p>
test-content
</p>
</AlertModal>
`;

View File

@@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatusBadge component behavior does not render if status does not have configured variant 1`] = `""`;
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`graded\` shows success button variant and message 1`] = `
<Badge
className="test-className"
variant="success"
>
<FormattedMessage
defaultMessage="Grading Completed"
description="Grading status label for graded submission"
id="ora-grading.lms-api.gradingStatusDisplay.graded"
/>
</Badge>
`;
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`inProgress\` shows warning button variant and message 1`] = `
<Badge
className="test-className"
variant="warning"
>
<FormattedMessage
defaultMessage="You are currently grading this response"
description="Grading status label for in-progress submission"
id="ora-grading.lms-api.gradingStatusDisplay.inProgress"
/>
</Badge>
`;
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`locked\` shows light button variant and message 1`] = `
<Badge
className="test-className"
variant="light"
>
<FormattedMessage
defaultMessage="Currently being graded by someone else"
description="Grading status label for locked submission"
id="ora-grading.lms-api.gradingStatusDisplay.locked"
/>
</Badge>
`;
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`ungraded\` shows primary button variant and message 1`] = `
<Badge
className="test-className"
variant="primary"
>
<FormattedMessage
defaultMessage="Ungraded"
description="Grading status label for ungraded submission"
id="ora-grading.lms-api.gradingStatusDisplay.ungraded"
/>
</Badge>
`;

View File

@@ -1,11 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { CTA } from '.';
describe('CTA component', () => {
test('snapshots', () => {
const el = shallow(<CTA hide />);
expect(el).toMatchSnapshot();
});
});

View File

@@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CTA component snapshots 1`] = `
<PageBanner>
<span>
<FormattedMessage
defaultMessage="Thanks for using the new ORA staff grading experience. "
description="Thank user for using ora and ask for feed back"
id="ora-grading.CTA.feedbackMessage"
/>
<Hyperlink
destination="https://docs.google.com/forms/d/1Hu1rgJcCHl5_EtDb5Up3hiZ40sSUtkZQfRHJ3fWOvfQ/edit"
isInline={true}
showLaunchIcon={false}
target="_blank"
variant="muted"
>
<FormattedMessage
defaultMessage="Provide some feedback"
description="placeholder for the feedback anchor link"
id="ora-grading.CTA.linkMessage"
/>
</Hyperlink>
<FormattedMessage
defaultMessage=" and let us know what you think!"
description="inform user to provide feedback"
id="ora-grading.CTA.letUsKnowMessage"
/>
</span>
</PageBanner>
`;

View File

@@ -1,29 +0,0 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { PageBanner, Hyperlink } from '@edx/paragon';
import messages from './messages';
/**
* <CTA />
*/
export const CTA = () => (
<PageBanner>
<span>
<FormattedMessage {...messages.ctaFeedbackMessage} />
<Hyperlink
isInline
variant="muted"
destination="https://docs.google.com/forms/d/1Hu1rgJcCHl5_EtDb5Up3hiZ40sSUtkZQfRHJ3fWOvfQ/edit"
target="_blank"
showLaunchIcon={false}
>
<FormattedMessage {...messages.ctaLinkMessage} />
</Hyperlink>
<FormattedMessage {...messages.ctaLetUsKnowMessage} />
</span>
</PageBanner>
);
export default CTA;

View File

@@ -1,23 +0,0 @@
/* eslint-disable quotes */
import { defineMessages } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
const messages = defineMessages({
ctaFeedbackMessage: {
id: 'ora-grading.CTA.feedbackMessage',
defaultMessage: 'Thanks for using the new ORA staff grading experience. ',
description: 'Thank user for using ora and ask for feed back',
},
ctaLinkMessage: {
id: 'ora-grading.CTA.linkMessage',
defaultMessage: 'Provide some feedback',
description: 'placeholder for the feedback anchor link',
},
ctaLetUsKnowMessage: {
id: 'ora-grading.CTA.letUsKnowMessage',
defaultMessage: ' and let us know what you think!',
description: 'inform user to provide feedback',
},
});
export default StrictDict(messages);

View File

@@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form } from '@edx/paragon'; import { Form } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { feedbackRequirement } from 'data/services/lms/constants'; import { feedbackRequirement } from 'data/services/lms/constants';
import { actions, selectors } from 'data/redux'; import { actions, selectors } from 'data/redux';
@@ -12,59 +12,56 @@ import messages from './messages';
/** /**
* <CriterionFeedback /> * <CriterionFeedback />
*/ */
export class CriterionFeedback extends React.Component { export const CriterionFeedback = ({
constructor(props) { orderNum,
super(props); isGrading,
this.onChange = this.onChange.bind(this); config,
} setValue,
value,
isInvalid,
}) => {
const intl = useIntl();
onChange(event) { const onChange = (event) => {
this.props.setValue({ setValue({
value: event.target.value, value: event.target.value,
orderNum: this.props.orderNum, orderNum,
}); });
} };
get commentMessage() { const translate = (msg) => intl.formatMessage(msg);
const { config, isGrading } = this.props;
let commentMessage = this.translate(isGrading ? messages.addComments : messages.comments); const getCommentMessage = () => {
let commentMessage = translate(isGrading ? messages.addComments : messages.comments);
if (config === feedbackRequirement.optional) { if (config === feedbackRequirement.optional) {
commentMessage += ` ${this.translate(messages.optional)}`; commentMessage += ` ${translate(messages.optional)}`;
} }
return commentMessage; return commentMessage;
};
if (config === feedbackRequirement.disabled) {
return null;
} }
translate = (msg) => this.props.intl.formatMessage(msg); return (
<Form.Group isInvalid={isInvalid}>
render() { <Form.Control
const { as="textarea"
config, className="criterion-feedback feedback-input"
isGrading, data-testid="criterion-feedback-input"
value, floatingLabel={getCommentMessage()}
isInvalid, value={value}
} = this.props; onChange={onChange}
if (config === feedbackRequirement.disabled) { disabled={!isGrading}
return null; />
} {isInvalid && (
return ( <Form.Control.Feedback type="invalid" className="feedback-error-msg" data-testid="criterion-feedback-error-msg">
<Form.Group isInvalid={this.feedbackIsInvalid}> {translate(messages.criterionFeedbackError)}
<Form.Control </Form.Control.Feedback>
as="textarea" )}
className="criterion-feedback feedback-input" </Form.Group>
floatingLabel={this.commentMessage} );
value={value} };
onChange={this.onChange}
disabled={!isGrading}
/>
{isInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
{this.translate(messages.criterionFeedbackError)}
</Form.Control.Feedback>
)}
</Form.Group>
);
}
}
CriterionFeedback.defaultProps = { CriterionFeedback.defaultProps = {
value: '', value: '',
@@ -73,8 +70,6 @@ CriterionFeedback.defaultProps = {
CriterionFeedback.propTypes = { CriterionFeedback.propTypes = {
orderNum: PropTypes.number.isRequired, orderNum: PropTypes.number.isRequired,
isGrading: PropTypes.bool.isRequired, isGrading: PropTypes.bool.isRequired,
// injected
intl: intlShape.isRequired,
// redux // redux
config: PropTypes.string.isRequired, config: PropTypes.string.isRequired,
setValue: PropTypes.func.isRequired, setValue: PropTypes.func.isRequired,
@@ -92,6 +87,4 @@ export const mapDispatchToProps = {
setValue: actions.grading.setCriterionFeedback, setValue: actions.grading.setCriterionFeedback,
}; };
export default injectIntl( export default connect(mapStateToProps, mapDispatchToProps)(CriterionFeedback);
connect(mapStateToProps, mapDispatchToProps)(CriterionFeedback),
);

View File

@@ -1,18 +1,18 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { actions, selectors } from 'data/redux'; import { actions, selectors } from 'data/redux';
import { import {
feedbackRequirement, feedbackRequirement,
gradeStatuses, gradeStatuses,
} from 'data/services/lms/constants'; } from 'data/services/lms/constants';
import { formatMessage } from 'testUtils';
import { import {
CriterionFeedback, CriterionFeedback,
mapStateToProps, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
} from './CriterionFeedback'; } from './CriterionFeedback';
import messages from './messages'; import { renderWithIntl } from '../../testUtils';
jest.mock('data/redux/app/selectors', () => ({ jest.mock('data/redux/app/selectors', () => ({
rubric: { rubric: {
@@ -36,7 +36,6 @@ jest.mock('data/redux/grading/selectors', () => ({
describe('Criterion Feedback', () => { describe('Criterion Feedback', () => {
const props = { const props = {
intl: { formatMessage },
orderNum: 1, orderNum: 1,
config: 'config string', config: 'config string',
isGrading: true, isGrading: true,
@@ -45,119 +44,50 @@ describe('Criterion Feedback', () => {
setValue: jest.fn().mockName('this.props.setValue'), setValue: jest.fn().mockName('this.props.setValue'),
isInvalid: false, isInvalid: false,
}; };
let el;
beforeEach(() => {
el = shallow(<CriterionFeedback {...props} />);
el.instance().onChange = jest.fn().mockName('this.onChange');
});
describe('snapshot', () => {
test('is grading', () => {
expect(el.instance().render()).toMatchSnapshot();
});
test('is graded', () => {
el.setProps({
isGrading: false,
gradeStatus: gradeStatuses.graded,
});
expect(el.instance().render()).toMatchSnapshot();
});
test('feedback value is invalid', () => {
el.setProps({
isInvalid: true,
});
expect(el.instance().render()).toMatchSnapshot();
});
Object.values(feedbackRequirement).forEach((requirement) => {
test(`feedback is configured to ${requirement}`, () => {
el.setProps({
config: requirement,
});
expect(el.instance().render()).toMatchSnapshot();
});
});
});
describe('component', () => { describe('component', () => {
describe('render', () => { describe('render', () => {
test('is grading (the feedback input is not disabled)', () => { it('shows a non-disabled input when grading', () => {
expect(el.isEmptyRender()).toEqual(false); renderWithIntl(<CriterionFeedback {...props} />);
expect(el.instance().props.value).toEqual(props.value); const input = screen.getByTestId('criterion-feedback-input');
const controlEl = el.find('.feedback-input'); expect(input).toBeInTheDocument();
expect(controlEl.prop('disabled')).toEqual(false); expect(input).not.toBeDisabled();
expect(controlEl.prop('value')).toEqual(props.value); expect(input).toHaveValue(props.value);
}); });
test('is graded (the input is disabled)', () => {
el.setProps({ it('shows a disabled input when not grading', () => {
isGrading: false, renderWithIntl(
gradeStatus: gradeStatuses.graded, <CriterionFeedback {...props} isGrading={false} gradeStatus={gradeStatuses.graded} />,
}); );
expect(el.instance().props.value).toEqual(props.value); const input = screen.getByTestId('criterion-feedback-input');
const controlEl = el.find('.feedback-input'); expect(input).toBeInTheDocument();
expect(controlEl.prop('disabled')).toEqual(true); expect(input).toBeDisabled();
expect(controlEl.prop('value')).toEqual(props.value); expect(input).toHaveValue(props.value);
}); });
test('is having invalid feedback (feedback get render)', () => {
el.setProps({ it('displays an error message when feedback is invalid', () => {
isInvalid: true, renderWithIntl(<CriterionFeedback {...props} isInvalid />);
}); expect(screen.getByTestId('criterion-feedback-error-msg')).toBeInTheDocument();
const feedbackErrorEl = el.find('.feedback-error-msg');
expect(el.instance().props.isInvalid).toEqual(true);
expect(feedbackErrorEl).toBeDefined();
}); });
test('is configure to disabled (the input does not get render)', () => {
el.setProps({ it('does not render anything when config is set to disabled', () => {
config: feedbackRequirement.disabled, const { container } = renderWithIntl(
}); <CriterionFeedback {...props} config={feedbackRequirement.disabled} />,
expect(el.isEmptyRender()).toEqual(true); );
expect(container.firstChild).toBeNull();
}); });
}); });
describe('behavior', () => { describe('behavior', () => {
test('onChange call set value', () => { it('calls setValue when input value changes', async () => {
el = shallow(<CriterionFeedback {...props} />); renderWithIntl(<CriterionFeedback {...props} />);
el.instance().onChange({ const user = userEvent.setup();
target: { const input = screen.getByTestId('criterion-feedback-input');
value: 'some value', await user.clear(input);
}, expect(props.setValue).toHaveBeenCalledWith({
value: '',
orderNum: props.orderNum,
}); });
expect(props.setValue).toBeCalledTimes(1);
});
});
describe('getter commentMessage', () => {
test('is grading', () => {
el.setProps({ config: feedbackRequirement.optional, isGrading: true });
expect(el.instance().commentMessage).toContain(
messages.optional.defaultMessage,
);
el.setProps({ config: feedbackRequirement.required });
expect(el.instance().commentMessage).not.toContain(
messages.optional.defaultMessage,
);
expect(el.instance().commentMessage).toContain(
messages.addComments.defaultMessage,
);
});
test('is not grading', () => {
el.setProps({ config: feedbackRequirement.optional, isGrading: false });
expect(el.instance().commentMessage).toContain(
messages.optional.defaultMessage,
);
el.setProps({ config: feedbackRequirement.required });
expect(el.instance().commentMessage).not.toContain(
messages.optional.defaultMessage,
);
expect(el.instance().commentMessage).toContain(
messages.comments.defaultMessage,
);
}); });
}); });
}); });
@@ -169,17 +99,17 @@ describe('Criterion Feedback', () => {
beforeEach(() => { beforeEach(() => {
mapped = mapStateToProps(testState, ownProps); mapped = mapStateToProps(testState, ownProps);
}); });
test('selectors.app.rubric.criterionFeedbackConfig', () => { it('gets config from selectors.app.rubric.criterionFeedbackConfig', () => {
expect(mapped.config).toEqual( expect(mapped.config).toEqual(
selectors.app.rubric.criterionFeedbackConfig(testState, ownProps), selectors.app.rubric.criterionFeedbackConfig(testState, ownProps),
); );
}); });
test('selector.grading.selected.criterionFeedback', () => { it('gets value from selectors.grading.selected.criterionFeedback', () => {
expect(mapped.value).toEqual( expect(mapped.value).toEqual(
selectors.grading.selected.criterionFeedback(testState, ownProps), selectors.grading.selected.criterionFeedback(testState, ownProps),
); );
}); });
test('selector.grading.validation.criterionFeedbackIsInvalid', () => { it('gets isInvalid from selectors.grading.validation.criterionFeedbackIsInvalid', () => {
expect(mapped.isInvalid).toEqual( expect(mapped.isInvalid).toEqual(
selectors.grading.validation.criterionFeedbackIsInvalid( selectors.grading.validation.criterionFeedbackIsInvalid(
testState, testState,
@@ -190,7 +120,7 @@ describe('Criterion Feedback', () => {
}); });
describe('mapDispatchToProps', () => { describe('mapDispatchToProps', () => {
test('maps actions.grading.setCriterionFeedback to setValue prop', () => { it('maps actions.grading.setCriterionFeedback to setValue prop', () => {
expect(mapDispatchToProps.setValue).toEqual( expect(mapDispatchToProps.setValue).toEqual(
actions.grading.setCriterionFeedback, actions.grading.setCriterionFeedback,
); );

View File

@@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form } from '@edx/paragon'; import { Form } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { actions, selectors } from 'data/redux'; import { actions, selectors } from 'data/redux';
import messages from './messages'; import messages from './messages';
@@ -11,50 +11,46 @@ import messages from './messages';
/** /**
* <RadioCriterion /> * <RadioCriterion />
*/ */
export class RadioCriterion extends React.Component { export const RadioCriterion = ({
constructor(props) { orderNum,
super(props); isGrading,
this.onChange = this.onChange.bind(this); config,
} data,
setCriterionOption,
isInvalid,
}) => {
const intl = useIntl();
onChange(event) { const onChange = (event) => {
this.props.setCriterionOption({ setCriterionOption({
orderNum: this.props.orderNum, orderNum,
value: event.target.value, value: event.target.value,
}); });
} };
render() { return (
const { <Form.RadioSet name={config.name} value={data}>
config, {config.options.map((option) => (
data, <Form.Radio
intl, className="criteria-option align-items-center"
isGrading, key={option.name}
isInvalid, value={option.name}
} = this.props; description={intl.formatMessage(messages.optionPoints, { points: option.points })}
return ( onChange={onChange}
<Form.RadioSet name={config.name} value={data}> disabled={!isGrading}
{config.options.map((option) => ( style={{ flexShrink: 0 }}
<Form.Radio >
className="criteria-option" {option.label}
key={option.name} </Form.Radio>
value={option.name} ))}
description={intl.formatMessage(messages.optionPoints, { points: option.points })} {isInvalid && (
onChange={this.onChange} <Form.Control.Feedback type="invalid" className="feedback-error-msg">
disabled={!isGrading} {intl.formatMessage(messages.rubricSelectedError)}
> </Form.Control.Feedback>
{option.label} )}
</Form.Radio> </Form.RadioSet>
))} );
{isInvalid && ( };
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
{intl.formatMessage(messages.rubricSelectedError)}
</Form.Control.Feedback>
)}
</Form.RadioSet>
);
}
}
RadioCriterion.defaultProps = { RadioCriterion.defaultProps = {
data: { data: {
@@ -66,8 +62,6 @@ RadioCriterion.defaultProps = {
RadioCriterion.propTypes = { RadioCriterion.propTypes = {
orderNum: PropTypes.number.isRequired, orderNum: PropTypes.number.isRequired,
isGrading: PropTypes.bool.isRequired, isGrading: PropTypes.bool.isRequired,
// injected
intl: intlShape.isRequired,
// redux // redux
config: PropTypes.shape({ config: PropTypes.shape({
prompt: PropTypes.string, prompt: PropTypes.string,
@@ -98,4 +92,4 @@ export const mapDispatchToProps = {
setCriterionOption: actions.grading.setCriterionOption, setCriterionOption: actions.grading.setCriterionOption,
}; };
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(RadioCriterion)); export default connect(mapStateToProps, mapDispatchToProps)(RadioCriterion);

View File

@@ -1,13 +1,12 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import { actions, selectors } from 'data/redux'; import { actions, selectors } from 'data/redux';
import { formatMessage } from 'testUtils';
import { import {
RadioCriterion, RadioCriterion,
mapDispatchToProps, mapDispatchToProps,
mapStateToProps, mapStateToProps,
} from './RadioCriterion'; } from './RadioCriterion';
import { renderWithIntl } from '../../testUtils';
jest.mock('data/redux/app/selectors', () => ({ jest.mock('data/redux/app/selectors', () => ({
rubric: { rubric: {
@@ -31,7 +30,6 @@ jest.mock('data/redux/grading/selectors', () => ({
describe('Radio Criterion Container', () => { describe('Radio Criterion Container', () => {
const props = { const props = {
intl: { formatMessage },
orderNum: 1, orderNum: 1,
isGrading: true, isGrading: true,
config: { config: {
@@ -55,77 +53,47 @@ describe('Radio Criterion Container', () => {
}, },
], ],
}, },
data: 'selected radio option', data: 'option name',
setCriterionOption: jest.fn().mockName('this.props.setCriterionOption'), setCriterionOption: jest.fn().mockName('this.props.setCriterionOption'),
isInvalid: false, isInvalid: false,
}; };
let el; describe('component rendering', () => {
beforeEach(() => { it('should render radio buttons that are enabled when in grading mode', () => {
el = shallow(<RadioCriterion {...props} />); const { container } = renderWithIntl(<RadioCriterion {...props} />);
el.instance().onChange = jest.fn().mockName('this.onChange');
});
describe('snapshot', () => {
test('is grading', () => {
expect(el.instance().render()).toMatchSnapshot();
});
test('is not grading', () => { const radioButtons = container.querySelectorAll('input[type="radio"]');
el.setProps({ expect(radioButtons.length).toEqual(props.config.options.length);
isGrading: false,
});
expect(el.instance().render()).toMatchSnapshot();
});
test('radio contain invalid response', () => { radioButtons.forEach(button => {
el.setProps({ expect(button).not.toBeDisabled();
isInvalid: true,
});
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('component', () => {
describe('rendering', () => {
test('is grading (all options are not disabled)', () => {
expect(el.isEmptyRender()).toEqual(false);
const optionsEl = el.find('.criteria-option');
expect(optionsEl.length).toEqual(props.config.options.length);
optionsEl.forEach((optionEl) => expect(optionEl.prop('disabled')).toEqual(false));
});
test('is not grading (all options are disabled)', () => {
el.setProps({
isGrading: false,
});
expect(el.isEmptyRender()).toEqual(false);
const optionsEl = el.find('.criteria-option');
expect(optionsEl.length).toEqual(props.config.options.length);
optionsEl.forEach((optionEl) => expect(optionEl.prop('disabled')).toEqual(true));
});
test('radio contain invalid response (error response get render)', () => {
el.setProps({
isInvalid: true,
});
expect(el.isEmptyRender()).toEqual(false);
const radioErrorEl = el.find('.feedback-error-msg');
expect(el.instance().props.isInvalid).toEqual(true);
expect(radioErrorEl).toBeDefined();
}); });
}); });
describe('behavior', () => { it('should render radio buttons that are disabled when not in grading mode', () => {
test('onChange call set crition option', () => { renderWithIntl(<RadioCriterion {...props} isGrading={false} />);
el = shallow(<RadioCriterion {...props} />);
el.instance().onChange({ const radioButtons = screen.queryAllByRole('radio');
target: { expect(radioButtons.length).toEqual(props.config.options.length);
value: 'some value',
}, radioButtons.forEach(button => {
}); expect(button).toBeDisabled();
expect(props.setCriterionOption).toBeCalledTimes(1);
}); });
}); });
it('should render an error message when the criterion is invalid', () => {
const { container } = renderWithIntl(<RadioCriterion {...props} isInvalid />);
const errorMessage = container.querySelector('.feedback-error-msg');
expect(errorMessage).toBeInTheDocument();
});
it('should not render an error message when the criterion is valid', () => {
const { container } = renderWithIntl(<RadioCriterion {...props} />);
const errorMessage = container.querySelector('.feedback-error-msg');
expect(errorMessage).not.toBeInTheDocument();
});
}); });
describe('mapStateToProps', () => { describe('mapStateToProps', () => {
@@ -135,18 +103,20 @@ describe('Radio Criterion Container', () => {
beforeEach(() => { beforeEach(() => {
mapped = mapStateToProps(testState, ownProps); mapped = mapStateToProps(testState, ownProps);
}); });
test('selectors.app.rubric.criterionConfig', () => {
it('should properly map config from rubric criterion config selector', () => {
expect(mapped.config).toEqual( expect(mapped.config).toEqual(
selectors.app.rubric.criterionConfig(testState, ownProps), selectors.app.rubric.criterionConfig(testState, ownProps),
); );
}); });
test('selectors.grading.selected.criterionSelectedOption', () => { it('should properly map data from selected criterion option selector', () => {
expect(mapped.data).toEqual( expect(mapped.data).toEqual(
selectors.grading.selected.criterionSelectedOption(testState, ownProps), selectors.grading.selected.criterionSelectedOption(testState, ownProps),
); );
}); });
test('selectors.grading.validation.criterionSelectedOptionIsInvalid', () => {
it('should properly map isInvalid from criterion validation selector', () => {
expect(mapped.isInvalid).toEqual( expect(mapped.isInvalid).toEqual(
selectors.grading.validation.criterionSelectedOptionIsInvalid(testState, ownProps), selectors.grading.validation.criterionSelectedOptionIsInvalid(testState, ownProps),
); );
@@ -154,7 +124,7 @@ describe('Radio Criterion Container', () => {
}); });
describe('mapDispatchToProps', () => { describe('mapDispatchToProps', () => {
test('maps actions.grading.setCriterionFeedback to setValue prop', () => { it('should map setCriterionOption action to props', () => {
expect(mapDispatchToProps.setCriterionOption).toEqual( expect(mapDispatchToProps.setCriterionOption).toEqual(
actions.grading.setCriterionOption, actions.grading.setCriterionOption,
); );

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form, FormControlFeedback } from '@edx/paragon'; import { Form, FormControlFeedback } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { selectors } from 'data/redux'; import { selectors } from 'data/redux';
@@ -14,10 +14,10 @@ import messages from './messages';
export const ReviewCriterion = ({ config }) => ( export const ReviewCriterion = ({ config }) => (
<div className="review-criterion"> <div className="review-criterion">
{config.options.map((option) => ( {config.options.map((option) => (
<div key={option.name} className="criteria-option"> <div key={option.name} className="criteria-option" data-testid="criteria-option">
<div> <div>
<Form.Label className="option-label">{option.label}</Form.Label> <Form.Label className="option-label" data-testid="option-label">{option.label}</Form.Label>
<FormControlFeedback className="option-points"> <FormControlFeedback className="option-points" data-testid="option-points">
<FormattedMessage {...messages.optionPoints} values={{ points: option.points }} /> <FormattedMessage {...messages.optionPoints} values={{ points: option.points }} />
</FormControlFeedback> </FormControlFeedback>
</div> </div>

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { screen } from '@testing-library/react';
import { selectors } from 'data/redux'; import { selectors } from 'data/redux';
import { renderWithIntl } from '../../testUtils';
import { ReviewCriterion, mapStateToProps } from './ReviewCriterion'; import { ReviewCriterion, mapStateToProps } from './ReviewCriterion';
import messages from './messages';
jest.mock('data/redux/app/selectors', () => ({ jest.mock('data/redux/app/selectors', () => ({
rubric: { rubric: {
@@ -20,7 +20,7 @@ jest.mock('data/redux/grading/selectors', () => ({
}, },
})); }));
describe('Review Crition Container', () => { describe('Review Criterion Container', () => {
const props = { const props = {
orderNum: 1, orderNum: 1,
config: { config: {
@@ -50,29 +50,20 @@ describe('Review Crition Container', () => {
}, },
}; };
let el;
beforeEach(() => {
el = shallow(<ReviewCriterion {...props} />);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
describe('component', () => { describe('component', () => {
test('rendering (everything show up)', () => { it('renders all criteria options with correct labels and points', () => {
expect(el.isEmptyRender()).toEqual(false); renderWithIntl(<ReviewCriterion {...props} />);
const optionsEl = el.find('.criteria-option');
expect(optionsEl.length).toEqual(props.config.options.length); const optionsElements = screen.getAllByTestId('criteria-option');
optionsEl.forEach((optionEl, i) => { expect(optionsElements.length).toEqual(props.config.options.length);
const option = props.config.options[i];
expect(optionEl.key()).toEqual(option.name); props.config.options.forEach((option, index) => {
expect(optionEl.find('.option-label').childAt(0).text()).toEqual( const optionElement = optionsElements[index];
option.label, const labelElement = optionElement.querySelector('[data-testid="option-label"]');
); const pointsElement = optionElement.querySelector('[data-testid="option-points"]');
expect(optionEl.find('.option-points').childAt(0).props()).toEqual({
...messages.optionPoints, expect(labelElement.textContent).toEqual(option.label);
values: { points: option.points }, expect(pointsElement.textContent).toEqual(`${props.config.options[index].points} points`);
});
}); });
}); });
}); });
@@ -81,16 +72,18 @@ describe('Review Crition Container', () => {
const testState = { arbitrary: 'some data' }; const testState = { arbitrary: 'some data' };
const ownProps = { orderNum: props.orderNum }; const ownProps = { orderNum: props.orderNum };
let mapped; let mapped;
beforeEach(() => { beforeEach(() => {
mapped = mapStateToProps(testState, ownProps); mapped = mapStateToProps(testState, ownProps);
}); });
test('selectors.app.rubric.criterionConfig', () => {
it('should map criterion config from state', () => {
expect(mapped.config).toEqual( expect(mapped.config).toEqual(
selectors.app.rubric.criterionConfig(testState, ownProps), selectors.app.rubric.criterionConfig(testState, ownProps),
); );
}); });
test('selectors.grading.selected.criterionGradeData', () => { it('should map criterion grade data from state', () => {
expect(mapped.data).toEqual( expect(mapped.data).toEqual(
selectors.grading.selected.criterionGradeData(testState, ownProps), selectors.grading.selected.criterionGradeData(testState, ownProps),
); );

View File

@@ -1,74 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Criterion Feedback snapshot feedback is configured to disabled 1`] = `null`;
exports[`Criterion Feedback snapshot feedback is configured to optional 1`] = `
<Form.Group>
<Form.Control
as="textarea"
className="criterion-feedback feedback-input"
disabled={false}
floatingLabel="Add comments (Optional)"
onChange={[MockFunction this.onChange]}
value="criterion value"
/>
</Form.Group>
`;
exports[`Criterion Feedback snapshot feedback is configured to required 1`] = `
<Form.Group>
<Form.Control
as="textarea"
className="criterion-feedback feedback-input"
disabled={false}
floatingLabel="Add comments"
onChange={[MockFunction this.onChange]}
value="criterion value"
/>
</Form.Group>
`;
exports[`Criterion Feedback snapshot feedback value is invalid 1`] = `
<Form.Group>
<Form.Control
as="textarea"
className="criterion-feedback feedback-input"
disabled={false}
floatingLabel="Add comments"
onChange={[MockFunction this.onChange]}
value="criterion value"
/>
<Form.Control.Feedback
className="feedback-error-msg"
type="invalid"
>
The feedback is required
</Form.Control.Feedback>
</Form.Group>
`;
exports[`Criterion Feedback snapshot is graded 1`] = `
<Form.Group>
<Form.Control
as="textarea"
className="criterion-feedback feedback-input"
disabled={true}
floatingLabel="Comments"
onChange={[MockFunction this.onChange]}
value="criterion value"
/>
</Form.Group>
`;
exports[`Criterion Feedback snapshot is grading 1`] = `
<Form.Group>
<Form.Control
as="textarea"
className="criterion-feedback feedback-input"
disabled={false}
floatingLabel="Add comments"
onChange={[MockFunction this.onChange]}
value="criterion value"
/>
</Form.Group>
`;

View File

@@ -1,85 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Radio Criterion Container snapshot is grading 1`] = `
<Form.RadioSet
name="random name"
value="selected radio option"
>
<Form.Radio
className="criteria-option"
description="1 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name"
>
this label
</Form.Radio>
<Form.Radio
className="criteria-option"
description="2 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name 2"
>
this label 2
</Form.Radio>
</Form.RadioSet>
`;
exports[`Radio Criterion Container snapshot is not grading 1`] = `
<Form.RadioSet
name="random name"
value="selected radio option"
>
<Form.Radio
className="criteria-option"
description="1 points"
disabled={true}
onChange={[MockFunction this.onChange]}
value="option name"
>
this label
</Form.Radio>
<Form.Radio
className="criteria-option"
description="2 points"
disabled={true}
onChange={[MockFunction this.onChange]}
value="option name 2"
>
this label 2
</Form.Radio>
</Form.RadioSet>
`;
exports[`Radio Criterion Container snapshot radio contain invalid response 1`] = `
<Form.RadioSet
name="random name"
value="selected radio option"
>
<Form.Radio
className="criteria-option"
description="1 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name"
>
this label
</Form.Radio>
<Form.Radio
className="criteria-option"
description="2 points"
disabled={false}
onChange={[MockFunction this.onChange]}
value="option name 2"
>
this label 2
</Form.Radio>
<Form.Control.Feedback
className="feedback-error-msg"
type="invalid"
>
Rubric selection is required
</Form.Control.Feedback>
</Form.RadioSet>
`;

View File

@@ -1,60 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Review Crition Container snapshot 1`] = `
<div
className="review-criterion"
>
<div
className="criteria-option"
key="option name"
>
<div>
<Form.Label
className="option-label"
>
this label
</Form.Label>
<FormControlFeedback
className="option-points"
>
<FormattedMessage
defaultMessage="{points} points"
description="criterion option point value display"
id="ora-grading.RadioCriterion.optionPoints"
values={
Object {
"points": 1,
}
}
/>
</FormControlFeedback>
</div>
</div>
<div
className="criteria-option"
key="option name 2"
>
<div>
<Form.Label
className="option-label"
>
this label 2
</Form.Label>
<FormControlFeedback
className="option-points"
>
<FormattedMessage
defaultMessage="{points} points"
description="criterion option point value display"
id="ora-grading.RadioCriterion.optionPoints"
values={
Object {
"points": 2,
}
}
/>
</FormControlFeedback>
</div>
</div>
</div>
`;

View File

@@ -1,144 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Criterion Container snapshot is graded and is not grading 1`] = `
<Form.Group>
<Form.Label
className="criteria-label"
>
<span
className="criteria-title"
>
prompt
</span>
<InfoPopover>
<div
className="help-popover-option"
key="option name"
>
<strong>
this label
</strong>
<br />
explanation
</div>
<div
className="help-popover-option"
key="option name 2"
>
<strong>
this label 2
</strong>
<br />
explanation 2
</div>
</InfoPopover>
</Form.Label>
<div
className="rubric-criteria"
>
<RadioCriterion
isGrading={false}
orderNum={1}
/>
</div>
<CriterionFeedback
isGrading={false}
orderNum={1}
/>
</Form.Group>
`;
exports[`Criterion Container snapshot is ungraded and is grading 1`] = `
<Form.Group>
<Form.Label
className="criteria-label"
>
<span
className="criteria-title"
>
prompt
</span>
<InfoPopover>
<div
className="help-popover-option"
key="option name"
>
<strong>
this label
</strong>
<br />
explanation
</div>
<div
className="help-popover-option"
key="option name 2"
>
<strong>
this label 2
</strong>
<br />
explanation 2
</div>
</InfoPopover>
</Form.Label>
<div
className="rubric-criteria"
>
<RadioCriterion
isGrading={true}
orderNum={1}
/>
</div>
<CriterionFeedback
isGrading={true}
orderNum={1}
/>
</Form.Group>
`;
exports[`Criterion Container snapshot is ungraded and is not grading 1`] = `
<Form.Group>
<Form.Label
className="criteria-label"
>
<span
className="criteria-title"
>
prompt
</span>
<InfoPopover>
<div
className="help-popover-option"
key="option name"
>
<strong>
this label
</strong>
<br />
explanation
</div>
<div
className="help-popover-option"
key="option name 2"
>
<strong>
this label 2
</strong>
<br />
explanation 2
</div>
</InfoPopover>
</Form.Label>
<div
className="rubric-criteria"
>
<ReviewCriterion
orderNum={1}
/>
</div>
<CriterionFeedback
isGrading={false}
orderNum={1}
/>
</Form.Group>
`;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form } from '@edx/paragon'; import { Form } from '@openedx/paragon';
import { selectors } from 'data/redux'; import { selectors } from 'data/redux';
import { gradeStatuses } from 'data/services/lms/constants'; import { gradeStatuses } from 'data/services/lms/constants';
@@ -25,7 +25,7 @@ export const CriterionContainer = (props) => {
<span className="criteria-title">{config.prompt}</span> <span className="criteria-title">{config.prompt}</span>
<InfoPopover> <InfoPopover>
{config.options.map((option) => ( {config.options.map((option) => (
<div key={option.name} className="help-popover-option"> <div key={option.name} className="help-popover-option" data-testid="help-popover-option">
<strong>{option.label}</strong> <strong>{option.label}</strong>
<br /> <br />
{option.explanation} {option.explanation}
@@ -33,7 +33,7 @@ export const CriterionContainer = (props) => {
))} ))}
</InfoPopover> </InfoPopover>
</Form.Label> </Form.Label>
<div className="rubric-criteria"> <div className="rubric-criteria" data-testid="rubric-criteria">
{isGrading || gradeStatus === gradeStatuses.graded ? ( {isGrading || gradeStatus === gradeStatuses.graded ? (
<RadioCriterion orderNum={orderNum} isGrading={isGrading} /> <RadioCriterion orderNum={orderNum} isGrading={isGrading} />
) : ( ) : (

View File

@@ -1,15 +1,50 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import PropTypes from 'prop-types';
import { selectors } from 'data/redux'; import { selectors } from 'data/redux';
import { gradeStatuses } from 'data/services/lms/constants'; import { gradeStatuses } from 'data/services/lms/constants';
import { CriterionContainer, mapStateToProps } from '.'; import { CriterionContainer, mapStateToProps } from '.';
jest.mock('components/InfoPopover', () => 'InfoPopover'); const MockRadioCriterion = ({ orderNum, isGrading }) => (
jest.mock('./RadioCriterion', () => 'RadioCriterion'); <div data-testid="radio-criterion-component">
jest.mock('./CriterionFeedback', () => 'CriterionFeedback'); RadioCriterion Component (orderNum={orderNum}, isGrading={String(isGrading)})
jest.mock('./ReviewCriterion', () => 'ReviewCriterion'); </div>
);
MockRadioCriterion.propTypes = {
orderNum: PropTypes.number.isRequired,
isGrading: PropTypes.bool.isRequired,
};
const MockReviewCriterion = ({ orderNum }) => (
<div data-testid="review-criterion-component">
ReviewCriterion Component (orderNum={orderNum})
</div>
);
MockReviewCriterion.propTypes = {
orderNum: PropTypes.number.isRequired,
};
const MockCriterionFeedback = ({ orderNum, isGrading }) => (
<div data-testid="criterion-feedback-component">
CriterionFeedback Component (orderNum={orderNum}, isGrading={String(isGrading)})
</div>
);
MockCriterionFeedback.propTypes = {
orderNum: PropTypes.number.isRequired,
isGrading: PropTypes.bool.isRequired,
};
const MockInfoPopover = ({ children }) => (
<div data-testid="info-popover">{children}</div>
);
MockInfoPopover.propTypes = {
children: PropTypes.node.isRequired,
};
jest.mock('data/redux/app/selectors', () => ({ jest.mock('data/redux/app/selectors', () => ({
rubric: { rubric: {
@@ -18,12 +53,18 @@ jest.mock('data/redux/app/selectors', () => ({
})), })),
}, },
})); }));
jest.mock('data/redux/grading/selectors', () => ({ jest.mock('data/redux/grading/selectors', () => ({
selected: { selected: {
gradeStatus: jest.fn((...args) => ({ selectedGradeStatus: args })), gradeStatus: jest.fn((...args) => ({ selectedGradeStatus: args })),
}, },
})); }));
jest.mock('./RadioCriterion', () => jest.fn((props) => MockRadioCriterion(props)));
jest.mock('./ReviewCriterion', () => jest.fn((props) => MockReviewCriterion(props)));
jest.mock('./CriterionFeedback', () => jest.fn((props) => MockCriterionFeedback(props)));
jest.mock('components/InfoPopover', () => jest.fn((props) => MockInfoPopover(props)));
describe('Criterion Container', () => { describe('Criterion Container', () => {
const props = { const props = {
isGrading: true, isGrading: true,
@@ -51,63 +92,43 @@ describe('Criterion Container', () => {
}, },
gradeStatus: gradeStatuses.ungraded, gradeStatus: gradeStatuses.ungraded,
}; };
let el;
beforeEach(() => {
el = shallow(<CriterionContainer {...props} />);
});
describe('snapshot', () => { describe('component rendering', () => {
test('is ungraded and is grading', () => { it('displays the criterion prompt', () => {
expect(el).toMatchSnapshot(); render(<CriterionContainer {...props} />);
expect(screen.getByText('prompt')).toBeInTheDocument();
}); });
test('is ungraded and is not grading', () => { it('displays all option explanations in the info popover', () => {
el.setProps({ render(<CriterionContainer {...props} />);
isGrading: false, const infoPopover = screen.getByTestId('info-popover');
}); expect(infoPopover).toHaveTextContent('explanation');
expect(el).toMatchSnapshot(); expect(infoPopover).toHaveTextContent('explanation 2');
expect(infoPopover).toHaveTextContent('this label');
expect(infoPopover).toHaveTextContent('this label 2');
}); });
test('is graded and is not grading', () => { it('renders RadioCriterion when is ungraded and is grading', () => {
el.setProps({ render(<CriterionContainer {...props} />);
isGrading: false, expect(screen.getByTestId('radio-criterion-component')).toBeInTheDocument();
gradeStatus: gradeStatuses.graded, expect(screen.queryByTestId('review-criterion-component')).not.toBeInTheDocument();
});
expect(el).toMatchSnapshot();
});
});
describe('component', () => {
test('rendering and all of the option show up', () => {
expect(el.isEmptyRender()).toEqual(false);
const optionsEl = el.find('.help-popover-option');
expect(optionsEl.length).toEqual(props.config.options.length);
optionsEl.forEach((optionEl, i) => {
expect(optionEl.key()).toEqual(props.config.options[i].name);
expect(optionEl.text()).toContain(props.config.options[i].explanation);
});
}); });
test('is ungraded and is grading (Radio criterion get render)', () => { it('renders ReviewCriterion when is ungraded and is not grading', () => {
const rubricCriteria = el.find('.rubric-criteria'); render(<CriterionContainer {...props} isGrading={false} />);
expect(rubricCriteria.children(0).name()).toEqual('RadioCriterion'); expect(screen.getByTestId('review-criterion-component')).toBeInTheDocument();
expect(screen.queryByTestId('radio-criterion-component')).not.toBeInTheDocument();
}); });
test('is ungraded and is not grading (Review criterion get render)', () => { it('renders RadioCriterion when is graded and is not grading', () => {
el.setProps({ render(<CriterionContainer {...props} isGrading={false} gradeStatus={gradeStatuses.graded} />);
isGrading: false, expect(screen.getByTestId('radio-criterion-component')).toBeInTheDocument();
}); expect(screen.queryByTestId('review-criterion-component')).not.toBeInTheDocument();
const rubricCriteria = el.find('.rubric-criteria');
expect(rubricCriteria.children(0).name()).toEqual('ReviewCriterion');
}); });
test('is graded and is not grading (Radio criterion get render)', () => { it('renders CriterionFeedback component', () => {
el.setProps({ render(<CriterionContainer {...props} />);
isGrading: false, expect(screen.getByTestId('criterion-feedback-component')).toBeInTheDocument();
gradeStatus: gradeStatuses.graded,
});
const rubricCriteria = el.find('.rubric-criteria');
expect(rubricCriteria.children(0).name()).toEqual('RadioCriterion');
}); });
}); });
@@ -115,16 +136,18 @@ describe('Criterion Container', () => {
const testState = { arbitraryState: 'some data' }; const testState = { arbitraryState: 'some data' };
const ownProps = { orderNum: props.orderNum }; const ownProps = { orderNum: props.orderNum };
let mapped; let mapped;
beforeEach(() => { beforeEach(() => {
mapped = mapStateToProps(testState, ownProps); mapped = mapStateToProps(testState, ownProps);
}); });
test('selectors.app.rubric.criterionConfig', () => {
it('maps rubric criterion config to props', () => {
expect(mapped.config).toEqual( expect(mapped.config).toEqual(
selectors.app.rubric.criterionConfig(testState, ownProps), selectors.app.rubric.criterionConfig(testState, ownProps),
); );
}); });
test('selectors.grading.selected.gradeStatus', () => { it('maps grading status to props', () => {
expect(mapped.gradeStatus).toEqual( expect(mapped.gradeStatus).toEqual(
selectors.grading.selected.gradeStatus(testState), selectors.grading.selected.gradeStatus(testState),
); );

View File

@@ -1,8 +1,8 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { selectors } from 'data/redux'; import { selectors } from 'data/redux';
import { DemoWarning, mapStateToProps } from '.'; import { DemoWarning, mapStateToProps } from '.';
import messages from './messages';
jest.mock('data/redux', () => ({ jest.mock('data/redux', () => ({
selectors: { selectors: {
@@ -10,24 +10,26 @@ jest.mock('data/redux', () => ({
}, },
})); }));
let el;
describe('DemoWarning component', () => { describe('DemoWarning component', () => {
describe('snapshots', () => { describe('behavior', () => {
test('does not render if disabled flag is missing', () => { it('does not render when hide prop is true', () => {
el = shallow(<DemoWarning hide />); const { container } = render(<IntlProvider locale="en"><DemoWarning hide /></IntlProvider>);
expect(el).toMatchSnapshot(); expect(container.firstChild).toBeNull();
expect(el.isEmptyRender()).toEqual(true);
}); });
test('snapshot: disabled flag is present', () => {
el = shallow(<DemoWarning hide={false} />); it('renders alert with warning message when hide prop is false', () => {
expect(el).toMatchSnapshot(); render(<IntlProvider locale="en"><DemoWarning hide={false} /></IntlProvider>);
expect(el.isEmptyRender()).toEqual(false); const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
expect(alert).toHaveClass('alert-warning');
expect(alert).toHaveTextContent(messages.demoModeMessage.defaultMessage);
expect(alert).toHaveTextContent(messages.demoModeHeading.defaultMessage);
}); });
}); });
describe('mapStateToProps', () => { describe('mapStateToProps', () => {
const testState = { some: 'test-state' }; it('maps hide prop from app.isEnabled selector', () => {
test('hide is forwarded from app.isEnabled', () => { const testState = { some: 'test-state' };
expect(mapStateToProps(testState).hide).toEqual( expect(mapStateToProps(testState).hide).toEqual(
selectors.app.isEnabled(testState), selectors.app.isEnabled(testState),
); );

View File

@@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DemoWarning component snapshots does not render if disabled flag is missing 1`] = `""`;
exports[`DemoWarning component snapshots snapshot: disabled flag is present 1`] = `
<Alert
className="mb-0 rounded-0"
variant="warning"
>
<Alert.Heading>
<FormattedMessage
defaultMessage="Demo Mode"
description="Demo mode heading"
id="ora-grading.ReviewModal.demoHeading"
/>
</Alert.Heading>
<p>
<FormattedMessage
defaultMessage="You are demoing the new ORA staff grading experience. You will be unable to submit grades until you activate the feature. This will become the default grading experience on May 9th (05/09/2022). To opt-in early, or opt-out, please contact Partner Support."
description="Demo mode message"
id="ora-grading.ReviewModal.demoMessage"
/>
</p>
</Alert>
`;

View File

@@ -3,8 +3,8 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon'; import { Alert } from '@openedx/paragon';
import { Info } from '@edx/paragon/icons'; import { Info } from '@openedx/paragon/icons';
import { selectors } from 'data/redux'; import { selectors } from 'data/redux';
import messages from './messages'; import messages from './messages';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink, Button } from '@edx/paragon'; import { Hyperlink, Button } from '@openedx/paragon';
import urls from 'data/services/lms/urls'; import urls from 'data/services/lms/urls';
import emptyStateSVG from './assets/empty-state.svg'; import emptyStateSVG from './assets/empty-state.svg';

View File

@@ -1,33 +1,38 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import { Hyperlink } from '@edx/paragon';
import urls from 'data/services/lms/urls'; import urls from 'data/services/lms/urls';
import { renderWithIntl } from '../../testUtils';
import EmptySubmission from './EmptySubmission'; import EmptySubmission from './EmptySubmission';
jest.mock('data/services/lms/urls', () => ({ jest.mock('data/services/lms/urls', () => ({
openResponse: (courseId) => `openResponseUrl(${courseId})`, openResponse: (courseId) => `openResponseUrl(${courseId})`,
})); }));
jest.mock('./assets/emptyState.svg', () => './assets/emptyState.svg'); jest.mock('./assets/empty-state.svg', () => './assets/empty-state.svg');
let el;
describe('EmptySubmission component', () => { describe('EmptySubmission component', () => {
describe('component', () => { const props = { courseId: 'test-course-id' };
const props = { courseId: 'test-course-id' };
beforeEach(() => { it('renders the empty state image with correct alt text', () => {
el = shallow(<EmptySubmission {...props} />); renderWithIntl(<EmptySubmission {...props} />);
}); expect(screen.getByAltText('empty state')).toBeInTheDocument();
test('snapshot', () => { });
expect(el).toMatchSnapshot();
}); it('renders the no results found title message', () => {
test('openResponse destination', () => { renderWithIntl(<EmptySubmission {...props} />);
expect( expect(screen.getByText('Nothing here yet')).toBeInTheDocument();
el.find(Hyperlink).at(0).props().destination, });
).toEqual(urls.openResponse(props.courseId));
}); it('renders hyperlink with correct destination URL', () => {
renderWithIntl(<EmptySubmission {...props} />);
const hyperlink = screen.getByRole('link');
expect(hyperlink).toHaveAttribute(
'href',
urls.openResponse(props.courseId),
);
});
it('renders the back to responses button', () => {
renderWithIntl(<EmptySubmission {...props} />);
expect(screen.getByText('Back to all open responses')).toBeInTheDocument();
}); });
}); });

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, DataTableContext } from '@edx/paragon'; import { Button, DataTableContext } from '@openedx/paragon';
import * as module from './FilterStatusComponent'; import * as module from './FilterStatusComponent';

View File

@@ -1,54 +1,18 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import PropTypes from 'prop-types';
import { render } from '@testing-library/react';
import { DataTableContext } from '@openedx/paragon';
import * as module from './FilterStatusComponent'; import * as module from './FilterStatusComponent';
const fieldIds = [ const fieldIds = ['field-id-0', 'field-id-1', 'field-id-2', 'field-id-3'];
'field-id-0',
'field-id-1',
'field-id-2',
'field-id-3',
];
const filterOrder = [1, 0, 3, 2]; const filterOrder = [1, 0, 3, 2];
const filters = filterOrder.map(v => ({ id: fieldIds[v] })); const filters = filterOrder.map((v) => ({ id: fieldIds[v] }));
const headers = [0, 1, 2, 3].map(v => ({ const headers = [0, 1, 2, 3].map((v) => ({
id: fieldIds[v], id: fieldIds[v],
Header: `HeaDer-${v}`, Header: `HeaDer-${v}`,
})); }));
describe('FilterStatusComponent hooks', () => {
const context = { headers, state: { filters } };
const mockTableContext = (newContext) => {
React.useContext.mockReturnValueOnce(newContext);
};
beforeEach(() => {
context.setAllFilters = jest.fn();
});
it('returns empty dict if setAllFilters or state.filters is falsey', () => {
mockTableContext({ ...context, setAllFilters: null });
expect(module.filterHooks()).toEqual({});
mockTableContext({ ...context, state: { filters: null } });
expect(module.filterHooks()).toEqual({});
});
describe('clearFilters', () => {
it('uses React.useCallback to clear filters, only once', () => {
mockTableContext(context);
const { cb, prereqs } = module.filterHooks().clearFilters.useCallback;
expect(prereqs).toEqual([context.setAllFilters]);
expect(context.setAllFilters).not.toHaveBeenCalled();
cb();
expect(context.setAllFilters).toHaveBeenCalledWith([]);
});
});
describe('filterNames', () => {
it('returns list of Header values by filter order', () => {
mockTableContext(context);
expect(module.filterHooks().filterNames).toEqual(
filterOrder.map(v => headers[v].Header),
);
});
});
});
describe('FilterStatusComponent component', () => { describe('FilterStatusComponent component', () => {
const props = { const props = {
className: 'css-class-name', className: 'css-class-name',
@@ -58,34 +22,98 @@ describe('FilterStatusComponent component', () => {
buttonClassName: 'css-class-name-for-button', buttonClassName: 'css-class-name-for-button',
showFilteredFields: true, showFilteredFields: true,
}; };
const hookProps = {
clearFilters: jest.fn().mockName('hookProps.clearFilters'),
filterNames: ['filter-name-0', 'filter-name-1'],
};
const { FilterStatusComponent } = module; const { FilterStatusComponent } = module;
const mockHooks = (value) => {
jest.spyOn(module, 'filterHooks').mockReturnValueOnce(value); const renderWithContext = (contextValue, componentProps = props) => {
const TestWrapper = ({ children }) => (
<DataTableContext.Provider value={contextValue}>
{children}
</DataTableContext.Provider>
);
TestWrapper.propTypes = {
children: PropTypes.node,
};
return render(
<TestWrapper>
<FilterStatusComponent {...componentProps} />
</TestWrapper>,
);
}; };
describe('snapshot', () => {
describe('with filters', () => { beforeEach(() => {
test('showFilteredFields', () => { jest.clearAllMocks();
mockHooks(hookProps); });
const el = shallow(<FilterStatusComponent {...props} />);
expect(el).toMatchSnapshot(); describe('behavior', () => {
}); it('does not render when there are no filters', () => {
test('showFilteredFields=false - hide filterTexts', () => { const contextValue = {
mockHooks(hookProps); headers,
const el = shallow( state: { filters: null },
<FilterStatusComponent {...props} showFilteredFields={false} />, setAllFilters: jest.fn(),
); };
expect(el).toMatchSnapshot(); const { container } = renderWithContext(contextValue);
expect(container.firstChild).toBeNull();
});
it('does not render when setAllFilters is not available', () => {
const contextValue = { headers, state: { filters }, setAllFilters: null };
const { container } = renderWithContext(contextValue);
expect(container.firstChild).toBeNull();
});
it('renders clear filters button with correct text when filters exist', () => {
const contextValue = {
headers,
state: { filters },
setAllFilters: jest.fn(),
};
const { getByText } = renderWithContext(contextValue);
expect(getByText(props.clearFiltersText)).toBeInTheDocument();
});
it('displays filtered field names when showFilteredFields is true', () => {
const contextValue = {
headers,
state: { filters },
setAllFilters: jest.fn(),
};
const { getByText } = renderWithContext(contextValue);
const expectedFilterNames = filterOrder.map((v) => headers[v].Header);
expectedFilterNames.forEach((name) => {
expect(getByText(name, { exact: false })).toBeInTheDocument();
}); });
}); });
test('without filters', () => {
mockHooks({}); it('does not display filtered field names when showFilteredFields is false', () => {
const el = shallow(<FilterStatusComponent {...props} />); const contextValue = {
expect(el).toMatchSnapshot(); headers,
expect(el.isEmptyRender()).toEqual(true); state: { filters },
setAllFilters: jest.fn(),
};
const { queryByText } = renderWithContext(contextValue, {
...props,
showFilteredFields: false,
});
expect(queryByText(/Filtered by/)).not.toBeInTheDocument();
});
it('applies correct CSS classes to the component', () => {
const contextValue = {
headers,
state: { filters },
setAllFilters: jest.fn(),
};
const { container } = renderWithContext(contextValue);
expect(container.firstChild).toHaveClass(props.className);
});
it('calls setAllFilters with empty array when clear button is clicked', () => {
const setAllFilters = jest.fn();
const contextValue = { headers, state: { filters }, setAllFilters };
const { getByText } = renderWithContext(contextValue);
const clearButton = getByText(props.clearFiltersText);
clearButton.click();
expect(setAllFilters).toHaveBeenCalledWith([]);
}); });
}); });
}); });

View File

@@ -6,8 +6,8 @@ import {
Alert, Alert,
Button, Button,
Hyperlink, Hyperlink,
} from '@edx/paragon'; } from '@openedx/paragon';
import { Info } from '@edx/paragon/icons'; import { Info } from '@openedx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';
import urls from 'data/services/lms/urls'; import urls from 'data/services/lms/urls';

View File

@@ -1,19 +1,14 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import userEvent from '@testing-library/user-event';
import { selectors, thunkActions } from 'data/redux'; import { selectors, thunkActions } from 'data/redux';
import { renderWithIntl } from '../../testUtils';
import { formatMessage } from 'testUtils'; import { ListError, mapDispatchToProps, mapStateToProps } from './ListError';
import { import messages from './messages';
ListError,
mapDispatchToProps,
mapStateToProps,
} from './ListError';
jest.mock('data/redux', () => ({ jest.mock('data/redux', () => ({
selectors: { selectors: {
app: { app: {
courseId: (...args) => ({ courseId: args }), courseId: jest.fn((state) => state.courseId || 'test-course-id'),
}, },
}, },
thunkActions: { thunkActions: {
@@ -27,41 +22,60 @@ jest.mock('data/services/lms/urls', () => ({
openResponse: (courseId) => `api/openResponse/${courseId}`, openResponse: (courseId) => `api/openResponse/${courseId}`,
})); }));
let el;
jest.useFakeTimers('modern');
describe('ListError component', () => { describe('ListError component', () => {
describe('component', () => { const props = {
const props = { courseId: 'test-course-id',
courseId: 'test-course-id', initializeApp: jest.fn(),
}; };
beforeEach(() => {
props.loadSelectionForReview = jest.fn(); beforeEach(() => {
props.intl = { formatMessage }; jest.clearAllMocks();
props.initializeApp = jest.fn(); });
describe('behavior', () => {
it('renders error alert with proper styling', () => {
renderWithIntl(<ListError {...props} />);
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
expect(alert).toHaveClass('alert-danger');
}); });
describe('render tests', () => {
beforeEach(() => { it('displays error heading and message', () => {
el = shallow(<ListError {...props} />); renderWithIntl(<ListError {...props} />);
}); const heading = screen.getByRole('alert').querySelector('.alert-heading');
test('snapshot', () => { expect(heading).toBeInTheDocument();
expect(el).toMatchSnapshot(); expect(heading).toHaveTextContent(messages.loadErrorHeading.defaultMessage);
}); });
it('displays try again button', () => {
renderWithIntl(<ListError {...props} />);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveClass('btn-primary');
});
it('calls initializeApp when try again button is clicked', async () => {
renderWithIntl(<ListError {...props} />);
const user = userEvent.setup();
const button = screen.getByRole('button');
await user.click(button);
expect(props.initializeApp).toHaveBeenCalledTimes(1);
}); });
}); });
describe('mapStateToProps', () => { describe('mapStateToProps', () => {
let mapped;
const testState = { some: 'test-state' }; const testState = { some: 'test-state' };
beforeEach(() => { it('maps courseId from app.courseId selector', () => {
mapped = mapStateToProps(testState); const mapped = mapStateToProps(testState);
});
test('courseId loads from app.courseId', () => {
expect(mapped.courseId).toEqual(selectors.app.courseId(testState)); expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
}); });
}); });
describe('mapDispatchToProps', () => { describe('mapDispatchToProps', () => {
it('loads initializeApp from thunkActions.app.initialize', () => { it('maps initializeApp from thunkActions.app.initialize', () => {
expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize); expect(mapDispatchToProps.initializeApp).toEqual(
thunkActions.app.initialize,
);
}); });
}); });
}); });

View File

@@ -1,12 +1,10 @@
@import "@edx/paragon/scss/core/core";
span.pgn__icon.breadcrumb-arrow { span.pgn__icon.breadcrumb-arrow {
width: 16px !important; width: 16px !important;
height: 16px !important; height: 16px !important;
}; };
.empty-submission { .empty-submission {
width: map-get($container-max-widths, "sm"); width: var(--pgn-size-container-max-width-sm);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@@ -15,7 +13,7 @@ span.pgn__icon.breadcrumb-arrow {
margin: auto; margin: auto;
> img { > img {
padding: map-get($spacers, 5); padding: var(--pgn-spacing-spacer-5);
} }
} }
@@ -25,4 +23,14 @@ span.pgn__icon.breadcrumb-arrow {
margin-bottom: 0; margin-bottom: 0;
} }
} }
@media (--pgn-size-breakpoint-max-width-xs) {
.badge {
white-space: normal;
}
.pgn__table-actions > div:first-of-type {
z-index: var(--pgn-elevation-modal-zindex) !important;
}
}
} }

View File

@@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ArrowBack, Launch } from '@edx/paragon/icons'; import { ArrowBack, Launch } from '@openedx/paragon/icons';
import { Hyperlink, Icon } from '@edx/paragon'; import { Hyperlink, Icon } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { selectors } from 'data/redux'; import { selectors } from 'data/redux';

View File

@@ -1,12 +1,6 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import { Hyperlink } from '@edx/paragon';
import * as constants from 'data/constants/app';
import urls from 'data/services/lms/urls';
import { selectors } from 'data/redux'; import { selectors } from 'data/redux';
import { renderWithIntl } from '../../testUtils';
import { import {
ListViewBreadcrumb, ListViewBreadcrumb,
mapStateToProps, mapStateToProps,
@@ -15,9 +9,9 @@ import {
jest.mock('data/redux', () => ({ jest.mock('data/redux', () => ({
selectors: { selectors: {
app: { app: {
courseId: (...args) => ({ courseId: args }), courseId: jest.fn((state) => state.courseId || 'test-course-id'),
ora: { ora: {
name: (...args) => ({ oraName: args }), name: jest.fn((state) => state.oraName || 'test-ora-name'),
}, },
}, },
}, },
@@ -28,41 +22,60 @@ jest.mock('data/services/lms/urls', () => ({
ora: (courseId, locationId) => `oraUrl(${courseId}, ${locationId})`, ora: (courseId, locationId) => `oraUrl(${courseId}, ${locationId})`,
})); }));
let el; jest.mock('data/constants/app', () => ({
locationId: () => 'test-location-id',
}));
describe('ListViewBreadcrumb component', () => { describe('ListViewBreadcrumb component', () => {
describe('component', () => { const props = {
const props = { courseId: 'test-course-id',
courseId: 'test-course-id', oraName: 'fake-ora-name',
oraName: 'fake-ora-name', };
};
beforeEach(() => { beforeEach(() => {
el = shallow(<ListViewBreadcrumb {...props} />); jest.clearAllMocks();
});
describe('behavior', () => {
it('renders back to responses link with correct destination', () => {
renderWithIntl(<ListViewBreadcrumb {...props} />);
const backLink = screen.getAllByRole('link').find(
link => link.getAttribute('href') === `openResponseUrl(${props.courseId})`,
);
expect(backLink).toBeInTheDocument();
}); });
test('snapshot: empty (no list data)', () => {
expect(el).toMatchSnapshot(); it('displays ORA name in heading', () => {
renderWithIntl(<ListViewBreadcrumb {...props} />);
const heading = screen.getByText(props.oraName);
expect(heading).toBeInTheDocument();
expect(heading).toHaveClass('h3');
}); });
test('openResponse destination', () => {
expect( it('renders ORA link with correct destination', () => {
el.find(Hyperlink).at(0).props().destination, renderWithIntl(<ListViewBreadcrumb {...props} />);
).toEqual(urls.openResponse(props.courseId)); const oraLink = screen.getAllByRole('link').find(
link => link.getAttribute('href') === `oraUrl(${props.courseId}, test-location-id)`,
);
expect(oraLink).toBeInTheDocument();
}); });
test('ora destination', () => {
expect( it('displays back to responses text', () => {
el.find(Hyperlink).at(1).props().destination, renderWithIntl(<ListViewBreadcrumb {...props} />);
).toEqual(urls.ora(props.courseId, constants.locationId())); expect(screen.getByText('Back to all open responses')).toBeInTheDocument();
}); });
}); });
describe('mapStateToProps', () => { describe('mapStateToProps', () => {
let mapped;
const testState = { some: 'test-state' }; const testState = { some: 'test-state' };
beforeEach(() => {
mapped = mapStateToProps(testState); it('maps courseId from app.courseId selector', () => {
}); const mapped = mapStateToProps(testState);
test('courseId loads from app.courseId', () => {
expect(mapped.courseId).toEqual(selectors.app.courseId(testState)); expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
}); });
test('oraName loads from app.ora.name', () => {
it('maps oraName from app.ora.name selector', () => {
const mapped = mapStateToProps(testState);
expect(mapped.oraName).toEqual(selectors.app.ora.name(testState)); expect(mapped.oraName).toEqual(selectors.app.ora.name(testState));
}); });
}); });

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button } from '@edx/paragon'; import { Button } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';

View File

@@ -1,20 +1,33 @@
import React from 'react'; import { screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import { renderWithIntl } from '../../testUtils';
import { SelectedBulkAction } from './SelectedBulkAction'; import { SelectedBulkAction } from './SelectedBulkAction';
describe('SelectedBulkAction component', () => { describe('SelectedBulkAction component', () => {
const props = { const props = {
selectedFlatRows: [{ id: 1 }, { id: 2 }], selectedFlatRows: [{ id: 1 }, { id: 2 }],
handleClick: jest.fn(), handleClick: jest.fn(() => () => {}),
}; };
test('snapshots', () => {
const el = shallow(<SelectedBulkAction {...props} handleClick={() => jest.fn()} />); beforeEach(() => {
expect(el).toMatchSnapshot(); jest.clearAllMocks();
}); });
test('handleClick', () => { it('renders button with correct text and selected count', () => {
shallow(<SelectedBulkAction {...props} />); renderWithIntl(<SelectedBulkAction {...props} />);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent(`View selected responses (${props.selectedFlatRows.length})`);
});
it('applies correct CSS class to button', () => {
renderWithIntl(<SelectedBulkAction {...props} />);
const button = screen.getByRole('button');
expect(button).toHaveClass('view-selected-responses-btn');
expect(button).toHaveClass('btn-primary');
});
it('calls handleClick with selectedFlatRows on render', () => {
renderWithIntl(<SelectedBulkAction {...props} />);
expect(props.handleClick).toHaveBeenCalledWith(props.selectedFlatRows); expect(props.handleClick).toHaveBeenCalledWith(props.selectedFlatRows);
}); });
}); });

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