Compare commits

..

145 Commits

Author SHA1 Message Date
Muhammad Abdullah Waheed
1b0a5c9abb fix: downgrading frontend-build for testing sharp version 2024-02-01 17:28:50 +05:00
Bilal Qamar
b7e4419c3a refactor: updated setupTest to resolve failing tests 2024-01-30 14:59:52 +05:00
Bilal Qamar
fca889e12c Merge branch 'master' of github.com:openedx/frontend-app-ora-grading into bilalqamar95/frontend-platform-upgrade 2024-01-30 14:59:28 +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
Bilal Qamar
edf6529704 refactor: updated frontend-platform & paragon version along with header, footer 2024-01-24 16:45:46 +05:00
Bilal Qamar
658fb7dfe3 Merge branch 'master' into bilalqamar95/frontend-platform-upgrade 2024-01-01 16:25:47 +05:00
Jenkins
3ef24a626b chore(i18n): update translations 2023-12-17 10:46:17 -05:00
Bilal Qamar
e7ec4f4543 refactor: updated package-lock 2023-12-01 15:30:46 +05:00
Bilal Qamar
5ec968653a refactor: bumped edx packages 2023-11-30 15:04:33 +05:00
Bilal Qamar
e09d40d8b2 Merge branch 'master' of github.com:edx/frontend-app-ora-grading into bilalqamar95/frontend-platform-upgrade 2023-11-30 14:03:21 +05:00
renovate[bot]
d810913038 fix(deps): update dependency @edx/brand to v1.2.3 2023-10-29 15:44:21 -04:00
Bilal Qamar
7572e738c9 feat: bumped frontend-platform to v6 2023-10-25 15:21:28 +05: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
Leangseu Kim
c644da3dcc fix: decodeURIComponent for locationId
Squashed commit of the following:

commit af4d469ef241a504112dceda2cb94bff24ba4a9c
Author: Vu Nguyen <vu.nguyen@dmttek.com>
Date:   Wed Oct 4 11:27:58 2023 +0700

    update test cases

commit d0c28fb99bede979fbcc8f57036ecb8027996772
Merge: 1f4d50b 5492520
Author: vunguyen-dmt <34919039+vunguyen-dmt@users.noreply.github.com>
Date:   Wed Oct 4 11:22:24 2023 +0700

    Merge branch 'openedx:master' into fix_location_id_error

commit 1f4d50b36514a6e4e9e575b938bf93b81317a21c
Merge: a280a35 eb73457
Author: Vu Nguyen <vu.nguyen@dmttek.com>
Date:   Fri Jul 21 03:45:05 2023 +0700

    Merge branch 'fix_location_id_error' of https://github.com/vunguyen-dmt/frontend-app-ora-grading into fix_location_id_error

commit a280a350fcf595dd2011018d113467a9cd88cd61
Author: Vu Nguyen <vu.nguyen@dmttek.com>
Date:   Fri Jul 21 03:44:28 2023 +0700

    fix: decodeURIComponent for locationId

commit eb734574bd0daae0c2523904763a8d755baa31f3
Author: Vu Nguyen <vu.nguyen@dmttek.com>
Date:   Fri Jul 21 03:41:28 2023 +0700

    update src/data/constants/app.test.js

commit 513dd324a5adc7312ac92c30164e0120784acd4b
Merge: e899846 78ada8c
Author: vunguyen-dmt <34919039+vunguyen-dmt@users.noreply.github.com>
Date:   Wed Jul 19 16:12:14 2023 +0700

    Merge branch 'master' into fix_location_id_error

commit e8998461de8ba9cbbe00a1726ad6266bde5d5ef5
Author: Vu Nguyen <vu.nguyen@dmttek.com>
Date:   Thu Jun 8 10:29:45 2023 +0700

    fix: decodeURIComponent for locationId
2023-10-10 11:30:26 -04:00
Illia Shestakov
cc11ce0f81 fix(deps): replace edx.org brand dependency with openedx brand (#199) 2023-10-10 10:48:33 -04:00
Jenkins
549252038f chore(i18n): update translations 2023-10-01 11:51:20 -04:00
Jenkins
192c8b4601 chore(i18n): update translations 2023-09-24 11:51:22 -04:00
Jhon Vente
70e13eccfa [DOCS] Readme updated according OEP-55 (#194)
* docs: README updated

* docs: update some badges and content

* fix: getting help url

---------

Co-authored-by: Edward Zarecor <ezarecor@tcril.org>
Co-authored-by: Matthew Carter <mcarter@edx.org>
2023-09-12 13:01:10 -04:00
Emad Rad
e25a5a9549 Persian language (#246)
* fix: corrected typos

explaination -> explanation
Critera -> Criteria
addtional -> additional
arbitary -> arbitrary
penging -> pending
downladFiles -> downloadFiles
isLoadeed -> isLoaded
selectror -> selector
commnents -> comments
stirng -> string
isGragrding -> isGrading
queu -> queue
seleted -> selected
feecback -> feedback

* feat: Persian language (fa_IR) added

* chore: Persian translations added

* chore: sort language codes alphabetically

* chore: camelCase variable used for translated messages

* chore: deprecated styling updated

---------

Co-authored-by: Leangseu Kim <lkim@edx.org>
2023-09-11 09:15:52 -04:00
renovate[bot]
358263de3c fix(deps): update dependency regenerator-runtime to ^0.14.0 2023-09-05 11:35:20 -04:00
renovate[bot]
dc7fc94ab5 fix(deps): update dependency core-js to v3.32.1 2023-09-05 10:29:35 -04:00
renovate[bot]
b643afd1b8 fix(deps): update dependency @edx/frontend-component-header to v4.6.0 2023-09-05 05:30:04 -04:00
renovate[bot]
5ac1868d30 fix(deps): update dependency @edx/frontend-component-footer to v12.2.1 2023-09-05 03:35:52 -04:00
renovate[bot]
65063df731 chore(deps): update dependency node to 18.17 2023-09-05 00:55:32 -04:00
renovate[bot]
9aee97dccb fix(deps): update dependency whatwg-fetch to v3.6.18 2023-09-04 20:24:14 -04:00
renovate[bot]
be1ce502c8 fix(deps): update dependency @zip.js/zip.js to v2.7.28 2023-09-04 18:32:59 -04:00
renovate[bot]
19e2f35522 fix(deps): update dependency @edx/frontend-platform to v4.6.1 2023-09-04 14:25:59 -04:00
renovate[bot]
d24ab3358b chore(deps): update dependency jest to v29.6.4 2023-09-04 12:18:38 -04:00
renovate[bot]
56803fb874 chore(deps): update dependency @edx/frontend-build to v12.9.10 2023-09-04 09:43:58 -04:00
Bilal Qamar
b7b94531aa feat: update react & react-dom to v17 (#190)
* feat: update react & react-dom to v17

* build: update pkgs

* fix: fix lint issue

* build: update paragon

* refactor: updated edx packages

* build: update -react-redux

* refactor: updated package-lock

* refactor: updated package-lock

* refactor: updated package-lock

---------

Co-authored-by: mashal-m <mashal.malik@arbisoft.com>
Co-authored-by: Mashal Malik <107556986+Mashal-m@users.noreply.github.com>
2023-08-28 10:36:11 -04:00
leangseu-edx
78ada8ce34 chore: disable cycle dependency error for eslint (#227)
* chore: disable cycle dependency error for eslint

* chore: remove done from async function

* chore: downgrade @testing-library/react to 12 for react@16 support

* fix: update integration test selection and waiting

---------

Co-authored-by: Ben Warzeski <bwarzesk@gmail.com>
2023-07-17 11:17:58 -04:00
renovate[bot]
194e61380c fix(deps): update dependency @testing-library/user-event to v14 2023-07-16 09:58:12 -04:00
renovate[bot]
fa36e20de9 chore(deps): update dependency jest to v29 2023-07-16 05:34:46 -04:00
renovate[bot]
61cf386ee6 chore(deps): update dependency @testing-library/react to v14 2023-07-16 03:27:30 -04:00
renovate[bot]
445cd15d9a fix(deps): update react-router monorepo to v5.3.4 2023-07-16 00:32:05 -04:00
renovate[bot]
db1cf48257 fix(deps): update dependency redux-thunk to v2.4.2 2023-07-15 20:19:32 -04:00
renovate[bot]
942d471097 fix(deps): update dependency redux to v4.2.1 2023-07-15 17:34:16 -04:00
renovate[bot]
1a769a4e70 fix(deps): update dependency query-string to v7.1.3 2023-07-15 15:15:53 -04:00
renovate[bot]
b5cb2af513 fix(deps): update dependency prop-types to v15.8.1 2023-07-15 11:46:48 -04:00
renovate[bot]
6666c0df83 fix(deps): update dependency history to v5.3.0 2023-07-15 09:28:25 -04:00
renovate[bot]
fa60d7d234 fix(deps): update dependency dompurify to v2.4.7 2023-07-15 05:53:57 -04:00
renovate[bot]
78cce21f10 fix(deps): update dependency core-js to v3.31.1 2023-07-15 03:31:40 -04:00
renovate[bot]
e21c2a63e7 fix(deps): update dependency axios to ^0.27.0 2023-07-14 23:40:41 -04:00
renovate[bot]
514792786d fix(deps): update dependency @zip.js/zip.js to v2.7.20 2023-07-14 21:59:46 -04:00
renovate[bot]
5ef2f1ba4f fix(deps): update dependency @fortawesome/react-fontawesome to ^0.2.0 2023-07-14 17:25:11 -04:00
renovate[bot]
b986849c85 fix(deps): update dependency @edx/frontend-platform to v4.6.0 2023-07-14 15:16:53 -04:00
renovate[bot]
f8565c30d1 fix(deps): update dependency @edx/paragon to v20.45.1 2023-07-14 11:52:35 -04:00
renovate[bot]
3a7e103317 chore(deps): update dependency @edx/frontend-build to v12.8.65 2023-07-14 10:29:33 -04:00
renovate[bot]
f977e14ea6 fix(deps): update dependency @edx/frontend-component-header to v4.4.3 2023-07-14 05:47:28 -04:00
renovate[bot]
ab4f1864f2 fix(deps): update dependency @edx/frontend-component-header to v4.4.2 2023-07-14 03:10:45 -04:00
renovate[bot]
6e4d4c479c fix(deps): update dependency @edx/frontend-component-footer to v12.1.2 2023-07-14 00:33:53 -04:00
renovate[bot]
d74532f988 fix(deps): update dependency @edx/brand to v2.1.2 2023-07-13 21:26:17 -04:00
renovate[bot]
d577bc79f7 chore(deps): update dependency @edx/frontend-build to v12.8.64 2023-07-13 19:25:50 -04:00
renovate[bot]
43ab328545 chore(deps): update dependency node to 18.16 2023-07-13 15:48:22 -04:00
renovate[bot]
3ddfdf34d0 chore(deps): update dependency jest-expect-message to v1.1.3 2023-07-13 13:36:24 -04:00
renovate[bot]
b751d41caf chore(deps): update dependency jest to v27.5.1 2023-07-13 08:05:51 -04:00
renovate[bot]
4f4b28e6f5 chore(deps): update dependency @edx/reactifex to v2.2.0 2023-07-13 05:44:51 -04:00
renovate[bot]
038bd117e1 chore(deps): update dependency @edx/frontend-build to v12.8.63 2023-07-13 03:16:03 -04:00
renovate[bot]
7bb31a9aa0 fix(deps): update dependency util to v0.12.5 2023-07-13 00:03:44 -04:00
renovate[bot]
b2f59fc3a1 chore(deps): update dependency semantic-release to v19.0.5 2023-07-12 20:29:24 -04:00
renovate[bot]
60b63944bd chore(deps): update dependency enzyme-adapter-react-16 to v1.15.7 2023-07-12 18:50:22 -04:00
leangseu-edx
2893a9e698 chore: make renovate run weekly instead of daily 2023-07-12 14:57:38 -04:00
renovate[bot]
de4d0fb7f2 chore(deps): update dependency @testing-library/jest-dom to v5.16.5 2023-07-12 14:39:10 -04:00
leangseu-edx
ca7254c3b0 chore: update schedule to widen the range renovate to update 2023-07-12 14:37:33 -04:00
Leangseu Kim
6095869271 chore: change renovate to daily to for testing 2023-07-11 12:07:11 -04:00
leangseu-edx
8fe67f918f chore: remove depends on from catalog-info.yaml 2023-07-05 09:22:17 -04:00
leangseu-edx
b28e58e7cd chore: update links for catalog-info.yaml 2023-07-05 09:16:31 -04:00
leangseu-edx
a1436c3266 chore: update header and footer package (#195)
* chore: update header and footer package

* chore: unit test
2023-06-30 13:00:29 -04:00
Moisés Arévalo
9b5e85a236 chore: updated security contact email 2023-06-29 10:08:02 -04:00
Leangseu Kim
fe1388666a feat: add renovate for js dependency update 2023-06-28 11:31:54 -04:00
Leangseu Kim
a07d6f9b80 fix: when there is no options, the validation should be valid 2023-06-07 11:04:41 -04:00
Adolfo R. Brandes
3e685be116 Merge pull request #183 from arbrandes/runtime-config 2023-05-31 16:42:09 +01:00
Adolfo R. Brandes
0bb5f50917 refactor: use getConfig 2023-05-31 12:32:09 -03:00
Adolfo R. Brandes
49357a4e87 feat: Support runtime configuration
frontend-platform supports runtime configuration since 2.5.0 (see the PR
that introduced it[1], but it requires MFE cooperation.  This implements
just that: by avoiding making configuration values constant, it should
now be possible to change them after initialization.

Almost all changes here relate to the `LMS_BASE_URL` setting, which in
most places was treated as a constant.

[1] openedx/frontend-platform#335
2023-05-25 11:28:28 -03:00
Leangseu Kim
33ba1cdd08 feat: upgraded to node v18, added .nvmrc and updated workflows 2023-05-22 11:20:32 -04:00
Nathan Sprenkle
7012fa82c9 docs: fix bad readme styling (#182) 2023-05-15 11:08:48 -04:00
Adolfo R. Brandes
7b418ff6e3 Merge pull request #167 from raccoongang/fix-location-id 2023-05-15 10:37:00 -03:00
Eugene Dyudyunov
cc349faeb2 fix: BadOraLocationResponse error
Refactor the locationId constant for the subdirectory-based
deployments support.

Exclude the MFE's `PUBLIC_PATH` from the constant.

The `window.location.pathname` example:
```
<PUBLIC_PATH>block-v1:oragrading+oragrading+oragrading+type@openassessment+block@ee217e897a954c1faa3b29317da0f2e7
```
Where the `PUBLIC_PATH` could be:
- `'/'` - for subdomain-based deployments (default)
- `'/mfe-specifix-public-path/'` - for subdirectory-based deployments
2023-05-12 15:33:01 +03:00
Nathan Sprenkle
455ca15af9 docs: remove old readme.md (#180) 2023-05-11 13:36:32 -04:00
Nathan Sprenkle
f992331bf4 docs: maintainership prep (#178)
* docs: add codeowners file

* docs: add catalog-info.yaml

* docs: update README

* docs: add deploy information

* docs: update contributing info
2023-05-09 14:30:31 -04:00
Omar Al-Ithawi
4158231d7a feat: use atlas in make pull_translations (#179)
Changes
-------
 - Bump frontend-platform to bring `intl-imports.js` script
 - Move all i18n imports into `src/i18n/index.js` so `intl-imports.js` can
   override it with latest translations
 - Add `atlas` into `make pull_translations` when `OPENEDX_ATLAS_PULL`
   environment variable is set.
 - package.json and package-lock.json are copied from https://github.com/openedx/frontend-app-ora-grading/pull/176
 - updated snapshots and updated tests in sync with `frontend-platform`
 - require package-lock.json version 3: same as https://github.com/openedx/frontend-app-ora-grading/pull/176

Refs: [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) implementing Translation Infrastructure OEP-58.
2023-05-09 10:12:40 -04:00
Jenkins
2fa46ab00e chore(i18n): update translations 2023-04-16 11:45:43 -04:00
Jenkins
adade6e48d chore(i18n): update translations 2023-03-26 11:50:42 -04:00
Yoiber
06aea1ff68 chore(i18n): add more languages (#160) 2023-03-25 13:00:56 -04:00
Mashal Malik
054304902f refactor: remove unused tranisfex v2 url (#172) 2023-03-06 12:18:24 +05:00
Leangseu Kim
ba9bddbda1 fix: removed coveralls and codecov packages with update in ci uploader 2023-03-02 09:51:13 -05:00
Feanil Patel
706d69aeca build: Creating a missing workflow file add-remove-label-on-comment.yml.
The .github/workflows/add-remove-label-on-comment.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 10:32:02 -05:00
Feanil Patel
6d3ed03cac build: Creating a missing workflow file add-depr-ticket-to-depr-board.yml.
The .github/workflows/add-depr-ticket-to-depr-board.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 10:32:02 -05:00
Feanil Patel
21a35cde82 build: Creating a missing workflow file self-assign-issue.yml.
The .github/workflows/self-assign-issue.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-28 10:32:02 -05:00
Leangseu Kim
66f85ee17e chore: upgrade transifex push translation to v3 2023-02-23 10:54:53 -05:00
Leangseu Kim
140cfc1639 chore: upgrade transifex push translation to v3 2023-02-23 10:30:35 -05:00
Leangseu Kim
26906d45f7 fix: upgrade frontend-build to v12 2023-02-23 10:20:59 -05:00
Leangseu Kim
a753170cb7 chore!: Dropped support for Node 12 2023-02-23 09:56:25 -05:00
Jenkins
690140ce46 chore(i18n): update translations 2023-02-22 10:44:03 -05:00
Nathan Sprenkle
6764a9766c docs: add CODEOWNERS (#149) 2022-12-06 16:38:39 -05:00
Diana Olarte
c646b88543 feat: allow runtime configuration (#144)
* feat: allow runtime configuration

* test: organize Head test
2022-11-21 10:06:24 -05:00
Leangseu Kim
b1d11119db fix: update transifex flag for tx cli 1.4.0 2022-10-18 12:09:17 -04:00
edX requirements bot
35532fed92 fix: update organization references (#142) 2022-10-03 12:56:21 +05:00
Nathan Sprenkle
15952d808a Merge pull request #141 from edx/transifex-bot-update-translations2022-09-18
chore(i18n): update translations
2022-09-19 15:30:58 -04:00
Jenkins
3a928e42bc chore(i18n): update translations 2022-09-18 15:40:16 +00:00
Ben Warzeski
15e756673f fix: remove return from useEffect call (#131)
* fix: remove return from useEffect call

* fix: text renderer tests
2022-07-20 13:29:26 -04:00
Leangseu Kim
cba03d305c chore: update rubric style 2022-07-19 13:32:22 -04:00
Leangseu Kim
956dee9a6d chore: change card body to card section 2022-07-19 11:28:15 -04:00
leangseu-edx
4f7d3aeb57 leangseu edx/header footer dependency (#127)
* chore: update dependency

* fix: update dependency to match deploy package for header and footer

* chore: update linting

* chore: update datatable filter for paragon upgrade
2022-07-15 11:02:44 -04:00
leangseu-edx
d4f1383822 fix: make student response persist break line on display (#125)
* fix: make student response persist break line on display

* chore: scroll bug when selecting text
2022-07-07 11:54:47 -04:00
Nathan Sprenkle
5efd1466bf Merge pull request #121 from edx/nsprenkle/readme
docs: add a readme
2022-06-21 14:00:23 -04:00
nsprenkle
36bd27517c docs: update reamde 2022-06-17 11:14:55 -04:00
nsprenkle
6c884ce215 docs: add a basic readme 2022-06-17 11:09:55 -04:00
Leangseu Kim
8b4f554cf6 fix: use moment to handle date 2022-06-13 15:08:26 -04:00
Leangseu Kim
0b1b079abd fix: patch to name duplicate 2022-06-13 10:14:23 -04:00
Leangseu Kim
b2c52111d7 chore: update text on CTA banner 2022-05-20 12:51:46 -04:00
leangseu-edx
18bc94e2ff chore: add CTA for page (#112)
* chore: add CTA for page

* chore: update hyperlink style
2022-05-19 10:16:26 -04:00
leangseu-edx
0f41df2cf3 feat: add fetch submission files (#110)
chore: remove cache busting
2022-05-12 09:45:46 -04:00
Ben Warzeski
91fbb8978a chore: update integration tests (#109) 2022-05-11 14:14:25 -04:00
leangseu-edx
5aecd88c70 fix: loose end on hook refactor (#111)
* fix: loose end on hook refactor

* chore: update package-lock.json to npm 8
2022-05-11 13:05:01 -04:00
Jawayria
2bf499fb43 Merge pull request #107 from edx/jenkins/version-check-0a90024
feat: Add package-lock file version check
2022-05-06 15:59:20 +05:00
edx-semantic-release
c217c32196 chore(i18n): update translations 2022-05-01 11:45:23 -04:00
Ben Warzeski
5f12c4fb8e chore: renderer test coverage (#103)
* chore: renderer test coverage

* fix: lint

* chore: api tests

* chore: tests for app reducer and StartGradeButton

* chore: lint

* fix: update reducer tests

* chore: more test coverage

* chore: test coverage

* chore: update test for merge conflicts
2022-04-29 14:54:33 -04:00
178 changed files with 17075 additions and 45624 deletions

2
.env
View File

@@ -30,3 +30,5 @@ ENTERPRISE_MARKETING_URL=''
ENTERPRISE_MARKETING_UTM_SOURCE=''
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
APP_ID=''
MFE_CONFIG_API_URL=''

View File

@@ -36,3 +36,5 @@ ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
APP_ID=''
MFE_CONFIG_API_URL=''

View File

@@ -4,8 +4,14 @@ const config = createConfig('eslint', {
rules: {
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-import-module-exports': 'off',
'import/no-self-import': 'off',
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
'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
'no-import-assign': 'off',
'no-promise-executor-return': 'off',
'import/no-cycle': 'off',
},
});

6
.github/CODEOWNERS vendored Normal file
View File

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

@@ -0,0 +1,19 @@
# Run the workflow that adds new tickets that are either:
# - labelled "DEPR"
# - title starts with "[DEPR]"
# - body starts with "Proposal Date" (this is the first template field)
# to the org-wide DEPR project board
name: Add newly created DEPR issues to the DEPR project board
on:
issues:
types: [opened]
jobs:
routeissue:
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
secrets:
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}

View File

@@ -0,0 +1,20 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "label: " it tries to apply
# the label indicated in rest of comment.
# If the comment starts with "remove label: ", it tries
# to remove the indicated label.
# Note: Labels are allowed to have spaces and this script does
# not parse spaces (as often a space is legitimate), so the command
# "label: really long lots of words label" will apply the
# label "really long lots of words label"
name: Allows for the adding and removing of labels via comment
on:
issue_comment:
types: [created]
jobs:
add_remove_labels:
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master

View File

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

View File

@@ -7,4 +7,4 @@ on:
jobs:
commitlint:
uses: edx/.github/.github/workflows/commitlint.yml@master
uses: openedx/.github/.github/workflows/commitlint.yml@master

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

12
.github/workflows/self-assign-issue.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "assign me" it assigns the author to the
# ticket (case insensitive)
name: Assign comment author to ticket if they say "assign me"
on:
issue_comment:
types: [created]
jobs:
self_assign_by_comment:
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18.19

View File

@@ -3,18 +3,17 @@ npm-install-%: ## install specified % npm package
git add package.json
transifex_resource = frontend-app-ora-grading
transifex_langs = "ar,fr,es_419,zh_CN"
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
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
NPM_TESTS=build i18n_extract lint test is-es5
NPM_TESTS=build i18n_extract lint test
.PHONY: test
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
@@ -49,15 +48,30 @@ push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --languages=$(transifex_langs)
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/paragon/src/i18n/messages:paragon \
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
endif
# This target is used by CI.
validate-no-uncommitted-package-lock-changes:

233
README.rst Normal file
View File

@@ -0,0 +1,233 @@
frontend-app-ora-grading
#############################
|license-badge| |status-badge| |ci-badge| |codecov-badge|
Purpose
*******
The ORA Staff Grading App is a micro-frontend (MFE) staff grading experience
for Open Response Assessments (ORAs). This experience was designed to
streamline the grading process and enable richer previews of submission content
and, eventually, replace on-platform grading workflows for ORA.
When enabled, ORAs with a staff grading step will link to this new MFE when
clicking "Grade Available Responses" from the ORA or link in the instructor
dashboard.
The ORA Staff Grader depends on the `lms/djangoapps/ora_staff_grader
<https://github.com/openedx/edx-platform/tree/master/lms/djangoapps/ora_staff_grader>`_
app in ``edx-platform``.
Getting Started
***************
Prerequisites
=============
The `devstack`_ is currently recommended as a development environment for your
new MFE. If you start it with ``make dev.up.lms`` that should give you
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.
.. _Devstack: https://github.com/openedx/devstack
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
Developing
==========
Cloning and Startup
--------------
First, clone the repo, install code prerequisites, and start the app.
.. code-block::
1. Clone your new repo:
``git clone git@github.com:openedx/frontend-app-ora-grading.git``
2. Use node v18.x.
The current version of the micro-frontend build scripts support node 18.
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
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
3. Install npm dependencies:
``cd frontend-app-ora-grading && npm install``
4. Update the application port to use for local development:
Default port is 1993. If this does not work for you, update the line
`PORT=1993` to your port in all .env.* files
5. Start the dev server:
``npm start``
The app will, by default, run on `http://localhost:1993` unless otherwise
specified in ``.env.development:PORT`` and ``.env.development:BASE_URL``.
Next, enable the ORA Grading micro-frontend in `edx-platform`
#. Add the path to the ORA Grading app in `edx-platform`:
#. Go to your environment settings (e.g. `edx-platform/lms/envs/private.py`)
#. Add the environment variable, ``ORA_GRADING_MICROFRONTEND_URL`` pointing
to the ORA Grading app location (e.g. ``http://localhost:1993``).
#. Start / restart the ``edx-platform`` ``lms``.
#. Enable the ORA Grading feature in Django Admin.
#. Go to Django Admin (`{lms-root}/admin`)
#. Navigate to ``django-waffle`` > ``Flags`` and click ``add/enable a new
flag``.
#. Add a new flag called ``openresponseassessment.enhanced_staff_grader``
and enable it.
From there, visit an Open Response Assessment with a Staff Graded Step and
click the "View and grade responses" button to begin grading in the ORA Staff
Grader experience.
Making Changes
--------------
Get / install the latest code:
.. code-block::
# Grab the latest code
git checkout master
git pull
# Install/update the dev requirements
npm install
Before committing:
.. code-block::
# Make a new branch for your changes
git checkout -b <your_github_username>/<short_description>
# Using your favorite editor, edit the code to make your change.
# Run your new tests
npm test
# Commit all your changes
git commit ...
git push
# Open a PR and ask for review.
Deploying
=========
This component follows the standard deploy process for MFEs. For details, see
the `MFE production deployment guide`_
.. _MFE production deployment guide: https://openedx.github.io/frontend-platform/#production-deployment-strategy
Internationalization
====================
Please see refer to the `frontend-platform i18n howto`_ for documentation on
internationalization.
.. _frontend-platform i18n howto: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst
Getting Help
************
If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a `Slack
invitation`_, then join our `community Slack workspace`_. Because this is a
frontend repository, the best place to discuss it would be in the `#wg-frontend
channel`_.
For anything non-trivial, the best path is to open an issue in this repository
with as many details about the issue you are facing as you can provide.
https://github.com/openedx/frontend-app-ora-grading/issues
For more information about these options, see the `Getting Help`_ page.
.. _Slack invitation: https://openedx.org/slack
.. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/community/connect
License
*******
The code in this repository is licensed under the AGPLv3 unless otherwise
noted.
Please see `LICENSE <LICENSE>`_ for details.
Contributing
************
Contributions are very welcome. Please read `How To Contribute`_ for details.
.. _How To Contribute: https://openedx.org/r/how-to-contribute
This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure
to have a discussion about your new feature idea with the maintainers prior to
beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing
your idea.
The Open edX Code of Conduct
****************************
All community members are expected to follow the `Open edX Code of Conduct`_.
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
People
******
The assigned maintainers for this component and other project details may be
found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml``
file in this repo.
.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-app-ora-grading
Reporting Security Issues
*************************
Please do not report security issues in public, and email security@openedx.org instead.
.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-app-ora-grading.svg
:target: https://github.com/openedx/frontend-app-ora-grading/blob/master/LICENSE
:alt: License
.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
.. |ci-badge| image:: https://github.com/openedx/frontend-app-ora-grading/actions/workflows/ci.yml/badge.svg
:target: https://github.com/openedx/frontend-app-ora-grading/actions/workflows/ci.yml
:alt: Continuous Integration
.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-app-ora-grading/coverage.svg?branch=master
:target: https://codecov.io/github/openedx/frontend-app-ora-grading?branch=master
:alt: Codecov

19
catalog-info.yaml Normal file
View File

@@ -0,0 +1,19 @@
# This file records information about this repo. Its use is described in OEP-55:
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'frontend-app-ora-grading'
description: "Frontend grading experience for Open Response Assessments (ORAs)"
links:
- url: "https://ora-grading.edx.org"
title: "Production Site"
icon: "Web"
- url: "https://ora-grading.stage.edx.org"
title: "Stage Site"
icon: "Web"
spec:
owner: group:content-aurora
type: 'website'
lifecycle: 'production'

View File

@@ -26,4 +26,3 @@ There are only two requirements for a good `make target` name
What `make validate-no-uncommitted-package-lock-changes` does is `git diff`s for any `package-lock.json` file changes in your project.
This is important because `npm` uses the pinned dependencies in your `package-lock.json` file to build the `node_modules` directory. However, the dependencies defined within the `package.json` file can be modified manually, for example, to become misaligned with the dependencies defined within the `package-lock.json`. So when `npm install` executes, the `package-lock.json` file will be updated to mirror the modified `package.json` changes.

View File

@@ -12,6 +12,9 @@ module.exports = createConfig('jest', {
coveragePathIgnorePatterns: [
'src/segment.js',
'src/postcss.config.js',
'testUtils', // don't unit test jest mocking tools
'src/data/services/lms/fakeData', // don't unit test mock data
'src/test', // don't unit test integration test utils
],
testTimeout: 120000,
testEnvironment: 'jsdom',

53339
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,6 @@
},
"scripts": {
"build": "fedx-scripts webpack",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"is-es5": "es-check es5 ./dist/*.js",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
@@ -26,22 +24,22 @@
"access": "public"
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
"@edx/frontend-component-footer": "10.1.6",
"@edx/frontend-component-header": "^2.4.6",
"@edx/frontend-platform": "^1.15.6",
"@edx/paragon": "16.14.4",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "12.7.0",
"@edx/frontend-component-header": "4.11.1",
"@edx/frontend-platform": "6.2.0",
"@edx/paragon": "21.5.6",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15",
"@fortawesome/react-fontawesome": "^0.2.0",
"@redux-beacon/segment": "^1.1.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",
"@testing-library/user-event": "^14.0.0",
"@zip.js/zip.js": "^2.4.6",
"axios": "^0.21.4",
"axios": "^0.27.0",
"classnames": "^2.3.1",
"core-js": "3.16.2",
"core-js": "3.35.1",
"dompurify": "^2.3.1",
"email-prop-type": "^3.0.1",
"enzyme": "^3.11.0",
@@ -49,46 +47,48 @@
"file-saver": "^2.0.5",
"filesize": "^8.0.6",
"font-awesome": "4.7.0",
"history": "5.0.1",
"history": "5.3.0",
"html-react-parser": "^1.3.0",
"lodash": "^4.17.21",
"prop-types": "15.7.2",
"query-string": "7.0.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-intl": "^5.20.9",
"moment": "^2.29.3",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-intl": "6.4.7",
"react-pdf": "^5.5.0",
"react-redux": "^7.2.4",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-redux": "^7.2.9",
"react-router": "6.21.3",
"react-router-dom": "6.21.3",
"react-router-redux": "^5.0.0-alpha.9",
"redux": "4.1.1",
"redux": "4.2.1",
"redux-beacon": "^2.1.0",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-thunk": "2.3.0",
"regenerator-runtime": "^0.13.9",
"redux-thunk": "2.4.2",
"regenerator-runtime": "^0.14.0",
"reselect": "^4.0.0",
"util": "^0.12.4",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@edx/frontend-build": "^9.1.4",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"@edx/browserslist-config": "^1.2.0",
"@edx/frontend-build": "13.0.10",
"@edx/reactifex": "^2.1.1",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "12.1.5",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"axios-mock-adapter": "^1.20.0",
"codecov": "^3.8.3",
"enzyme-adapter-react-16": "^1.15.6",
"es-check": "^6.0.0",
"fetch-mock": "^9.11.0",
"husky": "^7.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "27.0.6",
"jest": "29.7.0",
"jest-expect-message": "^1.0.2",
"react-dev-utils": "^11.0.4",
"react-test-renderer": "^16.14.0",
"react-dev-utils": "^12.0.1",
"react-test-renderer": "^17.0.2",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.4",
"semantic-release": "^17.4.5"
"semantic-release": "^19.0.3"
}
}

34
renovate.json Normal file
View File

@@ -0,0 +1,34 @@
{
"extends": [
"config:base",
"schedule:weekly",
":automergeLinters",
":automergeMinor",
":automergeTesters",
":enableVulnerabilityAlerts",
":rebaseStalePrs",
":semanticCommits",
":updateNotScheduled"
],
"packageRules": [
{
"matchDepTypes": [
"devDependencies"
],
"matchUpdateTypes": [
"lockFileMaintenance",
"minor",
"patch",
"pin"
],
"automerge": true
},
{
"matchPackagePatterns": ["@edx", "@openedx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
],
"timezone": "America/New_York",
"schedule": ["before 11pm"]
}

View File

@@ -9,19 +9,23 @@ import { LearningHeader as Header } from '@edx/frontend-component-header';
import { selectors } from 'data/redux';
import DemoWarning from 'containers/DemoWarning';
import CTA from 'containers/CTA';
import ListView from 'containers/ListView';
import './App.scss';
import Head from './components/Head';
export const App = ({ courseMetadata, isEnabled }) => (
<Router>
<div>
<Head />
<Header
courseTitle={courseMetadata.title}
courseNumber={courseMetadata.number}
courseOrg={courseMetadata.org}
/>
{!isEnabled && <DemoWarning />}
<CTA />
<main>
<ListView />
</main>

View File

@@ -23,7 +23,9 @@ jest.mock('@edx/frontend-component-header', () => ({
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;

View File

@@ -3,12 +3,14 @@
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>
@@ -22,11 +24,13 @@ exports[`App router component snapshot: disabled (show demo warning) 1`] = `
exports[`App router component snapshot: enabled 1`] = `
<BrowserRouter>
<div>
<Head />
<Header
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
/>
<CTA />
<main>
<ListView />
</main>

View File

@@ -0,0 +1,24 @@
// 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],
}
}
wrapWithRouter={false}
>
<App />
</AppProvider>
`;

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { AlertModal, ActionRow, Button } from '@edx/paragon';
import { nullMethod } from 'hooks';
export const ConfirmModal = ({
title,
@@ -15,7 +16,7 @@ export const ConfirmModal = ({
<AlertModal
className="confirm-modal"
title={title}
onClose={() => ({})}
onClose={nullMethod}
isOpen={isOpen}
footerNode={(
<ActionRow>

View File

@@ -9,141 +9,77 @@ import { ChevronLeft, ChevronRight } from '@edx/paragon/icons';
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import { rendererHooks } from './pdfHooks';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
/**
* <PDFRenderer />
*/
export class PDFRenderer extends React.Component {
static INITIAL_STATE = {
pageNumber: 1,
numPages: 1,
relativeHeight: 0,
};
export const PDFRenderer = ({
onError,
onSuccess,
url,
}) => {
const {
pageNumber,
numPages,
relativeHeight,
wrapperRef,
onDocumentLoadSuccess,
onLoadPageSuccess,
onDocumentLoadError,
onInputPageChange,
onNextPageButtonClick,
onPrevPageButtonClick,
hasNext,
hasPrev,
} = rendererHooks({ onError, onSuccess });
constructor(props) {
super(props);
this.state = { ...PDFRenderer.INITIAL_STATE };
this.wrapperRef = React.createRef();
this.onDocumentLoadSuccess = this.onDocumentLoadSuccess.bind(this);
this.onDocumentLoadError = this.onDocumentLoadError.bind(this);
this.onLoadPageSuccess = this.onLoadPageSuccess.bind(this);
this.onPrevPageButtonClick = this.onPrevPageButtonClick.bind(this);
this.onNextPageButtonClick = this.onNextPageButtonClick.bind(this);
this.onInputPageChange = this.onInputPageChange.bind(this);
}
onDocumentLoadSuccess = ({ numPages }) => {
this.props.onSuccess();
this.setState({ numPages });
};
onLoadPageSuccess = (page) => {
const pageWidth = page.view[2];
const pageHeight = page.view[3];
const wrapperHeight = this.wrapperRef.current.getBoundingClientRect().width;
const relativeHeight = (wrapperHeight * pageHeight) / pageWidth;
if (relativeHeight !== this.state.relativeHeight) {
this.setState({ relativeHeight });
}
};
onDocumentLoadError = (error) => {
let status;
switch (error.name) {
case 'MissingPDFException':
status = 404;
break;
default:
status = 500;
break;
}
this.props.onError(status);
};
onInputPageChange = ({ target: { value } }) => {
this.setPageNumber(parseInt(value, 10));
}
onPrevPageButtonClick = () => {
this.setPageNumber(this.state.pageNumber - 1);
}
onNextPageButtonClick = () => {
this.setPageNumber(this.state.pageNumber + 1);
}
setPageNumber(pageNumber) {
if (pageNumber > 0 && pageNumber <= this.state.numPages) {
this.setState({ pageNumber });
}
}
get hasNext() {
return this.state.pageNumber < this.state.numPages;
}
get hasPrev() {
return this.state.pageNumber > 1;
}
render() {
return (
<div ref={this.wrapperRef} className="pdf-renderer">
<Document
file={this.props.url}
onLoadSuccess={this.onDocumentLoadSuccess}
onLoadError={this.onDocumentLoadError}
>
{/* <Outline /> */}
<div
className="page-wrapper"
style={{
height: this.state.relativeHeight,
}}
>
<Page
pageNumber={this.state.pageNumber}
onLoadSuccess={this.onLoadPageSuccess}
/>
</div>
</Document>
<ActionRow className="d-flex justify-content-center m-0">
<IconButton
size="inline"
alt="previous pdf page"
iconAs={Icon}
src={ChevronLeft}
disabled={!this.hasPrev}
onClick={this.onPrevPageButtonClick}
return (
<div ref={wrapperRef} className="pdf-renderer">
<Document
file={url}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={onDocumentLoadError}
>
{/* <Outline /> */}
<div className="page-wrapper" style={{ height: relativeHeight }}>
<Page pageNumber={pageNumber} onLoadSuccess={onLoadPageSuccess} />
</div>
</Document>
<ActionRow className="d-flex justify-content-center m-0">
<IconButton
size="inline"
alt="previous pdf page"
iconAs={Icon}
src={ChevronLeft}
disabled={!hasPrev}
onClick={onPrevPageButtonClick}
/>
<Form.Group className="d-flex align-items-center m-0">
<Form.Label isInline>Page </Form.Label>
<Form.Control
type="number"
min={0}
max={numPages}
value={pageNumber}
onChange={onInputPageChange}
/>
<Form.Group className="d-flex align-items-center m-0">
<Form.Label isInline>Page </Form.Label>
<Form.Control
type="number"
min={0}
max={this.state.numPages}
value={this.state.pageNumber}
onChange={this.onInputPageChange}
/>
<Form.Label isInline> of {this.state.numPages}</Form.Label>
</Form.Group>
<IconButton
size="inline"
alt="next pdf page"
iconAs={Icon}
src={ChevronRight}
disabled={!this.hasNext}
onClick={this.onNextPageButtonClick}
/>
</ActionRow>
</div>
);
}
}
<Form.Label isInline> of {numPages}</Form.Label>
</Form.Group>
<IconButton
size="inline"
alt="next pdf page"
iconAs={Icon}
src={ChevronRight}
disabled={!hasNext}
onClick={onNextPageButtonClick}
/>
</ActionRow>
</div>
);
};
PDFRenderer.defaultProps = {};

View File

@@ -1,221 +1,57 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Document, Page } from 'react-pdf';
import { Form, IconButton } from '@edx/paragon';
import PDFRenderer from './PDFRenderer';
import * as hooks from './pdfHooks';
jest.mock('react-pdf', () => ({
pdfjs: { GlobalWorkerOptions: {} },
Document: () => 'Document',
Page: () => 'Page',
}));
jest.mock('./pdfHooks', () => ({
rendererHooks: jest.fn(),
}));
describe('PDF Renderer Component', () => {
const props = {
url: 'some_url.pdf',
onError: jest.fn().mockName('this.props.onError'),
onSuccess: jest.fn().mockName('this.props.onSuccess'),
};
const hookProps = {
pageNumber: 1,
numPages: 10,
relativeHeight: 200,
wrapperRef: { current: 'hooks.wrapperRef' },
onDocumentLoadSuccess: jest.fn().mockName('hooks.onDocumentLoadSuccess'),
onLoadPageSuccess: jest.fn().mockName('hooks.onLoadPageSuccess'),
onDocumentLoadError: jest.fn().mockName('hooks.onDocumentLoadError'),
onInputPageChange: jest.fn().mockName('hooks.onInputPageChange'),
onNextPageButtonClick: jest.fn().mockName('hooks.onNextPageButtonClick'),
onPrevPageButtonClick: jest.fn().mockName('hooks.onPrevPageButtonClick'),
hasNext: true,
hasPref: false,
};
props.onError = jest.fn().mockName('this.props.onError');
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
let el;
describe('snapshots', () => {
beforeEach(() => {
el = shallow(<PDFRenderer {...props} />);
el.instance().onDocumentLoadSuccess = jest
.fn()
.mockName('onDocumentLoadSuccess');
el.instance().onDocumentLoadError = jest
.fn()
.mockName('onDocumentLoadError');
el.instance().onLoadPageSuccess = jest.fn().mockName('onLoadPageSuccess');
});
test('snapshot', () => {
el.instance().onPrevPageButtonClick = jest
.fn()
.mockName('onPrevPageButtonClick');
el.instance().onNextPageButtonClick = jest
.fn()
.mockName('onNextPageButtonClick');
el.instance().onInputPageChange = jest.fn().mockName('onInputPageChange');
expect(el.instance().render()).toMatchSnapshot();
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('Component', () => {
const numPages = 99;
const pageNumber = 234;
beforeEach(() => {
el = shallow(<PDFRenderer {...props} />);
describe('snapshots', () => {
test('first page, prev is disabled', () => {
hooks.rendererHooks.mockReturnValue(hookProps);
expect(shallow(<PDFRenderer {...props} />)).toMatchSnapshot();
});
describe('render', () => {
describe('Top-level document', () => {
let documentEl;
beforeEach(() => { documentEl = el.find(Document); });
it('displays file from props.url', () => {
expect(documentEl.props().file).toEqual(props.url);
});
it('calls this.onDocumentLoadSuccess onLoadSuccess', () => {
expect(documentEl.props().onLoadSuccess).toEqual(el.instance().onDocumentLoadSuccess);
});
it('calls this.onDocumentLoadError onLoadError', () => {
expect(documentEl.props().onLoadError).toEqual(el.instance().onDocumentLoadError);
});
});
describe('Page', () => {
let pageProps;
beforeEach(() => {
el.instance().setState({ pageNumber });
pageProps = el.find(Page).props();
});
it('loads pageNumber from state', () => {
expect(pageProps.pageNumber).toEqual(pageNumber);
});
it('calls onLoadPageSuccess onLoadSuccess', () => {
expect(pageProps.onLoadSuccess).toEqual(el.instance().onLoadPageSuccess);
});
});
describe('pagination ActionRow', () => {
describe('Previous page button', () => {
let hasPrev;
beforeEach(() => {
hasPrev = jest.spyOn(el.instance(), 'hasPrev', 'get').mockReturnValue(false);
});
const btn = () => shallow(el.instance().render()).find(IconButton).at(0).props();
test('disabled iff not this.hasPrev', () => {
expect(btn().disabled).toEqual(true);
hasPrev.mockReturnValue(true);
expect(btn().disabled).toEqual(false);
});
it('calls onPrevPageButtonClick onClick', () => {
expect(btn().onClick).toEqual(el.instance().onPrevPageButtonClick);
});
});
describe('page indicator', () => {
const control = () => el.find(Form.Control).at(0).props();
const labels = () => {
const flat = el.find({ isInline: true });
return [0, 1].map(i => flat.at(i).text());
};
beforeEach(() => { el.instance().setState({ numPages, pageNumber }); });
test('labels: Page <state.pageNumber> of <state.numPages>', () => {
expect(`${labels()[0]}${control().value}${labels()[1]}`).toEqual(
`Page ${pageNumber} of ${numPages}`,
);
});
it('loads max from state.numPages', () => expect(control().max).toEqual(numPages));
it('loads value from state.pageNumber', () => {
expect(control().value).toEqual(pageNumber);
});
it('calls onInputPageChange onChange', () => {
expect(control().onChange).toEqual(el.instance().onInputPageChange);
});
});
describe('Next page button', () => {
let hasNext;
beforeEach(() => {
hasNext = jest.spyOn(el.instance(), 'hasNext', 'get').mockReturnValue(false);
});
const btn = () => shallow(el.instance().render()).find(IconButton).at(1).props();
test('disabled iff not this.hasNext', () => {
expect(btn().disabled).toEqual(true);
hasNext.mockReturnValue(true);
expect(btn().disabled).toEqual(false);
});
it('calls onNextPageButtonClick onClick', () => {
expect(btn().onClick).toEqual(el.instance().onNextPageButtonClick);
});
});
});
});
describe('behavior', () => {
test('initial state', () => {
expect(el.instance().state).toEqual(PDFRenderer.INITIAL_STATE);
});
describe('onDocumentLoadSuccess', () => {
test('loads numPages into state', () => {
el.instance().onDocumentLoadSuccess({ numPages });
expect(el.instance().state.numPages).toEqual(numPages);
});
});
describe('onLoadPageSuccess', () => {
const [pageHeight, pageWidth] = [23, 34];
const page = { view: [1, 2, pageWidth, pageHeight] };
const wrapperWidth = 20;
const expected = (wrapperWidth * pageHeight) / pageWidth;
beforeEach(() => {
el.instance().wrapperRef = {
current: {
getBoundingClientRect: () => ({ width: wrapperWidth }),
},
};
});
it('sets relative height if it has changes', () => {
el.instance().onLoadPageSuccess(page);
expect(el.instance().state.relativeHeight).toEqual(expected);
});
it('does not try to set height if has not changes', () => {
el.instance().setState({ relativeHeight: expected });
el.instance().setState = jest.fn();
el.instance().onLoadPageSuccess(page);
expect(el.instance().setState).not.toHaveBeenCalled();
});
});
describe('setPageNumber inheritors', () => {
beforeEach(() => {
el.instance().setPageNumber = jest.fn();
el.instance().setState({ pageNumber });
});
describe('onInputChange', () => {
it('calls setPageNumber with int value of event target value', () => {
el.instance().onInputPageChange({ target: { value: '23' } });
expect(el.instance().setPageNumber).toHaveBeenCalledWith(23);
});
});
describe('onPrevPageButtonClick', () => {
it('calls setPageNumber with state.pageNumber - 1', () => {
el.instance().onPrevPageButtonClick();
expect(el.instance().setPageNumber).toHaveBeenCalledWith(pageNumber - 1);
});
});
describe('onNextPageButtonClick', () => {
it('calls setPageNumber with state.pageNumber + 1', () => {
el.instance().onNextPageButtonClick();
expect(el.instance().setPageNumber).toHaveBeenCalledWith(pageNumber + 1);
});
});
});
describe('setPageNumber', () => {
it('calls setState with pageNumber iff valid', () => {
el.instance().setState({ numPages });
const setState = jest.spyOn(el.instance(), 'setState');
el.instance().setPageNumber(0);
expect(setState).not.toHaveBeenCalled();
el.instance().setPageNumber(numPages + 1);
expect(setState).not.toHaveBeenCalled();
el.instance().setPageNumber(2);
expect(setState).toHaveBeenCalledWith({ pageNumber: 2 });
});
});
describe('hasNext getter', () => {
it('returns true iff state.pageNumber < state.numPages', () => {
el.instance().setState({ pageNumber: 1, numPages: 1 });
expect(el.instance().hasNext).toEqual(false);
el.instance().setState({ pageNumber: 1, numPages: 2 });
expect(el.instance().hasNext).toEqual(true);
});
});
describe('hasPrev getter', () => {
it('returns true iff state.pageNumber > 1', () => {
el.instance().setState({ pageNumber: 1 });
expect(el.instance().hasPrev).toEqual(false);
el.instance().setState({ pageNumber: 2 });
expect(el.instance().hasPrev).toEqual(true);
});
test('on last page, next is disabled', () => {
hooks.rendererHooks.mockReturnValue({
...hookProps,
pageNumber: hookProps.numPages,
hasNext: false,
hasPrev: true,
});
expect(shallow(<PDFRenderer {...props} />)).toMatchSnapshot();
});
});
});

View File

@@ -1,18 +1,9 @@
import React, { useMemo, useState } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { get } from 'axios';
import { rendererHooks } from './textHooks';
const TXTRenderer = ({ url, onError, onSuccess }) => {
const [content, setContent] = useState('');
useMemo(() => {
get(url)
.then(({ data }) => {
onSuccess();
setContent(data);
})
.catch(({ response }) => onError(response.status));
}, [url]);
const { content } = rendererHooks({ url, onError, onSuccess });
return (
<pre className="txt-renderer">
{content}

View File

@@ -3,23 +3,21 @@ import { shallow } from 'enzyme';
import TXTRenderer from './TXTRenderer';
jest.mock('axios', () => ({
get: jest.fn((...args) => Promise.resolve({ data: `Content of ${args}` })),
}));
jest.mock('./textHooks', () => {
const content = 'test-content';
return {
content,
rendererHooks: (args) => ({ content, rendererHooks: args }),
};
});
describe('TXT Renderer Component', () => {
const props = {
url: 'some_url.txt',
onError: jest.fn().mockName('this.props.onError'),
onSuccess: jest.fn().mockName('this.props.onSuccess'),
};
props.onError = jest.fn().mockName('this.props.onError');
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
let el;
beforeEach(() => {
el = shallow(<TXTRenderer {...props} />);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
expect(shallow(<TXTRenderer {...props} />)).toMatchSnapshot();
});
});

View File

@@ -1,24 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PDF Renderer Component snapshots snapshot 1`] = `
exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = `
<div
className="pdf-renderer"
>
<Document
file="some_url.pdf"
onLoadError={[MockFunction onDocumentLoadError]}
onLoadSuccess={[MockFunction onDocumentLoadSuccess]}
onLoadError={[MockFunction hooks.onDocumentLoadError]}
onLoadSuccess={[MockFunction hooks.onDocumentLoadSuccess]}
>
<div
className="page-wrapper"
style={
Object {
"height": 0,
"height": 200,
}
}
>
<Page
onLoadSuccess={[MockFunction onLoadPageSuccess]}
onLoadSuccess={[MockFunction hooks.onLoadPageSuccess]}
pageNumber={1}
/>
</div>
@@ -30,7 +30,7 @@ exports[`PDF Renderer Component snapshots snapshot 1`] = `
alt="previous pdf page"
disabled={true}
iconAs="Icon"
onClick={[MockFunction onPrevPageButtonClick]}
onClick={[MockFunction hooks.onPrevPageButtonClick]}
size="inline"
src={[MockFunction icons.ChevronLeft]}
/>
@@ -43,9 +43,9 @@ exports[`PDF Renderer Component snapshots snapshot 1`] = `
Page
</Form.Label>
<Form.Control
max={1}
max={10}
min={0}
onChange={[MockFunction onInputPageChange]}
onChange={[MockFunction hooks.onInputPageChange]}
type="number"
value={1}
/>
@@ -53,14 +53,82 @@ exports[`PDF Renderer Component snapshots snapshot 1`] = `
isInline={true}
>
of
1
10
</Form.Label>
</Form.Group>
<IconButton
alt="next pdf page"
disabled={true}
disabled={false}
iconAs="Icon"
onClick={[MockFunction onNextPageButtonClick]}
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]}
/>

View File

@@ -4,6 +4,6 @@ exports[`TXT Renderer Component snapshot 1`] = `
<pre
className="txt-renderer"
>
Content of some_url.txt
test-content
</pre>
`;

View File

@@ -0,0 +1,81 @@
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 { ErrorStatuses } from 'data/constants/requests';
import { StrictDict } from 'utils';
import * as module from './pdfHooks';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export const errors = StrictDict({
missingPDF: 'MissingPDFException',
});
export const state = StrictDict({
pageNumber: (val) => useState(val),
numPages: (val) => useState(val),
relativeHeight: (val) => useState(val),
});
export const initialState = {
pageNumber: 1,
numPages: 1,
relativeHeight: 1,
};
export const safeSetPageNumber = ({ numPages, rawSetPageNumber }) => (pageNumber) => {
if (pageNumber > 0 && pageNumber <= numPages) {
rawSetPageNumber(pageNumber);
}
};
export const rendererHooks = ({
onError,
onSuccess,
}) => {
const [pageNumber, rawSetPageNumber] = module.state.pageNumber(initialState.pageNumber);
const [numPages, setNumPages] = module.state.numPages(initialState.numPages);
const [relativeHeight, setRelativeHeight] = module.state.relativeHeight(
initialState.relativeHeight,
);
const setPageNumber = module.safeSetPageNumber({ numPages, rawSetPageNumber });
const wrapperRef = useRef();
return {
pageNumber,
numPages,
relativeHeight,
wrapperRef,
onDocumentLoadSuccess: (args) => {
onSuccess();
setNumPages(args.numPages);
},
onLoadPageSuccess: (page) => {
const pageWidth = page.view[2];
const pageHeight = page.view[3];
const wrapperHeight = wrapperRef.current.getBoundingClientRect().width;
const newHeight = (wrapperHeight * pageHeight) / pageWidth;
setRelativeHeight(newHeight);
},
onDocumentLoadError: (error) => {
let status;
if (error.name === errors.missingPDF) {
status = ErrorStatuses.notFound;
} else {
status = ErrorStatuses.serverError;
}
onError(status);
},
onInputPageChange: ({ target: { value } }) => setPageNumber(parseInt(value, 10)),
onPrevPageButtonClick: () => setPageNumber(pageNumber - 1),
onNextPageButtonClick: () => setPageNumber(pageNumber + 1),
hasNext: pageNumber < numPages,
hasPrev: pageNumber > 1,
};
};

View File

@@ -0,0 +1,148 @@
import React from 'react';
import { MockUseState } from 'testUtils';
import { keyStore } from 'utils';
import { ErrorStatuses } from 'data/constants/requests';
import * as hooks from './pdfHooks';
jest.mock('react-pdf', () => ({
pdfjs: { GlobalWorkerOptions: {} },
Document: () => 'Document',
Page: () => 'Page',
}));
const state = new MockUseState(hooks);
const hookKeys = keyStore(hooks);
const testValue = 'my-test-value';
describe('PDF Renderer hooks', () => {
beforeAll(() => {
jest.clearAllMocks();
});
describe('state hooks', () => {
state.testGetter(state.keys.pageNumber);
state.testGetter(state.keys.numPages);
state.testGetter(state.keys.relativeHeight);
});
describe('non-state hooks', () => {
beforeEach(() => state.mock());
afterEach(() => state.restore());
describe('safeSetPageNumber', () => {
it('returns value handler that sets page number if valid', () => {
const rawSetPageNumber = jest.fn();
const numPages = 10;
hooks.safeSetPageNumber({ numPages, rawSetPageNumber })(0);
expect(rawSetPageNumber).not.toHaveBeenCalled();
hooks.safeSetPageNumber({ numPages, rawSetPageNumber })(numPages + 1);
expect(rawSetPageNumber).not.toHaveBeenCalled();
hooks.safeSetPageNumber({ numPages, rawSetPageNumber })(numPages - 1);
expect(rawSetPageNumber).toHaveBeenCalledWith(numPages - 1);
});
});
describe('rendererHooks', () => {
const props = {
url: 'some_url.pdf',
onError: jest.fn().mockName('this.props.onError'),
onSuccess: jest.fn().mockName('this.props.onSuccess'),
};
let setPageNumber;
let hook;
let mockSetPageNumber;
let mockSafeSetPageNumber;
beforeEach(() => {
mockSetPageNumber = jest.fn(val => ({ setPageNumber: { val } }));
mockSafeSetPageNumber = jest.fn(() => mockSetPageNumber);
setPageNumber = jest.spyOn(hooks, hookKeys.safeSetPageNumber)
.mockImplementation(mockSafeSetPageNumber);
hook = hooks.rendererHooks(props);
});
afterAll(() => {
setPageNumber.mockRestore();
});
describe('returned object', () => {
Object.keys(state.keys).forEach(key => {
test(`${key} tied to store and initialized from initialState`, () => {
expect(hook[key]).toEqual(hooks.initialState[key]);
expect(hook[key]).toEqual(state.stateVals[key]);
});
});
});
test('wrapperRef passed as react ref', () => {
expect(hook.wrapperRef.useRef).toEqual(true);
});
describe('onDocumentLoadSuccess', () => {
it('calls onSuccess and sets numPages based on args', () => {
hook.onDocumentLoadSuccess({ numPages: testValue });
expect(props.onSuccess).toHaveBeenCalled();
expect(state.setState.numPages).toHaveBeenCalledWith(testValue);
});
});
describe('onLoadPageSuccess', () => {
it('sets relative height based on page size', () => {
const width = 23;
React.useRef.mockReturnValueOnce({
current: {
getBoundingClientRect: () => ({ width }),
},
});
const [pageWidth, pageHeight] = [20, 30];
const page = { view: [0, 0, pageWidth, pageHeight] };
hook = hooks.rendererHooks(props);
const height = (width * pageHeight) / pageWidth;
hook.onLoadPageSuccess(page);
expect(state.setState.relativeHeight).toHaveBeenCalledWith(height);
});
});
describe('onDocumentLoadError', () => {
it('calls onError with notFound error if error is missingPDF error', () => {
hook.onDocumentLoadError({ name: hooks.errors.missingPDF });
expect(props.onError).toHaveBeenCalledWith(ErrorStatuses.notFound);
});
it('calls onError with serverError by default', () => {
hook.onDocumentLoadError({ name: testValue });
expect(props.onError).toHaveBeenCalledWith(ErrorStatuses.serverError);
});
});
describe('onInputPageChange', () => {
it('calls setPageNumber with int event target value', () => {
hook.onInputPageChange({ target: { value: '2.3' } });
expect(mockSetPageNumber).toHaveBeenCalledWith(2);
});
});
describe('onPrevPageButtonClick', () => {
it('calls setPageNumber with current page number - 1', () => {
hook.onPrevPageButtonClick();
expect(mockSetPageNumber).toHaveBeenCalledWith(hook.pageNumber - 1);
});
});
describe('onNextPageButtonClick', () => {
it('calls setPageNumber with current page number + 1', () => {
hook.onNextPageButtonClick();
expect(mockSetPageNumber).toHaveBeenCalledWith(hook.pageNumber + 1);
});
});
test('hasNext returns true iff pageNumber is less than total number of pages', () => {
state.mockVal(state.keys.numPages, 10);
state.mockVal(state.keys.pageNumber, 9);
hook = hooks.rendererHooks(props);
expect(hook.hasNext).toEqual(true);
state.mockVal(state.keys.pageNumber, 10);
hook = hooks.rendererHooks(props);
expect(hook.hasNext).toEqual(false);
});
test('hasPrev returns true iff pageNumber is greater than 1', () => {
state.mockVal(state.keys.pageNumber, 1);
hook = hooks.rendererHooks(props);
expect(hook.hasPrev).toEqual(false);
state.mockVal(state.keys.pageNumber, 0);
hook = hooks.rendererHooks(props);
expect(hook.hasPrev).toEqual(false);
state.mockVal(state.keys.pageNumber, 2);
hook = hooks.rendererHooks(props);
expect(hook.hasPrev).toEqual(true);
});
});
});
});

View File

@@ -0,0 +1,34 @@
import { useEffect, useState } from 'react';
import { get } from 'axios';
import { StrictDict } from 'utils';
import * as module from './textHooks';
export const state = StrictDict({
content: (val) => useState(val),
});
export const fetchFile = async ({
setContent,
url,
onError,
onSuccess,
}) => get(url)
.then(({ data }) => {
onSuccess();
setContent(data);
})
.catch((e) => onError(e.response.status));
export const rendererHooks = ({ url, onError, onSuccess }) => {
const [content, setContent] = module.state.content('');
useEffect(() => {
module.fetchFile({
setContent,
url,
onError,
onSuccess,
});
}, [onError, onSuccess, setContent, url]);
return { content };
};

View File

@@ -0,0 +1,94 @@
/* eslint-disable prefer-promise-reject-errors */
import { useEffect } from 'react';
import * as axios from 'axios';
import { keyStore } from 'utils';
import { MockUseState } from 'testUtils';
import * as hooks from './textHooks';
jest.mock('axios', () => ({
get: jest.fn(),
}));
const hookKeys = keyStore(hooks);
const state = new MockUseState(hooks);
let hook;
const testValue = 'test-value';
const props = {
url: 'test-url',
onError: jest.fn(),
onSuccess: jest.fn(),
};
describe('Text file preview hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('state hooks', () => {
state.testGetter(state.keys.content);
});
describe('non-state hooks', () => {
beforeEach(() => {
state.mock();
});
afterEach(() => {
state.restore();
});
describe('rendererHooks', () => {
it('returns content tied to hook state', () => {
hook = hooks.rendererHooks(props);
expect(hook.content).toEqual(state.stateVals.content);
expect(hook.content).toEqual('');
});
describe('initialization behavior', () => {
let cb;
let prereqs;
const loadHook = () => {
hook = hooks.rendererHooks(props);
[[cb, prereqs]] = useEffect.mock.calls;
};
it('calls fetchFile method, predicated on setContent, url, and callbacks', () => {
jest.spyOn(hooks, hookKeys.fetchFile).mockImplementationOnce(() => {});
loadHook();
expect(useEffect).toHaveBeenCalled();
expect(prereqs).toEqual([
props.onError,
props.onSuccess,
state.setState.content,
props.url,
]);
expect(hooks.fetchFile).not.toHaveBeenCalled();
cb();
expect(hooks.fetchFile).toHaveBeenCalledWith({
onError: props.onError,
onSuccess: props.onSuccess,
setContent: state.setState.content,
url: props.url,
});
});
});
});
describe('fetchFile', () => {
describe('onSuccess', () => {
it('calls get', async () => {
const testData = 'test-data';
axios.get.mockReturnValueOnce(Promise.resolve({ data: testData }));
await hooks.fetchFile({ ...props, setContent: state.setState.content });
expect(props.onSuccess).toHaveBeenCalled();
expect(state.setState[state.keys.content]).toHaveBeenCalledWith(testData);
});
});
describe('onError', () => {
it('calls get on the passed url when it changes', async () => {
axios.get.mockReturnValueOnce(Promise.reject(
{ response: { status: testValue } },
));
await hooks.fetchFile({ ...props, setContent: state.setState.content });
expect(props.onError).toHaveBeenCalledWith(testValue);
});
});
});
});
});

View File

@@ -12,7 +12,11 @@ import './FileCard.scss';
*/
export const FileCard = ({ file, children }) => (
<Card className="file-card" key={file.name}>
<Collapsible className="file-collapsible" defaultOpen title={<h3 className="file-card-title">{file.name}</h3>}>
<Collapsible
className="file-collapsible"
defaultOpen
title={<h3 className="file-card-title">{file.name}</h3>}
>
<div className="preview-panel">
<FileInfo><FilePopoverContent {...file} /></FileInfo>
{children}

View File

@@ -28,6 +28,6 @@
@include media-breakpoint-down(sm) {
.file-card-title {
width: map-get($container-max-widths, "sm")/2;
width: calc(map-get($container-max-widths, "sm")/2);
}
}

View File

@@ -8,6 +8,7 @@ import {
} from '@edx/paragon';
import { InfoOutline } from '@edx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { nullMethod } from 'hooks';
import messages from './messages';
/**
@@ -19,13 +20,13 @@ export const FileInfo = ({ onClick, children }) => (
placement="right-end"
flip
overlay={(
<Popover className="overlay-help-popover">
<Popover id="file-popover" className="overlay-help-popover">
<Popover.Content>{children}</Popover.Content>
</Popover>
)}
>
<Button
size="small"
size="sm"
variant="tertiary"
onClick={onClick}
iconAfter={InfoOutline}
@@ -36,7 +37,7 @@ export const FileInfo = ({ onClick, children }) => (
);
FileInfo.defaultProps = {
onClick: () => {},
onClick: nullMethod,
};
FileInfo.propTypes = {
onClick: PropTypes.func,

View File

@@ -1,123 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StrictDict } from 'utils';
import { FileTypes } from 'data/constants/files';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
PDFRenderer,
ImageRenderer,
TXTRenderer,
} from 'components/FilePreview/BaseRenderers';
import FileCard from './FileCard';
import { ErrorBanner, LoadingBanner } from './Banners';
import messages from './messages';
export const RENDERERS = StrictDict({
[FileTypes.pdf]: PDFRenderer,
[FileTypes.jpg]: ImageRenderer,
[FileTypes.jpeg]: ImageRenderer,
[FileTypes.bmp]: ImageRenderer,
[FileTypes.png]: ImageRenderer,
[FileTypes.txt]: TXTRenderer,
[FileTypes.gif]: ImageRenderer,
[FileTypes.jfif]: ImageRenderer,
[FileTypes.pjpeg]: ImageRenderer,
[FileTypes.pjp]: ImageRenderer,
[FileTypes.svg]: ImageRenderer,
});
export const ERROR_STATUSES = {
404: {
headingMessage: messages.fileNotFoundError,
children: <FormattedMessage {...messages.fileNotFoundError} />,
},
500: {
headingMessage: messages.unknownError,
children: <FormattedMessage {...messages.unknownError} />,
},
};
export const SUPPORTED_TYPES = Object.keys(RENDERERS);
export const getFileType = (fileName) => fileName.split('.').pop()?.toLowerCase();
export const isSupported = (file) => SUPPORTED_TYPES.includes(getFileType(file.name));
import { renderHooks } from './hooks';
/**
* <FileRenderer />
*/
export class FileRenderer extends React.Component {
constructor(props) {
super(props);
this.state = {
errorStatus: null,
isLoading: true,
};
this.onError = this.onError.bind(this);
this.onSuccess = this.onSuccess.bind(this);
this.resetState = this.resetState.bind(this);
}
onError(status) {
this.setState({
errorStatus: status,
isLoading: false,
});
}
onSuccess() {
this.setState({
errorStatus: null,
isLoading: false,
});
}
get error() {
const status = this.state.errorStatus;
return {
...ERROR_STATUSES[status] || ERROR_STATUSES[500],
actions: [
{
id: 'retry',
onClick: this.resetState,
message: messages.retryButton,
},
],
};
}
resetState = () => {
this.setState({
errorStatus: null,
isLoading: true,
});
};
render() {
const { file } = this.props;
const Renderer = RENDERERS[getFileType(file.name)];
return (
<FileCard key={file.downloadUrl} file={file}>
{this.state.isLoading && <LoadingBanner />}
{this.state.errorStatus ? (
<ErrorBanner {...this.error} />
) : (
<Renderer
fileName={file.name}
url={file.downloadUrl}
onError={this.onError}
onSuccess={this.onSuccess}
/>
)}
</FileCard>
);
}
}
export const FileRenderer = ({
file,
intl,
}) => {
const {
Renderer,
isLoading,
errorStatus,
error,
rendererProps,
} = renderHooks({ file, intl });
return (
<FileCard key={file.downloadUrl} file={file}>
{isLoading && <LoadingBanner />}
{errorStatus ? (
<ErrorBanner {...error} />
) : (
<Renderer {...rendererProps} />
)}
</FileCard>
);
};
FileRenderer.defaultProps = {};
FileRenderer.propTypes = {
@@ -125,6 +39,8 @@ FileRenderer.propTypes = {
name: PropTypes.string,
downloadUrl: PropTypes.string,
}).isRequired,
// injected
intl: intlShape.isRequired,
};
export default FileRenderer;
export default injectIntl(FileRenderer);

View File

@@ -1,132 +1,52 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FileTypes } from 'data/constants/files';
import {
ImageRenderer,
PDFRenderer,
TXTRenderer,
} from 'components/FilePreview/BaseRenderers';
import {
FileRenderer,
getFileType,
ERROR_STATUSES,
RENDERERS,
} from './FileRenderer';
import { formatMessage } from 'testUtils';
import { keyStore } from 'utils';
import { ErrorStatuses } from 'data/constants/requests';
import { FileRenderer } from './FileRenderer';
import * as hooks from './hooks';
jest.mock('./FileCard', () => 'FileCard');
jest.mock('components/FilePreview/BaseRenderers', () => ({
PDFRenderer: () => 'PDFRenderer',
ImageRenderer: () => 'ImageRenderer',
TXTRenderer: () => 'TXTRenderer',
}));
jest.mock('./Banners', () => ({
ErrorBanner: () => 'ErrorBanner',
LoadingBanner: () => 'LoadingBanner',
}));
const hookKeys = keyStore(hooks);
const props = {
file: {
downloadUrl: 'file download url',
name: 'filename.txt',
},
intl: { formatMessage },
};
describe('FileRenderer', () => {
describe('component', () => {
const supportedTypes = Object.keys(RENDERERS);
const files = [
...supportedTypes.map((fileType, index) => ({
name: `fake_file_${index}.${fileType}`,
description: `file description ${index}`,
downloadUrl: `/url-path/fake_file_${index}.${fileType}`,
})),
];
const els = files.map((file) => {
const el = shallow(<FileRenderer file={file} />);
el.instance().onError = jest.fn().mockName('this.props.onError');
el.instance().onSuccess = jest.fn().mockName('this.props.onSuccess');
return el;
});
describe('snapshot', () => {
els.forEach((el) => {
const file = el.prop('file');
const fileType = getFileType(file.name);
test(`successful rendering ${fileType}`, () => {
el.setState({ isLoading: false });
expect(el.instance().render()).toMatchSnapshot();
});
});
Object.keys(ERROR_STATUSES).forEach((status) => {
test(`has error ${status}`, () => {
const el = shallow(<FileRenderer file={files[0]} />);
el.instance().setState({
errorStatus: status,
isLoading: false,
});
el.instance().resetState = jest.fn().mockName('this.resetState');
expect(el.instance().render()).toMatchSnapshot();
});
});
});
describe('component', () => {
describe('uses the correct renderers', () => {
const checkFile = (index, expectedRenderer) => {
const file = files[index];
const el = shallow(<FileRenderer file={file} />);
const renderer = el.find(expectedRenderer);
const { url, fileName } = renderer.props();
expect(renderer).toBeDefined();
expect(url).toEqual(file.downloadUrl);
expect(fileName).toEqual(file.name);
test('isLoading, no Error', () => {
const hookProps = {
Renderer: () => 'Renderer',
isloading: true,
errorStatus: null,
error: null,
rendererProps: { prop: 'hooks.rendererProps' },
};
/**
* The manual process for this is prefer. I want to be more explicit
* of which file correspond to which renderer. If I use RENDERERS dicts,
* this wouldn't be a test.
*/
test(FileTypes.pdf, () => checkFile(0, PDFRenderer));
test(FileTypes.jpg, () => checkFile(1, ImageRenderer));
test(FileTypes.jpeg, () => checkFile(2, ImageRenderer));
test(FileTypes.bmp, () => checkFile(3, ImageRenderer));
test(FileTypes.png, () => checkFile(4, ImageRenderer));
test(FileTypes.txt, () => checkFile(5, TXTRenderer));
test(FileTypes.gif, () => checkFile(6, ImageRenderer));
test(FileTypes.jfif, () => checkFile(7, ImageRenderer));
test(FileTypes.pjpeg, () => checkFile(8, ImageRenderer));
test(FileTypes.pjp, () => checkFile(9, ImageRenderer));
test(FileTypes.svg, () => checkFile(10, ImageRenderer));
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
expect(shallow(<FileRenderer {...props} />)).toMatchSnapshot();
});
test('getter for error', () => {
const el = els[0];
Object.keys(ERROR_STATUSES).forEach((status) => {
el.setState({
isLoading: false,
errorStatus: status,
});
const { actions, ...expectedError } = el.instance().error;
expect(ERROR_STATUSES[status]).toEqual(expectedError);
});
});
});
describe('renderer constraints', () => {
els.forEach((el) => {
const file = el.prop('file');
const fileType = getFileType(file.name);
const RendererComponent = RENDERERS[fileType];
const ActualRendererComponent = jest.requireActual(
'components/FilePreview/BaseRenderers',
)[RendererComponent.name];
test(`${fileType} renderer must have onError and onSuccess props`, () => {
/* eslint-disable react/forbid-foreign-prop-types */
expect(ActualRendererComponent.propTypes.onError).toBeDefined();
expect(ActualRendererComponent.propTypes.onSuccess).toBeDefined();
});
test('is not loading, with error', () => {
const hookProps = {
Renderer: () => 'Renderer',
isloading: false,
errorStatus: ErrorStatuses.serverError,
error: { prop: 'hooks.errorProps' },
rendererProps: { prop: 'hooks.rendererProps' },
};
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
expect(shallow(<FileRenderer {...props} />)).toMatchSnapshot();
});
});
});

View File

@@ -6,6 +6,7 @@ exports[`File Preview Card component snapshot 1`] = `
overlay={
<Popover
className="overlay-help-popover"
id="file-popover"
>
<Popover.Content>
<h1>
@@ -20,7 +21,7 @@ exports[`File Preview Card component snapshot 1`] = `
<Button
iconAfter={[MockFunction icons.InfoOutline]}
onClick={[MockFunction this.props.onClick]}
size="small"
size="sm"
variant="tertiary"
>
<FormattedMessage

View File

@@ -1,292 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileRenderer component snapshot has error 404 1`] = `
exports[`FileRenderer component snapshot is not loading, with error 1`] = `
<FileCard
file={
Object {
"description": "file description 0",
"downloadUrl": "/url-path/fake_file_0.pdf",
"name": "fake_file_0.pdf",
"downloadUrl": "file download url",
"name": "filename.txt",
}
}
key="file download url"
>
<ErrorBanner
actions={
Array [
Object {
"id": "retry",
"message": Object {
"defaultMessage": "Retry",
"description": "Retry button for error in file renderer",
"id": "ora-grading.ResponseDisplay.FileRenderer.retryButton",
},
"onClick": [MockFunction this.resetState],
},
]
}
headingMessage={
Object {
"defaultMessage": "File not found",
"description": "File not found error message",
"id": "ora-grading.ResponseDisplay.FileRenderer.fileNotFound",
}
}
>
<FormattedMessage
defaultMessage="File not found"
description="File not found error message"
id="ora-grading.ResponseDisplay.FileRenderer.fileNotFound"
/>
</ErrorBanner>
</FileCard>
`;
exports[`FileRenderer component snapshot has error 500 1`] = `
<FileCard
file={
Object {
"description": "file description 0",
"downloadUrl": "/url-path/fake_file_0.pdf",
"name": "fake_file_0.pdf",
}
}
>
<ErrorBanner
actions={
Array [
Object {
"id": "retry",
"message": Object {
"defaultMessage": "Retry",
"description": "Retry button for error in file renderer",
"id": "ora-grading.ResponseDisplay.FileRenderer.retryButton",
},
"onClick": [MockFunction this.resetState],
},
]
}
headingMessage={
Object {
"defaultMessage": "Unknown errors",
"description": "Unknown errors message",
"id": "ora-grading.ResponseDisplay.FileRenderer.unknownError",
}
}
>
<FormattedMessage
defaultMessage="Unknown errors"
description="Unknown errors message"
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
/>
</ErrorBanner>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering bmp 1`] = `
<FileCard
file={
Object {
"description": "file description 3",
"downloadUrl": "/url-path/fake_file_3.bmp",
"name": "fake_file_3.bmp",
}
}
>
<ImageRenderer
fileName="fake_file_3.bmp"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_3.bmp"
prop="hooks.errorProps"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering gif 1`] = `
exports[`FileRenderer component snapshot isLoading, no Error 1`] = `
<FileCard
file={
Object {
"description": "file description 6",
"downloadUrl": "/url-path/fake_file_6.gif",
"name": "fake_file_6.gif",
"downloadUrl": "file download url",
"name": "filename.txt",
}
}
key="file download url"
>
<ImageRenderer
fileName="fake_file_6.gif"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_6.gif"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering jfif 1`] = `
<FileCard
file={
Object {
"description": "file description 7",
"downloadUrl": "/url-path/fake_file_7.jfif",
"name": "fake_file_7.jfif",
}
}
>
<ImageRenderer
fileName="fake_file_7.jfif"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_7.jfif"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering jpeg 1`] = `
<FileCard
file={
Object {
"description": "file description 2",
"downloadUrl": "/url-path/fake_file_2.jpeg",
"name": "fake_file_2.jpeg",
}
}
>
<ImageRenderer
fileName="fake_file_2.jpeg"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_2.jpeg"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering jpg 1`] = `
<FileCard
file={
Object {
"description": "file description 1",
"downloadUrl": "/url-path/fake_file_1.jpg",
"name": "fake_file_1.jpg",
}
}
>
<ImageRenderer
fileName="fake_file_1.jpg"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_1.jpg"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering pdf 1`] = `
<FileCard
file={
Object {
"description": "file description 0",
"downloadUrl": "/url-path/fake_file_0.pdf",
"name": "fake_file_0.pdf",
}
}
>
<PDFRenderer
fileName="fake_file_0.pdf"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_0.pdf"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering pjp 1`] = `
<FileCard
file={
Object {
"description": "file description 9",
"downloadUrl": "/url-path/fake_file_9.pjp",
"name": "fake_file_9.pjp",
}
}
>
<ImageRenderer
fileName="fake_file_9.pjp"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_9.pjp"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering pjpeg 1`] = `
<FileCard
file={
Object {
"description": "file description 8",
"downloadUrl": "/url-path/fake_file_8.pjpeg",
"name": "fake_file_8.pjpeg",
}
}
>
<ImageRenderer
fileName="fake_file_8.pjpeg"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_8.pjpeg"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering png 1`] = `
<FileCard
file={
Object {
"description": "file description 4",
"downloadUrl": "/url-path/fake_file_4.png",
"name": "fake_file_4.png",
}
}
>
<ImageRenderer
fileName="fake_file_4.png"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_4.png"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering svg 1`] = `
<FileCard
file={
Object {
"description": "file description 10",
"downloadUrl": "/url-path/fake_file_10.svg",
"name": "fake_file_10.svg",
}
}
>
<ImageRenderer
fileName="fake_file_10.svg"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_10.svg"
/>
</FileCard>
`;
exports[`FileRenderer component snapshot successful rendering txt 1`] = `
<FileCard
file={
Object {
"description": "file description 5",
"downloadUrl": "/url-path/fake_file_5.txt",
"name": "fake_file_5.txt",
}
}
>
<TXTRenderer
fileName="fake_file_5.txt"
onError={[MockFunction this.props.onError]}
onSuccess={[MockFunction this.props.onSuccess]}
url="/url-path/fake_file_5.txt"
<Renderer
prop="hooks.rendererProps"
/>
</FileCard>
`;

View File

@@ -0,0 +1,102 @@
import React from 'react';
import { StrictDict } from 'utils';
import { ErrorStatuses } from 'data/constants/requests';
import { FileTypes } from 'data/constants/files';
import {
PDFRenderer,
ImageRenderer,
TXTRenderer,
} from 'components/FilePreview/BaseRenderers';
import * as module from './hooks';
import messages from './messages';
/**
* Config data
*/
export const RENDERERS = StrictDict({
[FileTypes.pdf]: PDFRenderer,
[FileTypes.jpg]: ImageRenderer,
[FileTypes.jpeg]: ImageRenderer,
[FileTypes.bmp]: ImageRenderer,
[FileTypes.png]: ImageRenderer,
[FileTypes.txt]: TXTRenderer,
[FileTypes.gif]: ImageRenderer,
[FileTypes.jfif]: ImageRenderer,
[FileTypes.pjpeg]: ImageRenderer,
[FileTypes.pjp]: ImageRenderer,
[FileTypes.svg]: ImageRenderer,
});
export const SUPPORTED_TYPES = Object.keys(RENDERERS);
export const ERROR_STATUSES = {
[ErrorStatuses.notFound]: messages.fileNotFoundError,
[ErrorStatuses.serverError]: messages.unknownError,
};
/**
* State hooks
*/
export const state = StrictDict({
errorStatus: (val) => React.useState(val),
isLoading: (val) => React.useState(val),
});
/**
* Util methods and transforms
*/
export const getFileType = (fileName) => fileName.split('.').pop()?.toLowerCase();
export const isSupported = (file) => module.SUPPORTED_TYPES.includes(
module.getFileType(file.name),
);
/**
* component hooks
*/
export const renderHooks = ({
file,
intl,
}) => {
const [errorStatus, setErrorStatus] = module.state.errorStatus(null);
const [isLoading, setIsLoading] = module.state.isLoading(true);
const setState = (newState) => {
setErrorStatus(newState.errorStatus);
setIsLoading(newState.isLoading);
};
const stopLoading = (status = null) => setState({ isLoading: false, errorStatus: status });
const errorMessage = (
module.ERROR_STATUSES[errorStatus] || module.ERROR_STATUSES[ErrorStatuses.serverError]
);
const errorAction = {
id: 'retry',
onClick: () => setState({ errorStatus: null, isLoading: true }),
message: messages.retryButton,
};
const error = {
headerMessage: errorMessage,
children: intl.formatMessage(errorMessage),
actions: [errorAction],
};
const Renderer = module.RENDERERS[module.getFileType(file.name)];
const rendererProps = {
fileName: file.name,
url: file.downloadUrl,
onError: stopLoading,
onSuccess: () => stopLoading(),
};
return {
errorStatus,
isLoading,
error,
Renderer,
rendererProps,
};
};

View File

@@ -0,0 +1,117 @@
import { MockUseState, formatMessage } from 'testUtils';
import { keyStore } from 'utils';
import { ErrorStatuses } from 'data/constants/requests';
import * as hooks from './hooks';
const testValue = 'Test-Value';
const state = new MockUseState(hooks);
const hookKeys = keyStore(hooks);
let hook;
describe('FilePreview hooks', () => {
describe('state hooks', () => {
});
describe('non-state hooks', () => {
beforeEach(() => {
state.mock();
});
afterEach(() => {
state.restore();
});
describe('utility methods', () => {
describe('getFileType', () => {
it('returns file extension if available, in lowercase', () => {
expect(hooks.getFileType('thing.TXT')).toEqual('txt');
expect(hooks.getFileType(testValue)).toEqual(testValue.toLowerCase());
});
});
describe('isSupported', () => {
it('returns true iff the filetype is included in SUPPORTED_TYPES', () => {
let spy = jest.spyOn(hooks, hookKeys.getFileType).mockImplementationOnce(v => v);
expect(hooks.isSupported({ name: hooks.SUPPORTED_TYPES[0] })).toEqual(true);
spy = jest.spyOn(hooks, hookKeys.getFileType).mockImplementationOnce(v => v);
expect(hooks.isSupported({ name: testValue })).toEqual(false);
spy.mockRestore();
});
});
});
describe('component hooks', () => {
describe('renderHooks', () => {
const file = {
name: 'test-file-name.txt',
downloadUrl: 'my-test-download-url.jpg',
};
beforeEach(() => {
hook = hooks.renderHooks({ intl: { formatMessage }, file });
});
describe('returned object', () => {
test('errorStatus and isLoading tied to state, initialized to null and true', () => {
expect(hook.errorStatus).toEqual(state.stateVals.errorStatus);
expect(hook.errorStatus).toEqual(null);
expect(hook.isLoading).toEqual(state.stateVals.isLoading);
expect(hook.isLoading).toEqual(true);
});
describe('error', () => {
it('loads message from current error status, if valid, else from serverError', () => {
expect(hook.error.headerMessage).toEqual(
hooks.ERROR_STATUSES[ErrorStatuses.serverError],
);
expect(hook.error.children).toEqual(
formatMessage(hooks.ERROR_STATUSES[ErrorStatuses.serverError]),
);
state.mockVal(state.keys.errorStatus, ErrorStatuses.notFound);
hook = hooks.renderHooks({ intl: { formatMessage }, file });
expect(hook.error.headerMessage).toEqual(
hooks.ERROR_STATUSES[ErrorStatuses.notFound],
);
expect(hook.error.children).toEqual(
formatMessage(hooks.ERROR_STATUSES[ErrorStatuses.notFound]),
);
});
it('provides a single action', () => {
expect(hook.error.actions.length).toEqual(1);
});
describe('action', () => {
it('sets errorState to null and isLoading to true on click', () => {
hook.error.actions[0].onClick();
expect(state.setState.isLoading).toHaveBeenCalledWith(true);
expect(state.setState.errorStatus).toHaveBeenCalledWith(null);
});
});
});
describe('Renderer', () => {
it('returns configured renderer based on filetype', () => {
hooks.SUPPORTED_TYPES.forEach(type => {
jest.spyOn(hooks, hookKeys.getFileType).mockReturnValueOnce(type);
hook = hooks.renderHooks({ intl: { formatMessage }, file });
expect(hook.Renderer).toEqual(hooks.RENDERERS[type]);
});
});
});
describe('rendererProps', () => {
it('forwards url and fileName from file', () => {
expect(hook.rendererProps.fileName).toEqual(file.name);
expect(hook.rendererProps.url).toEqual(file.downloadUrl);
});
describe('onError', () => {
it('it sets isLoading to false and loads errorStatus', () => {
hook.rendererProps.onError(testValue);
expect(state.setState.isLoading).toHaveBeenCalledWith(false);
expect(state.setState.errorStatus).toHaveBeenCalledWith(testValue);
});
});
describe('onSuccess', () => {
it('it sets isLoading to false and errorStatus to null', () => {
hook.rendererProps.onSuccess(testValue);
expect(state.setState.isLoading).toHaveBeenCalledWith(false);
expect(state.setState.errorStatus).toHaveBeenCalledWith(null);
});
});
});
});
});
});
});
});

View File

@@ -1 +1,2 @@
export { default as FileRenderer, isSupported } from './FileRenderer';
export { default as FileRenderer } from './FileRenderer';
export { isSupported } from './hooks';

View File

@@ -0,0 +1,14 @@
// 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

@@ -0,0 +1,20 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const Head = () => {
const { formatMessage } = useIntl();
return (
<Helmet>
<title>
{formatMessage(messages.PageTitle, { siteName: getConfig().SITE_NAME })}
</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
);
};
export default Head;

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { shallow } from 'enzyme';
import Head from '.';
jest.mock('react-helmet', () => ({
Helmet: 'Helmet',
}));
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
SITE_NAME: 'site-name',
FAVICON_URL: 'favicon-url',
}),
}));
describe('Head', () => {
it('snapshot', () => {
const el = shallow(<Head />);
expect(el).toMatchSnapshot();
expect(el.find('title').text()).toContain(getConfig().SITE_NAME);
expect(el.find('link').prop('href')).toEqual(getConfig().FAVICON_URL);
});
});

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
PageTitle: {
id: 'PageTitle',
defaultMessage: 'ORA staff grading | {siteName}',
description: 'Title tag',
},
});
export default messages;

View File

@@ -6,6 +6,7 @@ exports[`Info Popover Component snapshot 1`] = `
overlay={
<Popover
className="overlay-help-popover"
id="info-popover"
>
<Popover.Content>
<div>

View File

@@ -10,6 +10,8 @@ import {
import { InfoOutline } from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { nullMethod } from 'hooks';
import messages from './messages';
/**
@@ -21,7 +23,7 @@ export const InfoPopover = ({ onClick, children, intl }) => (
placement="right-end"
flip
overlay={(
<Popover className="overlay-help-popover">
<Popover id="info-popover" className="overlay-help-popover">
<Popover.Content>{children}</Popover.Content>
</Popover>
)}
@@ -37,7 +39,7 @@ export const InfoPopover = ({ onClick, children, intl }) => (
);
InfoPopover.defaultProps = {
onClick: () => {},
onClick: nullMethod,
};
InfoPopover.propTypes = {
onClick: PropTypes.func,

View File

@@ -4,21 +4,29 @@ import PropTypes from 'prop-types';
import { Badge } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import { gradingStatuses as statuses } from 'data/services/lms/constants';
import messages from 'data/services/lms/messages';
export const statusVariants = {
[statuses.ungraded]: 'primary',
[statuses.locked]: 'light',
[statuses.graded]: 'success',
[statuses.inProgress]: 'warning',
};
export const buttonVariants = StrictDict({
primary: 'primary',
light: 'light',
success: 'success',
warning: 'warning',
});
export const statusVariants = StrictDict({
[statuses.ungraded]: buttonVariants.primary,
[statuses.locked]: buttonVariants.light,
[statuses.graded]: buttonVariants.success,
[statuses.inProgress]: buttonVariants.warning,
});
/**
* <StatusBadge />
*/
export const StatusBadge = ({ className, status }) => {
if (statusVariants[status] === undefined) {
if (!Object.keys(statusVariants).includes(status)) {
return null;
}
return (

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { shallow } from 'enzyme';
import { gradingStatuses } from 'data/services/lms/constants';
import { StatusBadge } from './StatusBadge';
const className = 'test-className';
describe('StatusBadge component', () => {
const render = (status) => shallow(<StatusBadge className={className} status={status} />);
describe('behavior', () => {
it('does not render if status does not have configured variant', () => {
const el = render('arbitrary');
expect(el).toMatchSnapshot();
expect(el.isEmptyRender()).toEqual(true);
});
describe('status snapshots: loads badge with configured variant and message.', () => {
test('`ungraded` shows primary button variant and message', () => {
const el = render(gradingStatuses.ungraded);
expect(el).toMatchSnapshot();
});
test('`locked` shows light button variant and message', () => {
const el = render(gradingStatuses.locked);
expect(el).toMatchSnapshot();
});
test('`graded` shows success button variant and message', () => {
const el = render(gradingStatuses.graded);
expect(el).toMatchSnapshot();
});
test('`inProgress` shows warning button variant and message', () => {
const el = render(gradingStatuses.inProgress);
expect(el).toMatchSnapshot();
});
});
});
});

View File

@@ -20,7 +20,7 @@ exports[`ConfirmModal snapshot: closed 1`] = `
</ActionRow>
}
isOpen={false}
onClose={[Function]}
onClose={[MockFunction hooks.nullMethod]}
title="test-title"
>
<p>
@@ -49,7 +49,7 @@ exports[`ConfirmModal snapshot: open 1`] = `
</ActionRow>
}
isOpen={true}
onClose={[Function]}
onClose={[MockFunction hooks.nullMethod]}
title="test-title"
>
<p>

View File

@@ -0,0 +1,55 @@
// 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,16 +0,0 @@
const configuration = {
// BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
// LOGIN_URL: process.env.LOGIN_URL,
// LOGOUT_URL: process.env.LOGOUT_URL,
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
// REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
// DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
// SECURE_COOKIES: process.env.NODE_ENV !== 'development',
// SEGMENT_KEY: process.env.SEGMENT_KEY,
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
};
const features = {};
export { configuration, features };

View File

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

@@ -0,0 +1,31 @@
// 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

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

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

@@ -163,7 +163,7 @@ describe('Criterion Feedback', () => {
});
describe('mapStateToProps', () => {
const testState = { abitaryState: 'some data' };
const testState = { arbitraryState: 'some data' };
const ownProps = { orderNum: props.orderNum };
let mapped;
beforeEach(() => {

View File

@@ -33,27 +33,25 @@ export class RadioCriterion extends React.Component {
isInvalid,
} = this.props;
return (
<>
<Form.RadioSet name={config.name} value={data}>
{config.options.map((option) => (
<Form.Radio
className="criteria-option"
key={option.name}
value={option.name}
description={intl.formatMessage(messages.optionPoints, { points: option.points })}
onChange={this.onChange}
disabled={!isGrading}
>
{option.label}
</Form.Radio>
))}
{isInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
{intl.formatMessage(messages.rubricSelectedError)}
</Form.Control.Feedback>
)}
</Form.RadioSet>
</>
<Form.RadioSet name={config.name} value={data}>
{config.options.map((option) => (
<Form.Radio
className="criteria-option"
key={option.name}
value={option.name}
description={intl.formatMessage(messages.optionPoints, { points: option.points })}
onChange={this.onChange}
disabled={!isGrading}
>
{option.label}
</Form.Radio>
))}
{isInvalid && (
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
{intl.formatMessage(messages.rubricSelectedError)}
</Form.Control.Feedback>
)}
</Form.RadioSet>
);
}
}

View File

@@ -40,14 +40,14 @@ describe('Radio Criterion Container', () => {
feedback: 'feedback mock',
options: [
{
explanation: 'explaination',
explanation: 'explanation',
feedback: 'option feedback',
label: 'this label',
name: 'option name',
points: 1,
},
{
explanation: 'explaination 2',
explanation: 'explanation 2',
feedback: 'option feedback 2',
label: 'this label 2',
name: 'option name 2',
@@ -129,7 +129,7 @@ describe('Radio Criterion Container', () => {
});
describe('mapStateToProps', () => {
const testState = { arbitary: 'some data' };
const testState = { arbitrary: 'some data' };
const ownProps = { orderNum: props.orderNum };
let mapped;
beforeEach(() => {

View File

@@ -29,14 +29,14 @@ describe('Review Crition Container', () => {
feedback: 'feedback mock',
options: [
{
explanation: 'explaination',
explanation: 'explanation',
feedback: 'option feedback',
label: 'this label',
name: 'option name',
points: 1,
},
{
explanation: 'explaination 2',
explanation: 'explanation 2',
feedback: 'option feedback 2',
label: 'this label 2',
name: 'option name 2',
@@ -78,7 +78,7 @@ describe('Review Crition Container', () => {
});
describe('mapStateToProps', () => {
const testState = { arbitary: 'some data' };
const testState = { arbitrary: 'some data' };
const ownProps = { orderNum: props.orderNum };
let mapped;
beforeEach(() => {

View File

@@ -1,91 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Radio Criterion Container snapshot is grading 1`] = `
<React.Fragment>
<Form.RadioSet
name="random name"
value="selected radio option"
<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"
>
<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>
</React.Fragment>
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`] = `
<React.Fragment>
<Form.RadioSet
name="random name"
value="selected radio option"
<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"
>
<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>
</React.Fragment>
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`] = `
<React.Fragment>
<Form.RadioSet
name="random name"
value="selected radio option"
<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"
>
<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>
</React.Fragment>
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

@@ -19,7 +19,7 @@ exports[`Criterion Container snapshot is graded and is not grading 1`] = `
this label
</strong>
<br />
explaination
explanation
</div>
<div
className="help-popover-option"
@@ -29,7 +29,7 @@ exports[`Criterion Container snapshot is graded and is not grading 1`] = `
this label 2
</strong>
<br />
explaination 2
explanation 2
</div>
</InfoPopover>
</Form.Label>
@@ -67,7 +67,7 @@ exports[`Criterion Container snapshot is ungraded and is grading 1`] = `
this label
</strong>
<br />
explaination
explanation
</div>
<div
className="help-popover-option"
@@ -77,7 +77,7 @@ exports[`Criterion Container snapshot is ungraded and is grading 1`] = `
this label 2
</strong>
<br />
explaination 2
explanation 2
</div>
</InfoPopover>
</Form.Label>
@@ -115,7 +115,7 @@ exports[`Criterion Container snapshot is ungraded and is not grading 1`] = `
this label
</strong>
<br />
explaination
explanation
</div>
<div
className="help-popover-option"
@@ -125,7 +125,7 @@ exports[`Criterion Container snapshot is ungraded and is not grading 1`] = `
this label 2
</strong>
<br />
explaination 2
explanation 2
</div>
</InfoPopover>
</Form.Label>

View File

@@ -34,14 +34,14 @@ describe('Criterion Container', () => {
feedback: 'feedback mock',
options: [
{
explanation: 'explaination',
explanation: 'explanation',
feedback: 'option feedback',
label: 'this label',
name: 'option name',
points: 2,
},
{
explanation: 'explaination 2',
explanation: 'explanation 2',
feedback: 'option feedback 2',
label: 'this label 2',
name: 'option name 2',
@@ -89,16 +89,16 @@ describe('Criterion Container', () => {
});
test('is ungraded and is grading (Radio criterion get render)', () => {
const rubricCritera = el.find('.rubric-criteria');
expect(rubricCritera.children(0).name()).toEqual('RadioCriterion');
const rubricCriteria = el.find('.rubric-criteria');
expect(rubricCriteria.children(0).name()).toEqual('RadioCriterion');
});
test('is ungraded and is not grading (Review criterion get render)', () => {
el.setProps({
isGrading: false,
});
const rubricCritera = el.find('.rubric-criteria');
expect(rubricCritera.children(0).name()).toEqual('ReviewCriterion');
const rubricCriteria = el.find('.rubric-criteria');
expect(rubricCriteria.children(0).name()).toEqual('ReviewCriterion');
});
test('is graded and is not grading (Radio criterion get render)', () => {
@@ -106,13 +106,13 @@ describe('Criterion Container', () => {
isGrading: false,
gradeStatus: gradeStatuses.graded,
});
const rubricCritera = el.find('.rubric-criteria');
expect(rubricCritera.children(0).name()).toEqual('RadioCriterion');
const rubricCriteria = el.find('.rubric-criteria');
expect(rubricCriteria.children(0).name()).toEqual('RadioCriterion');
});
});
describe('mapStateToProps', () => {
const testState = { abitaryState: 'some data' };
const testState = { arbitraryState: 'some data' };
const ownProps = { orderNum: props.orderNum };
let mapped;
beforeEach(() => {

View File

@@ -14,7 +14,7 @@ const messages = defineMessages({
optional: {
id: 'ora-grading.CriterionFeedback.optional',
defaultMessage: '(Optional)',
description: 'addtional label for optional feedback field',
description: 'additional label for optional feedback field',
},
optionPoints: {
id: 'ora-grading.RadioCriterion.optionPoints',

View File

@@ -9,7 +9,7 @@ export const filterHooks = () => {
if (!setAllFilters || !state.filters) {
return {};
}
const clearFilters = React.useCallback(() => setAllFilters([]), []);
const clearFilters = React.useCallback(() => setAllFilters([]), [setAllFilters]);
const headerMap = headers.reduce(
(obj, cur) => ({ ...obj, [cur.id]: cur.Header }),
{},

View File

@@ -34,7 +34,7 @@ describe('FilterStatusComponent hooks', () => {
it('uses React.useCallback to clear filters, only once', () => {
mockTableContext(context);
const { cb, prereqs } = module.filterHooks().clearFilters.useCallback;
expect(prereqs).toEqual([]);
expect(prereqs).toEqual([context.setAllFilters]);
expect(context.setAllFilters).not.toHaveBeenCalled();
cb();
expect(context.setAllFilters).toHaveBeenCalledWith([]);

View File

@@ -19,3 +19,10 @@ span.pgn__icon.breadcrumb-arrow {
}
}
.submissions-table {
.pgn__data-table-filters-breakout-filter {
.pgn__form-group {
margin-bottom: 0;
}
}
}

View File

@@ -22,7 +22,7 @@ export const ListViewBreadcrumb = ({ courseId, oraName }) => (
</Hyperlink>
<p className="py-4">
<span className="h3">{oraName}</span>
<Hyperlink className="align-middle" destination={urls.ora(courseId, locationId)}>
<Hyperlink className="align-middle" destination={urls.ora(courseId, locationId())}>
<Icon src={Launch} className="d-inline-block" />
</Hyperlink>
</p>

View File

@@ -50,7 +50,7 @@ describe('ListViewBreadcrumb component', () => {
test('ora destination', () => {
expect(
el.find(Hyperlink).at(1).props().destination,
).toEqual(urls.ora(props.courseId, constants.locationId));
).toEqual(urls.ora(props.courseId, constants.locationId()));
});
});
describe('mapStateToProps', () => {

View File

@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';
export const SelectedBulkAction = ({ selectedFlatRows, handleClick }) => (
<Button
onClick={handleClick(selectedFlatRows)}
variant="primary"
className="view-selected-responses-btn"
>
<FormattedMessage {...messages.viewSelectedResponses} values={{ value: selectedFlatRows.length }} />
</Button>
);
SelectedBulkAction.defaultProps = {
selectedFlatRows: [],
};
SelectedBulkAction.propTypes = {
selectedFlatRows: PropTypes.arrayOf(PropTypes.object),
handleClick: PropTypes.func.isRequired,
};
export default SelectedBulkAction;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { shallow } from 'enzyme';
import { SelectedBulkAction } from './SelectedBulkAction';
describe('SelectedBulkAction component', () => {
const props = {
selectedFlatRows: [{ id: 1 }, { id: 2 }],
handleClick: jest.fn(),
};
test('snapshots', () => {
const el = shallow(<SelectedBulkAction {...props} handleClick={() => jest.fn()} />);
expect(el).toMatchSnapshot();
});
test('handleClick', () => {
shallow(<SelectedBulkAction {...props} />);
expect(props.handleClick).toHaveBeenCalledWith(props.selectedFlatRows);
});
});

View File

@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import moment from 'moment';
import {
DataTable,
@@ -16,6 +17,8 @@ import { selectors, thunkActions } from 'data/redux';
import StatusBadge from 'components/StatusBadge';
import FilterStatusComponent from './FilterStatusComponent';
import TableAction from './TableAction';
import SelectedBulkAction from './SelectedBulkAction';
import messages from './messages';
@@ -23,12 +26,6 @@ import messages from './messages';
* <SubmissionsTable />
*/
export class SubmissionsTable extends React.Component {
constructor(props) {
super(props);
this.handleViewAllResponsesClick = this.handleViewAllResponsesClick.bind(this);
this.selectedBulkAction = this.selectedBulkAction.bind(this);
}
get gradeStatusOptions() {
return Object.keys(gradingStatuses).map(statusKey => ({
name: this.translate(lmsMessages[gradingStatuses[statusKey]]),
@@ -53,9 +50,9 @@ export class SubmissionsTable extends React.Component {
}
formatDate = ({ value }) => {
const date = new Date(value);
const date = new Date(moment(value));
return date.toLocaleString();
}
};
formatGrade = ({ value: score }) => (
score === null ? '-' : `${score.pointsEarned}/${score.pointsPossible}`
@@ -65,82 +62,66 @@ export class SubmissionsTable extends React.Component {
translate = (...args) => this.props.intl.formatMessage(...args);
handleViewAllResponsesClick(data) {
const getsubmissionUUID = (row) => row.original.submissionUUID;
const rows = data.selectedRows.length ? data.selectedRows : data.tableInstance.rows;
this.props.loadSelectionForReview(rows.map(getsubmissionUUID));
}
selectedBulkAction(selectedFlatRows) {
return {
buttonText: this.translate(
messages.viewSelectedResponses,
{ value: selectedFlatRows.length },
),
className: 'view-selected-responses-btn',
handleClick: this.handleViewAllResponsesClick,
variant: 'primary',
};
}
handleViewAllResponsesClick = (data) => () => {
const getSubmissionUUID = (row) => row.original.submissionUUID;
this.props.loadSelectionForReview(data.map(getSubmissionUUID));
};
render() {
if (!this.props.listData.length) {
return null;
}
return (
<DataTable
isFilterable
FilterStatusComponent={FilterStatusComponent}
numBreakoutFilters={2}
defaultColumnValues={{ Filter: TextFilter }}
isSelectable
isSortable
isPaginated
itemCount={this.props.listData.length}
initialState={{ pageSize: 10, pageIndex: 0 }}
data={this.props.listData}
tableActions={[
{
buttonText: this.translate(messages.viewAllResponses),
handleClick: this.handleViewAllResponsesClick,
className: 'view-all-responses-btn',
variant: 'primary',
},
]}
bulkActions={[
this.selectedBulkAction,
]}
columns={[
{
Header: this.userLabel,
accessor: this.userAccessor,
},
{
Header: this.dateSubmittedLabel,
accessor: submissionFields.dateSubmitted,
Cell: this.formatDate,
disableFilters: true,
},
{
Header: this.translate(messages.grade),
accessor: submissionFields.score,
Cell: this.formatGrade,
disableFilters: true,
},
{
Header: this.translate(messages.gradingStatus),
accessor: submissionFields.gradingStatus,
Cell: this.formatStatus,
Filter: MultiSelectDropdownFilter,
filter: 'includesValue',
filterChoices: this.gradeStatusOptions,
},
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.TableFooter />
</DataTable>
<div className="submissions-table">
<DataTable
isFilterable
FilterStatusComponent={FilterStatusComponent}
numBreakoutFilters={2}
defaultColumnValues={{ Filter: TextFilter }}
isSelectable
isSortable
isPaginated
itemCount={this.props.listData.length}
initialState={{ pageSize: 10, pageIndex: 0 }}
data={this.props.listData}
tableActions={[
<TableAction handleClick={this.handleViewAllResponsesClick} />,
]}
bulkActions={[
<SelectedBulkAction handleClick={this.handleViewAllResponsesClick} />,
]}
columns={[
{
Header: this.userLabel,
accessor: this.userAccessor,
},
{
Header: this.dateSubmittedLabel,
accessor: submissionFields.dateSubmitted,
Cell: this.formatDate,
disableFilters: true,
},
{
Header: this.translate(messages.grade),
accessor: submissionFields.score,
Cell: this.formatGrade,
disableFilters: true,
},
{
Header: this.translate(messages.gradingStatus),
accessor: submissionFields.gradingStatus,
Cell: this.formatStatus,
Filter: MultiSelectDropdownFilter,
filter: 'includesValue',
filterChoices: this.gradeStatusOptions,
},
]}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.TableFooter />
</DataTable>
</div>
);
}
}

View File

@@ -20,6 +20,8 @@ import {
} from './SubmissionsTable';
jest.mock('./FilterStatusComponent', () => jest.fn().mockName('FilterStatusComponent'));
jest.mock('./TableAction', () => jest.fn().mockName('TableAction'));
jest.mock('./SelectedBulkAction', () => jest.fn().mockName('SelectedBulkAction'));
jest.mock('data/redux', () => ({
selectors: {
@@ -43,9 +45,9 @@ let el;
jest.useFakeTimers('modern');
const dates = [
new Date(16131215154955).toLocaleTimeString(),
new Date(16131225154955).toLocaleTimeString(),
new Date(16131215250955).toLocaleTimeString(),
'2021-12-08 09:06:15.319213+00:00',
'2021-12-10 18:06:15.319213+00:00',
'2021-12-11 07:06:15.319213+00:00',
];
const individualData = [
@@ -128,7 +130,6 @@ describe('SubmissionsTable component', () => {
describe('snapshots', () => {
beforeEach(() => {
mockMethod('handleViewAllResponsesClick');
mockMethod('selectedBulkAction');
mockMethod('formatDate');
mockMethod('formatGrade');
mockMethod('formatStatus');
@@ -165,9 +166,6 @@ describe('SubmissionsTable component', () => {
['itemCount', 3],
['initialState', { pageSize: 10, pageIndex: 0 }],
])('%s = %p', (key, value) => expect(tableProps[key]).toEqual(value));
test('bulkActions linked to selectedBulkAction', () => {
expect(tableProps.bulkActions).toEqual([el.instance().selectedBulkAction]);
});
describe('individual columns', () => {
let columns;
beforeEach(() => {
@@ -277,41 +275,14 @@ describe('SubmissionsTable component', () => {
});
describe('handleViewAllResponsesClick', () => {
it('calls loadSelectionForReview with submissionUUID from all rows if there are no selectedRows', () => {
const data = {
selectedRows: [
],
tableInstance: {
rows: [
{ original: { submissionUUID: '123' } },
{ original: { submissionUUID: '456' } },
{ original: { submissionUUID: '789' } },
],
},
};
el.instance().handleViewAllResponsesClick(data);
const data = [
{ original: { submissionUUID: '123' } },
{ original: { submissionUUID: '456' } },
{ original: { submissionUUID: '789' } },
];
el.instance().handleViewAllResponsesClick(data)();
expect(el.instance().props.loadSelectionForReview).toHaveBeenCalledWith(['123', '456', '789']);
});
it('calls loadSelectionForReview with submissionUUID from selected rows if there are any', () => {
const data = {
selectedRows: [
{ original: { submissionUUID: '123' } },
{ original: { submissionUUID: '456' } },
{ original: { submissionUUID: '789' } },
],
};
el.instance().handleViewAllResponsesClick(data);
expect(
el.instance().props.loadSelectionForReview,
).toHaveBeenCalledWith(['123', '456', '789']);
});
});
describe('selectedBulkAction', () => {
it('includes selection length and triggers handleViewAllResponsesClick', () => {
const rows = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const action = el.instance().selectedBulkAction(rows);
expect(action.buttonText).toEqual(expect.stringContaining(rows.length.toString()));
expect(action.handleClick).toEqual(el.instance().handleViewAllResponsesClick);
});
});
});
});

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './messages';
export const TableAction = ({ tableInstance, handleClick }) => (
<Button
onClick={handleClick(tableInstance.rows)}
variant="primary"
className="view-all-responses-btn"
>
<FormattedMessage {...messages.viewAllResponses} />
</Button>
);
TableAction.defaultProps = {
tableInstance: {
rows: [],
},
};
TableAction.propTypes = {
tableInstance: PropTypes.shape({
rows: PropTypes.arrayOf(PropTypes.object),
}),
handleClick: PropTypes.func.isRequired,
};
export default TableAction;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { shallow } from 'enzyme';
import { TableAction } from './TableAction';
describe('TableAction component', () => {
const props = {
tableInstance: { rows: [{ id: 1 }, { id: 2 }] },
handleClick: jest.fn(),
};
test('snapshots', () => {
const el = shallow(<TableAction {...props} handleClick={() => jest.fn()} />);
expect(el).toMatchSnapshot();
});
test('handleClick', () => {
shallow(<TableAction {...props} />);
expect(props.handleClick).toHaveBeenCalledWith(props.tableInstance.rows);
});
});

View File

@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectedBulkAction component snapshots 1`] = `
<Button
className="view-selected-responses-btn"
onClick={[MockFunction]}
variant="primary"
>
<FormattedMessage
defaultMessage="View selected responses ({value})"
description="Button text to load selected responses for review/grading"
id="ora-grading.ListView.viewSelectedResponses"
values={
Object {
"value": 2,
}
}
/>
</Button>
`;

View File

@@ -3,237 +3,243 @@
exports[`SubmissionsTable component component render tests snapshots snapshot: empty (no list data) 1`] = `""`;
exports[`SubmissionsTable component component render tests snapshots snapshot: happy path 1`] = `
<DataTable
FilterStatusComponent={[MockFunction FilterStatusComponent]}
bulkActions={
Array [
[MockFunction this.selectedBulkAction],
]
}
columns={
Array [
Object {
"Header": "Username",
"accessor": "username",
},
Object {
"Cell": [MockFunction this.formatDate],
"Header": "Learner submission date",
"accessor": "dateSubmitted",
"disableFilters": true,
},
Object {
"Cell": [MockFunction this.formatGrade],
"Header": "Grade",
"accessor": "score",
"disableFilters": true,
},
Object {
"Cell": [MockFunction this.formatStatus],
"Filter": "MultiSelectDropdownFilter",
"Header": "Grading status",
"accessor": "gradingStatus",
"filter": "includesValue",
"filterChoices": Array [
Object {
"name": "Ungraded",
"value": "ungraded",
},
Object {
"name": "Grading Completed",
"value": "graded",
},
Object {
"name": "Currently being graded by someone else",
"value": "locked",
},
Object {
"name": "You are currently grading this response",
"value": "in-progress",
},
],
},
]
}
data={
Array [
Object {
"dateSubmitted": "9:05:54 PM",
"gradingStatus": "ungraded",
"score": Object {
"pointsEarned": 1,
"pointsPossible": 10,
},
"username": "username-1",
},
Object {
"dateSubmitted": "11:52:34 PM",
"gradingStatus": "graded",
"score": Object {
"pointsEarned": 2,
"pointsPossible": 10,
},
"username": "username-2",
},
Object {
"dateSubmitted": "9:07:30 PM",
"gradingStatus": "in-progress",
"score": Object {
"pointsEarned": 3,
"pointsPossible": 10,
},
"username": "username-3",
},
]
}
defaultColumnValues={
Object {
"Filter": "TextFilter",
}
}
initialState={
Object {
"pageIndex": 0,
"pageSize": 10,
}
}
isFilterable={true}
isPaginated={true}
isSelectable={true}
isSortable={true}
itemCount={3}
numBreakoutFilters={2}
tableActions={
Array [
Object {
"buttonText": "View all responses",
"className": "view-all-responses-btn",
"handleClick": [MockFunction this.handleViewAllResponsesClick],
"variant": "primary",
},
]
}
<div
className="submissions-table"
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.TableFooter />
</DataTable>
<DataTable
FilterStatusComponent={[MockFunction FilterStatusComponent]}
bulkActions={
Array [
<mockConstructor
handleClick={[MockFunction this.handleViewAllResponsesClick]}
/>,
]
}
columns={
Array [
Object {
"Header": "Username",
"accessor": "username",
},
Object {
"Cell": [MockFunction this.formatDate],
"Header": "Learner submission date",
"accessor": "dateSubmitted",
"disableFilters": true,
},
Object {
"Cell": [MockFunction this.formatGrade],
"Header": "Grade",
"accessor": "score",
"disableFilters": true,
},
Object {
"Cell": [MockFunction this.formatStatus],
"Filter": "MultiSelectDropdownFilter",
"Header": "Grading status",
"accessor": "gradingStatus",
"filter": "includesValue",
"filterChoices": Array [
Object {
"name": "Ungraded",
"value": "ungraded",
},
Object {
"name": "Grading Completed",
"value": "graded",
},
Object {
"name": "Currently being graded by someone else",
"value": "locked",
},
Object {
"name": "You are currently grading this response",
"value": "in-progress",
},
],
},
]
}
data={
Array [
Object {
"dateSubmitted": "2021-12-08 09:06:15.319213+00:00",
"gradingStatus": "ungraded",
"score": Object {
"pointsEarned": 1,
"pointsPossible": 10,
},
"username": "username-1",
},
Object {
"dateSubmitted": "2021-12-10 18:06:15.319213+00:00",
"gradingStatus": "graded",
"score": Object {
"pointsEarned": 2,
"pointsPossible": 10,
},
"username": "username-2",
},
Object {
"dateSubmitted": "2021-12-11 07:06:15.319213+00:00",
"gradingStatus": "in-progress",
"score": Object {
"pointsEarned": 3,
"pointsPossible": 10,
},
"username": "username-3",
},
]
}
defaultColumnValues={
Object {
"Filter": "TextFilter",
}
}
initialState={
Object {
"pageIndex": 0,
"pageSize": 10,
}
}
isFilterable={true}
isPaginated={true}
isSelectable={true}
isSortable={true}
itemCount={3}
numBreakoutFilters={2}
tableActions={
Array [
<mockConstructor
handleClick={[MockFunction this.handleViewAllResponsesClick]}
/>,
]
}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.TableFooter />
</DataTable>
</div>
`;
exports[`SubmissionsTable component component render tests snapshots snapshot: team happy path 1`] = `
<DataTable
FilterStatusComponent={[MockFunction FilterStatusComponent]}
bulkActions={
Array [
[MockFunction this.selectedBulkAction],
]
}
columns={
Array [
Object {
"Header": "Team name",
"accessor": "teamName",
},
Object {
"Cell": [MockFunction this.formatDate],
"Header": "Team submission date",
"accessor": "dateSubmitted",
"disableFilters": true,
},
Object {
"Cell": [MockFunction this.formatGrade],
"Header": "Grade",
"accessor": "score",
"disableFilters": true,
},
Object {
"Cell": [MockFunction this.formatStatus],
"Filter": "MultiSelectDropdownFilter",
"Header": "Grading status",
"accessor": "gradingStatus",
"filter": "includesValue",
"filterChoices": Array [
Object {
"name": "Ungraded",
"value": "ungraded",
},
Object {
"name": "Grading Completed",
"value": "graded",
},
Object {
"name": "Currently being graded by someone else",
"value": "locked",
},
Object {
"name": "You are currently grading this response",
"value": "in-progress",
},
],
},
]
}
data={
Array [
Object {
"dateSubmitted": "9:05:54 PM",
"gradingStatus": "ungraded",
"score": Object {
"pointsEarned": 1,
"pointsPossible": 10,
},
"teamName": "teamName-1",
},
Object {
"dateSubmitted": "11:52:34 PM",
"gradingStatus": "graded",
"score": Object {
"pointsEarned": 2,
"pointsPossible": 10,
},
"teamName": "teamName-2",
},
Object {
"dateSubmitted": "9:07:30 PM",
"gradingStatus": "in-progress",
"score": Object {
"pointsEarned": 3,
"pointsPossible": 10,
},
"teamName": "teamName-3",
},
]
}
defaultColumnValues={
Object {
"Filter": "TextFilter",
}
}
initialState={
Object {
"pageIndex": 0,
"pageSize": 10,
}
}
isFilterable={true}
isPaginated={true}
isSelectable={true}
isSortable={true}
itemCount={3}
numBreakoutFilters={2}
tableActions={
Array [
Object {
"buttonText": "View all responses",
"className": "view-all-responses-btn",
"handleClick": [MockFunction this.handleViewAllResponsesClick],
"variant": "primary",
},
]
}
<div
className="submissions-table"
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.TableFooter />
</DataTable>
<DataTable
FilterStatusComponent={[MockFunction FilterStatusComponent]}
bulkActions={
Array [
<mockConstructor
handleClick={[MockFunction this.handleViewAllResponsesClick]}
/>,
]
}
columns={
Array [
Object {
"Header": "Team name",
"accessor": "teamName",
},
Object {
"Cell": [MockFunction this.formatDate],
"Header": "Team submission date",
"accessor": "dateSubmitted",
"disableFilters": true,
},
Object {
"Cell": [MockFunction this.formatGrade],
"Header": "Grade",
"accessor": "score",
"disableFilters": true,
},
Object {
"Cell": [MockFunction this.formatStatus],
"Filter": "MultiSelectDropdownFilter",
"Header": "Grading status",
"accessor": "gradingStatus",
"filter": "includesValue",
"filterChoices": Array [
Object {
"name": "Ungraded",
"value": "ungraded",
},
Object {
"name": "Grading Completed",
"value": "graded",
},
Object {
"name": "Currently being graded by someone else",
"value": "locked",
},
Object {
"name": "You are currently grading this response",
"value": "in-progress",
},
],
},
]
}
data={
Array [
Object {
"dateSubmitted": "2021-12-08 09:06:15.319213+00:00",
"gradingStatus": "ungraded",
"score": Object {
"pointsEarned": 1,
"pointsPossible": 10,
},
"teamName": "teamName-1",
},
Object {
"dateSubmitted": "2021-12-10 18:06:15.319213+00:00",
"gradingStatus": "graded",
"score": Object {
"pointsEarned": 2,
"pointsPossible": 10,
},
"teamName": "teamName-2",
},
Object {
"dateSubmitted": "2021-12-11 07:06:15.319213+00:00",
"gradingStatus": "in-progress",
"score": Object {
"pointsEarned": 3,
"pointsPossible": 10,
},
"teamName": "teamName-3",
},
]
}
defaultColumnValues={
Object {
"Filter": "TextFilter",
}
}
initialState={
Object {
"pageIndex": 0,
"pageSize": 10,
}
}
isFilterable={true}
isPaginated={true}
isSelectable={true}
isSortable={true}
itemCount={3}
numBreakoutFilters={2}
tableActions={
Array [
<mockConstructor
handleClick={[MockFunction this.handleViewAllResponsesClick]}
/>,
]
}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.TableFooter />
</DataTable>
</div>
`;

View File

@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TableAction component snapshots 1`] = `
<Button
className="view-all-responses-btn"
onClick={[MockFunction]}
variant="primary"
>
<FormattedMessage
defaultMessage="View all responses"
description="Button text to load all responses for review/grading"
id="ora-grading.ListView.viewAllResponses"
/>
</Button>
`;

View File

@@ -1,7 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FileRenderer, isSupported } from 'components/FilePreview';
import { FileRenderer } from 'components/FilePreview';
import { isSupported } from 'components/FilePreview/hooks';
/**
* <PreviewDisplay />

View File

@@ -7,7 +7,6 @@ import { PreviewDisplay } from './PreviewDisplay';
jest.mock('components/FilePreview', () => ({
FileRenderer: () => 'FileRenderer',
isSupported: jest.requireActual('components/FilePreview').isSupported,
}));
describe('PreviewDisplay', () => {

View File

@@ -43,6 +43,11 @@
.preview-display {
padding: map-get($spacers, 3) 0;
}
.response-display-text-content {
white-space: pre-line;
overflow: hidden;
}
}
@include media-breakpoint-down(sm) {

View File

@@ -10,6 +10,7 @@ import messages from './messages';
jest.mock('./components/FileNameCell', () => jest.fn().mockName('FileNameCell'));
jest.mock('./components/FileExtensionCell', () => jest.fn().mockName('FileExtensionCell'));
jest.mock('./components/FilePopoverCell', () => jest.fn().mockName('FilePopoverCell'));
jest.mock('./FileDownload', () => 'FileDownload');
describe('SubmissionFiles', () => {
describe('component', () => {

View File

@@ -43,7 +43,7 @@ exports[`FileDownload component snapshot download is completed 1`] = `
/>,
"pending": <FormattedMessage
defaultMessage="Downloading"
description="Download files penging state label"
description="Download files pending state label"
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
/>,
}
@@ -96,7 +96,7 @@ exports[`FileDownload component snapshot download is failed 1`] = `
/>,
"pending": <FormattedMessage
defaultMessage="Downloading"
description="Download files penging state label"
description="Download files pending state label"
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
/>,
}
@@ -149,7 +149,7 @@ exports[`FileDownload component snapshot download is inactive 1`] = `
/>,
"pending": <FormattedMessage
defaultMessage="Downloading"
description="Download files penging state label"
description="Download files pending state label"
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
/>,
}
@@ -202,7 +202,7 @@ exports[`FileDownload component snapshot download is pending 1`] = `
/>,
"pending": <FormattedMessage
defaultMessage="Downloading"
description="Download files penging state label"
description="Download files pending state label"
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
/>,
}

View File

@@ -95,7 +95,7 @@ exports[`SubmissionFiles component snapshot files existed for props 1`] = `
<Card.Footer
className="text-right"
>
<Connect(FileDownload)
<FileDownload
files={
Array [
Object {

View File

@@ -5,9 +5,11 @@ exports[`ResponseDisplay component snapshot file upload disable with valid respo
className="response-display"
>
<Card>
<Card.Body>
<Card.Section
className="response-display-text-content"
>
parsed html (sanitized (some text response here))
</Card.Body>
</Card.Section>
</Card>
</div>
`;
@@ -62,9 +64,11 @@ exports[`ResponseDisplay component snapshot file upload enable with valid respon
}
/>
<Card>
<Card.Body>
<Card.Section
className="response-display-text-content"
>
parsed html (sanitized (some text response here))
</Card.Body>
</Card.Section>
</Card>
</div>
`;

View File

@@ -48,7 +48,7 @@ export class ResponseDisplay extends React.Component {
/* eslint-disable react/no-array-index-key */
this.textContents.map((textContent, index) => (
<Card key={index}>
<Card.Body>{textContent}</Card.Body>
<Card.Section className="response-display-text-content">{textContent}</Card.Section>
</Card>
))
}

View File

@@ -24,7 +24,7 @@ const messages = defineMessages({
downloading: {
id: 'ora-grading.ResponseDisplay.SubmissionFiles.downloading',
defaultMessage: 'Downloading',
description: 'Download files penging state label',
description: 'Download files pending state label',
},
downloaded: {
id: 'ora-grading.ResponseDisplay.SubmissionFiles.downloaded',

View File

@@ -1,138 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Button } from '@edx/paragon';
import { Cancel, Highlight } from '@edx/paragon/icons';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { selectors, thunkActions } from 'data/redux';
import { RequestKeys } from 'data/constants/requests';
import { gradingStatuses as statuses } from 'data/services/lms/constants';
import StopGradingConfirmModal from './StopGradingConfirmModal';
import OverrideGradeConfirmModal from './OverrideGradeConfirmModal';
import messages from './messages';
export const buttonArgs = {
[statuses.ungraded]: {
label: messages.startGrading,
iconAfter: Highlight,
},
[statuses.graded]: {
label: messages.overrideGrade,
iconAfter: Highlight,
},
[statuses.inProgress]: {
label: messages.stopGrading,
iconAfter: Cancel,
},
};
export class StartGradingButton extends React.Component {
constructor(props) {
super(props);
this.state = {
showConfirmStopGrading: false,
showConfirmOverrideGrade: false,
};
this.showConfirmStopGrading = this.showConfirmStopGrading.bind(this);
this.hideConfirmStopGrading = this.hideConfirmStopGrading.bind(this);
this.showConfirmOverrideGrade = this.showConfirmOverrideGrade.bind(this);
this.hideConfirmOverrideGrade = this.hideConfirmOverrideGrade.bind(this);
this.confirmStopGrading = this.confirmStopGrading.bind(this);
this.confirmOverrideGrade = this.confirmOverrideGrade.bind(this);
this.handleClick = this.handleClick.bind(this);
}
showConfirmStopGrading() {
this.setState({ showConfirmStopGrading: true });
}
hideConfirmStopGrading() {
this.setState({ showConfirmStopGrading: false });
}
showConfirmOverrideGrade() {
this.setState({ showConfirmOverrideGrade: true });
}
hideConfirmOverrideGrade() {
this.setState({ showConfirmOverrideGrade: false });
}
confirmStopGrading() {
this.hideConfirmStopGrading();
this.props.stopGrading();
}
confirmOverrideGrade() {
this.hideConfirmOverrideGrade();
this.props.startGrading();
}
handleClick() {
if (this.props.gradingStatus === statuses.inProgress) {
this.showConfirmStopGrading();
} else if (this.props.gradingStatus === statuses.graded) {
this.showConfirmOverrideGrade();
} else {
this.props.startGrading();
}
}
render() {
const { gradingStatus } = this.props;
if (gradingStatus === statuses.locked) {
return null;
}
const args = buttonArgs[gradingStatus];
return (
<>
<Button
variant="primary"
iconAfter={args.iconAfter}
onClick={this.handleClick}
disabled={this.props.gradeIsPending || this.props.lockIsPending}
>
<FormattedMessage {...args.label} />
</Button>
<OverrideGradeConfirmModal
isOpen={this.state.showConfirmOverrideGrade}
onCancel={this.hideConfirmOverrideGrade}
onConfirm={this.confirmOverrideGrade}
/>
<StopGradingConfirmModal
isOpen={this.state.showConfirmStopGrading}
onCancel={this.hideConfirmStopGrading}
onConfirm={this.confirmStopGrading}
isOverride={this.props.gradeStatus === statuses.graded}
/>
</>
);
}
}
StartGradingButton.propTypes = {
gradeStatus: PropTypes.string.isRequired,
gradingStatus: PropTypes.string.isRequired,
startGrading: PropTypes.func.isRequired,
stopGrading: PropTypes.func.isRequired,
gradeIsPending: PropTypes.bool.isRequired,
lockIsPending: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
gradeIsPending: selectors.requests.isPending(state, { requestKey: RequestKeys.submitGrade }),
lockIsPending: selectors.requests.isPending(state, { requestKey: RequestKeys.setLock }),
gradeStatus: selectors.grading.selected.gradeStatus(state),
gradingStatus: selectors.grading.selected.gradingStatus(state),
});
export const mapDispatchToProps = {
startGrading: thunkActions.grading.startGrading,
stopGrading: thunkActions.grading.cancelGrading,
};
export default connect(mapStateToProps, mapDispatchToProps)(StartGradingButton);

View File

@@ -1,132 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { selectors, thunkActions } from 'data/redux';
import { RequestKeys } from 'data/constants/requests';
import { gradingStatuses as statuses } from 'data/services/lms/constants';
import {
StartGradingButton,
mapStateToProps,
mapDispatchToProps,
} from './StartGradingButton';
jest.mock('data/redux', () => ({
selectors: {
grading: {
selected: {
gradeStatus: (state) => ({ gradeStatus: state }),
gradingStatus: (state) => ({ gradingStatus: state }),
},
},
requests: { isPending: (...args) => ({ isPending: args }) },
},
thunkActions: {
grading: {
startGrading: jest.fn(),
stopGrading: jest.fn(),
},
},
}));
jest.mock('./OverrideGradeConfirmModal', () => 'OverrideGradeConfirmModal');
jest.mock('./StopGradingConfirmModal', () => 'StopGradingConfirmModal');
let el;
describe('StartGradingButton component', () => {
describe('component', () => {
const props = {
gradeIsPending: false,
lockIsPending: false,
};
beforeEach(() => {
props.startGrading = jest.fn().mockName('this.props.startGrading');
props.stopGrading = jest.fn().mockName('this.props.stopGrading');
});
describe('snapshotes', () => {
const mockedEl = (gradingStatus, gradeStatus) => {
const renderedEl = shallow(
<StartGradingButton
{...props}
gradingStatus={gradingStatus}
gradeStatus={gradeStatus || gradingStatus}
/>,
);
const mockMethod = (methodName) => {
renderedEl.instance()[methodName] = jest.fn().mockName(`this.${methodName}`);
};
mockMethod('handleClick');
mockMethod('hideConfirmOverrideGrade');
mockMethod('confirmOverrideGrade');
mockMethod('hideConfirmStopGrading');
mockMethod('confirmStopGrading');
return renderedEl;
};
test('snapshot: locked (null)', () => {
el = mockedEl(statuses.locked);
expect(el.instance().render()).toMatchSnapshot();
expect(el.isEmptyRender()).toEqual(true);
});
test('snapshot: ungraded (startGrading callback)', () => {
expect(mockedEl(statuses.ungraded).instance().render()).toMatchSnapshot();
});
test('snapshot: grade pending (disabled)', () => {
el = mockedEl(statuses.ungraded);
el.setProps({ gradeIsPending: true });
expect(el.instance().render()).toMatchSnapshot();
});
test('snapshot: lock pending (disabled)', () => {
el = mockedEl(statuses.ungraded);
el.setProps({ lockIsPending: true });
expect(el.instance().render()).toMatchSnapshot();
});
test('snapshot: graded, confirmOverride (startGrading callback)', () => {
el = mockedEl(statuses.graded);
el.setState({ showConfirmOverrideGrade: true });
expect(el.instance().render()).toMatchSnapshot();
});
test('snapshot: inProgress, isOverride, confirmStop (stopGrading callback)', () => {
el = mockedEl(statuses.inProgress, statuses.graded);
el.setState({ showConfirmStopGrading: true });
expect(el.instance().render()).toMatchSnapshot();
});
});
});
describe('mapStateToProps', () => {
let mapped;
const testState = { some: 'test-state' };
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('gradeIsPending loads from requests.gradeIsPending(submitGrade)', () => {
expect(mapped.gradeIsPending).toEqual(
selectors.requests.isPending(
testState,
{ requestKey: RequestKeys.submitGrade },
),
);
});
test('lockIsPending loads from requests.lockIsPending(setLock)', () => {
expect(mapped.lockIsPending).toEqual(
selectors.requests.isPending(
testState,
{ requestKey: RequestKeys.setLock },
),
);
});
test('gradeStatus loads from grading.selected.gradeStatus', () => {
expect(mapped.gradeStatus).toEqual(selectors.grading.selected.gradeStatus(testState));
});
test('gradingStatus loads from grading.selected.gradingStatus', () => {
expect(mapped.gradingStatus).toEqual(selectors.grading.selected.gradingStatus(testState));
});
});
describe('mapDispatchToProps', () => {
it('loads startGrading from thunkActions.grading.stargGrading', () => {
expect(mapDispatchToProps.startGrading).toEqual(thunkActions.grading.startGrading);
});
it('loads stopGrading from thunkActions.grading.cancelGrading', () => {
expect(mapDispatchToProps.stopGrading).toEqual(thunkActions.grading.cancelGrading);
});
});
});

View File

@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StartGradingButton component component snapshots hide: renders empty component if hook.hide is true 1`] = `""`;
exports[`StartGradingButton component component snapshots smoke test: forwards props to components from hooks 1`] = `
<Fragment>
<Button
props="hooks.buttonArgs"
variant="primary"
/>
<OverrideGradeConfirmModal
props="hooks.overrideGradeArgs"
/>
<StopGradingConfirmModal
props="hooks.stopGradingArgs"
/>
</Fragment>
`;

View File

@@ -0,0 +1,141 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Cancel, Highlight } from '@edx/paragon/icons';
import { selectors, thunkActions } from 'data/redux';
import { RequestKeys } from 'data/constants/requests';
import { gradingStatuses as statuses } from 'data/services/lms/constants';
import { StrictDict } from 'utils';
import * as module from './hooks';
import messages from './messages';
export const buttonConfig = {
[statuses.ungraded]: {
label: messages.startGrading,
iconAfter: Highlight,
},
[statuses.graded]: {
label: messages.overrideGrade,
iconAfter: Highlight,
},
[statuses.inProgress]: {
label: messages.stopGrading,
iconAfter: Cancel,
},
};
export const state = StrictDict({
showConfirmStopGrading: (val) => React.useState(val),
showConfirmOverrideGrade: (val) => React.useState(val),
});
export const reduxValues = () => ({
gradeStatus: useSelector(selectors.grading.selected.gradeStatus),
gradingStatus: useSelector(selectors.grading.selected.gradingStatus),
gradeIsPending: useSelector((reduxState) => (
selectors.requests.isPending(reduxState, { requestKey: RequestKeys.submitGrade })
)),
lockIsPending: useSelector((reduxState) => (
selectors.requests.isPending(reduxState, { requestKey: RequestKeys.setLock })
)),
});
export const buttonArgs = ({
intl,
dispatch,
overrideGradeState,
stopGradingState,
gradingStatus,
isPending,
}) => ({
iconAfter: module.buttonConfig[gradingStatus].iconAfter,
children: intl.formatMessage(module.buttonConfig[gradingStatus].label),
disabled: isPending,
onClick: () => {
if (gradingStatus === statuses.inProgress) {
stopGradingState.setShow(true);
} else if (gradingStatus === statuses.graded) {
overrideGradeState.setShow(true);
} else {
dispatch(thunkActions.grading.startGrading());
}
},
});
export const overrideGradeArgs = ({
dispatch,
overrideGradeState: { show, setShow },
}) => ({
isOpen: show,
onCancel: () => setShow(false),
onConfirm: () => {
setShow(false);
dispatch(thunkActions.grading.startGrading());
},
});
export const stopGradingArgs = ({
dispatch,
isGraded,
stopGradingState: { show, setShow },
}) => ({
isOpen: show,
onCancel: () => setShow(false),
onConfirm: () => {
setShow(false);
dispatch(thunkActions.grading.cancelGrading());
},
isOverride: isGraded,
});
export const buttonHooks = ({
dispatch,
intl,
}) => {
const showState = {
stopGrading: state.showConfirmStopGrading(false),
overrideGrade: state.showConfirmOverrideGrade(false),
};
const overrideGradeState = {
show: showState.overrideGrade[0],
setShow: showState.overrideGrade[1],
};
const stopGradingState = {
show: showState.stopGrading[0],
setShow: showState.stopGrading[1],
};
const {
gradeStatus,
gradingStatus,
gradeIsPending,
lockIsPending,
} = module.reduxValues();
const hide = gradingStatus === statuses.locked;
if (hide) {
return { hide };
}
return {
hide,
overrideGradeArgs: module.overrideGradeArgs({
dispatch,
overrideGradeState,
}),
stopGradingArgs: module.stopGradingArgs({
dispatch,
stopGradingState,
isGraded: gradeStatus === statuses.graded,
}),
buttonArgs: module.buttonArgs({
intl,
dispatch,
stopGradingState,
overrideGradeState,
isPending: lockIsPending || gradeIsPending,
gradingStatus,
}),
};
};

View File

@@ -0,0 +1,280 @@
import { useSelector } from 'react-redux';
import { formatMessage, MockUseState } from 'testUtils';
import { RequestKeys } from 'data/constants/requests';
import { gradingStatuses } from 'data/services/lms/constants';
import { selectors, thunkActions } from 'data/redux';
import { keyStore } from 'utils';
import * as hooks from './hooks';
jest.mock('react-redux', () => ({
useSelector: args => ({ useSelector: args }),
}));
jest.mock('data/redux', () => ({
selectors: {
grading: {
selected: {
gradeStatus: jest.fn((...args) => ({ gradeStatus: args })),
gradingStatus: jest.fn((...args) => ({ gradingStatus: args })),
},
},
requests: {
isPending: jest.fn((...args) => ({ isPending: args })),
},
},
thunkActions: {
grading: {
startGrading: jest.fn((...args) => ({ startGrading: args })),
cancelGrading: jest.fn((...args) => ({ cancelGrading: args })),
},
},
}));
const state = new MockUseState(hooks);
const hookKeys = keyStore(hooks);
let hook;
const testValue = 'my-test-value';
const dispatch = jest.fn();
const intl = { formatMessage };
describe('Start Grading Button hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('state hooks', () => {
state.testGetter(state.keys.showConfirmStopGrading);
state.testGetter(state.keys.showConfirmOverrideGrade);
});
describe('redux values', () => {
const testState = { my: 'test-state' };
beforeEach(() => {
hook = hooks.reduxValues();
});
test('gradeStatus drawn from selected grade status', () => {
expect(hook.gradeStatus).toEqual(
useSelector(selectors.grading.selected.gradeStatus),
);
});
test('gradingStatus drawn from selected grading status', () => {
expect(hook.gradingStatus).toEqual(
useSelector(selectors.grading.selected.gradingStatus),
);
});
test('gradeIsPending drawn from requests.isPending on submitGrade request', () => {
expect(hook.gradeIsPending.useSelector(testState)).toEqual(
selectors.requests.isPending(testState, { requestKey: RequestKeys.submitGrade }),
);
});
test('lockIsPending drawn from requests.isPending on setLock request', () => {
expect(hook.lockIsPending.useSelector(testState)).toEqual(
selectors.requests.isPending(testState, { requestKey: RequestKeys.setLock }),
);
});
});
describe('non-state hooks', () => {
beforeEach(state.mock);
afterEach(state.restore);
describe('buttonArgs', () => {
const props = {
intl,
dispatch,
overrideGradeState: { setShow: jest.fn() },
stopGradingState: { setShow: jest.fn() },
gradingStatus: gradingStatuses.ungraded,
isPending: false,
};
describe('returned args', () => {
const testStatusConfig = (status) => {
describe(`status config for ${status} submissions`, () => {
beforeEach(() => {
selectors.grading.selected.gradingStatus.mockReturnValue(status);
hook = hooks.buttonArgs({ ...props, gradingStatus: status });
});
it('loads configured iconAfter', () => {
expect(hook.iconAfter).toEqual(hooks.buttonConfig[status].iconAfter);
});
it('loads and formats label from config', () => {
expect(hook.children).toEqual(formatMessage(hooks.buttonConfig[status].label));
});
describe('onClick', () => {
if (status === gradingStatuses.inProgress) {
it('shows the confirm stop-grading modal', () => {
hook.onClick();
expect(props.stopGradingState.setShow).toHaveBeenCalledWith(true);
});
} else if (status === gradingStatuses.graded) {
it('shows the confirm stop-grading modal', () => {
hook.onClick();
expect(props.overrideGradeState.setShow).toHaveBeenCalledWith(true);
});
} else {
it('dispatches the startGrading thunkAction', () => {
hook.onClick();
expect(props.dispatch).toHaveBeenCalledWith(thunkActions.grading.startGrading());
});
}
});
});
};
testStatusConfig(gradingStatuses.ungraded);
testStatusConfig(gradingStatuses.graded);
testStatusConfig(gradingStatuses.inProgress);
});
});
describe('overrideGradeArgs', () => {
const props = { show: 'test-show', setShow: jest.fn() };
beforeEach(() => {
hook = hooks.overrideGradeArgs({ dispatch, overrideGradeState: props });
});
test('isOpen returns the override grade show state', () => {
expect(hook.isOpen).toEqual(props.show);
});
test('onCancel: sets override grade show state to false', () => {
hook.onCancel();
expect(props.setShow).toHaveBeenCalledWith(false);
});
describe('onConfirm', () => {
test('sets override grade show state to false and starts grading', () => {
hook.onConfirm();
expect(props.setShow).toHaveBeenCalledWith(false);
expect(dispatch).toHaveBeenCalledWith(thunkActions.grading.startGrading());
});
});
});
describe('stopGradingArgs', () => {
const props = { show: 'test-show', setShow: jest.fn() };
beforeEach(() => {
hook = hooks.stopGradingArgs({
dispatch,
isGraded: testValue,
stopGradingState: props,
});
});
test('isOpen returns the stop grading show state', () => {
expect(hook.isOpen).toEqual(props.show);
});
test('onCancel: sets stop grading show state to false', () => {
hook.onCancel();
expect(props.setShow).toHaveBeenCalledWith(false);
});
describe('onConfirm', () => {
test('sets stop grading show state to false and cancels grading', () => {
hook.onConfirm();
expect(props.setShow).toHaveBeenCalledWith(false);
expect(dispatch).toHaveBeenCalledWith(thunkActions.grading.cancelGrading());
});
});
test('isOverride is set to isGraded arg', () => {
expect(hook.isOverride).toEqual(testValue);
});
});
describe('button component hooks', () => {
const reduxValues = {
gradeStatus: 'redux-values.gradeStatus',
gradingStatus: 'redux-values.gradingStatus',
gradeIsPending: false,
lockIsPending: false,
};
const mocks = {
buttonArgs: jest.fn(args => ({ buttonArgs: args })),
overrideGradeArgs: jest.fn(args => ({ overrideGradeArgs: args })),
reduxValues: () => reduxValues,
stopGradingArgs: jest.fn(args => ({ stopGradingArgs: args })),
};
let overrideGradeState;
let stopGradingState;
const mockHooks = (values) => {
jest.spyOn(hooks, hookKeys.buttonArgs).mockImplementationOnce(mocks.buttonArgs);
jest.spyOn(hooks, hookKeys.overrideGradeArgs).mockImplementationOnce(mocks.overrideGradeArgs);
jest.spyOn(hooks, hookKeys.reduxValues).mockImplementationOnce(() => ({
...reduxValues,
...values,
}));
jest.spyOn(hooks, hookKeys.stopGradingArgs).mockImplementationOnce(mocks.stopGradingArgs);
};
beforeEach(() => {
mockHooks(reduxValues);
hook = hooks.buttonHooks({ dispatch, intl });
overrideGradeState = {
show: state.stateVals.showConfirmOverrideGrade,
setShow: state.setState.showConfirmOverrideGrade,
};
stopGradingState = {
show: state.stateVals.showConfirmStopGrading,
setShow: state.setState.showConfirmStopGrading,
};
});
describe('behavior', () => {
it('initializes showConfirmStopGrading and showConfirmOverrideGrade to false', () => {
expect(hooks.state.showConfirmStopGrading).toHaveBeenCalledWith(false);
expect(hooks.state.showConfirmOverrideGrade).toHaveBeenCalledWith(false);
});
});
describe('returned object', () => {
test('hide returns true iff gradingStatus is not locked', () => {
expect(hook.hide).toEqual(false);
mockHooks({ gradingStatus: gradingStatuses.locked });
hook = hooks.buttonHooks({ dispatch, intl });
expect(hook.hide).toEqual(true);
});
test('returns only hide hook if locked', () => {
mockHooks({ gradingStatus: gradingStatuses.locked });
hook = hooks.buttonHooks({ dispatch, intl });
expect(hook).toEqual({ hide: true });
});
test('overrideGradeArgs: calls local hook with dispatch and override grade state', () => {
expect(hook.overrideGradeArgs).toEqual(mocks.overrideGradeArgs({
dispatch,
overrideGradeState,
}));
});
describe('stopGradingArgs forwards local hook called with', () => {
test('dispatch, stop grading state, and if submission is graded', () => {
expect(hook.stopGradingArgs).toEqual(mocks.stopGradingArgs({
dispatch,
stopGradingState,
isGraded: false,
}));
mockHooks({ gradeStatus: gradingStatuses.graded });
hook = hooks.buttonHooks({ dispatch, intl });
expect(hook.stopGradingArgs).toEqual(mocks.stopGradingArgs({
dispatch,
stopGradingState,
isGraded: true,
}));
});
});
describe('buttonArgs forwards local hook called with', () => {
test('props, local state, grading status and whether lock or grade are pending', () => {
expect(hook.buttonArgs).toEqual(mocks.buttonArgs({
intl,
dispatch,
stopGradingState,
overrideGradeState,
gradingStatus: reduxValues.gradingStatus,
isPending: false,
}));
[
{ gradeIsPending: true },
{ lockIsPending: true },
{ gradeIsPending: true, lockIsPending: true },
].forEach(values => {
mockHooks(values);
hook = hooks.buttonHooks({ dispatch, intl });
expect(hook.buttonArgs).toEqual(mocks.buttonArgs({
intl,
dispatch,
stopGradingState,
overrideGradeState,
gradingStatus: reduxValues.gradingStatus,
isPending: true,
}));
});
});
});
});
});
});
});

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { Button } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import StopGradingConfirmModal from '../StopGradingConfirmModal';
import OverrideGradeConfirmModal from '../OverrideGradeConfirmModal';
import * as hooks from './hooks';
export const StartGradingButton = ({ intl }) => {
const dispatch = useDispatch();
const {
hide,
buttonArgs,
overrideGradeArgs,
stopGradingArgs,
} = hooks.buttonHooks({ dispatch, intl });
if (hide) {
return null;
}
return (
<>
<Button variant="primary" {...buttonArgs} />
<OverrideGradeConfirmModal {...overrideGradeArgs} />
<StopGradingConfirmModal {...stopGradingArgs} />
</>
);
};
StartGradingButton.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(StartGradingButton);

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { shallow } from 'enzyme';
import { useDispatch } from 'react-redux';
import { formatMessage } from 'testUtils';
import * as hooks from './hooks';
import { StartGradingButton } from '.';
jest.mock('../OverrideGradeConfirmModal', () => 'OverrideGradeConfirmModal');
jest.mock('../StopGradingConfirmModal', () => 'StopGradingConfirmModal');
jest.mock('./hooks', () => ({
buttonHooks: jest.fn(),
}));
const intl = { formatMessage };
let el;
describe('StartGradingButton component', () => {
describe('component', () => {
const dispatch = useDispatch();
const props = { intl };
const buttonHooks = {
hide: false,
buttonArgs: { props: 'hooks.buttonArgs' },
overrideGradeArgs: { props: 'hooks.overrideGradeArgs' },
stopGradingArgs: { props: 'hooks.stopGradingArgs' },
};
describe('behavior', () => {
it('initializes buttonHooks with dispatch and intl fields', () => {
hooks.buttonHooks.mockReturnValueOnce(buttonHooks);
el = shallow(<StartGradingButton {...props} />);
expect(hooks.buttonHooks).toHaveBeenCalledWith({ dispatch, intl });
});
});
describe('snapshots', () => {
test('hide: renders empty component if hook.hide is true', () => {
hooks.buttonHooks.mockReturnValueOnce({ ...buttonHooks, hide: true });
el = shallow(<StartGradingButton {...props} />);
expect(el).toMatchSnapshot();
expect(el.isEmptyRender()).toEqual(true);
});
test('smoke test: forwards props to components from hooks', () => {
hooks.buttonHooks.mockReturnValueOnce(buttonHooks);
el = shallow(<StartGradingButton {...props} />);
expect(el).toMatchSnapshot();
expect(el.isEmptyRender()).toEqual(false);
});
});
});
});

View File

@@ -0,0 +1,21 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
startGrading: {
id: 'ora-grading.ReviewActions.StartGradingButton.startGrading',
defaultMessage: 'Start grading',
description: 'Review pane button text to start grading',
},
overrideGrade: {
id: 'ora-grading.ReviewActions.StartGradingButton.overrideGrade',
defaultMessage: 'Override grade',
description: 'Review pane button text to start grading an already graded submission',
},
stopGrading: {
id: 'ora-grading.ReviewActions.StartGradingButton.stopGrading',
defaultMessage: 'Stop grading this response',
description: 'Review pane button text to stop grading',
},
});
export default messages;

View File

@@ -19,7 +19,7 @@ jest.mock('data/redux/grading/selectors', () => ({
doesExist: (state) => ({ nextDoesExist: state }),
},
activeIndex: (state) => ({ activeIndex: state }),
selectionLength: (state) => ({ selectionlength: state }),
selectionLength: (state) => ({ selectionLength: state }),
}));
jest.mock('data/redux/requests/selectors', () => ({
allowNavigation: (state) => ({ allowNavigation: state }),

View File

@@ -1,143 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StartGradingButton component component snapshotes snapshot: grade pending (disabled) 1`] = `
<React.Fragment>
<Button
disabled={true}
iconAfter={[MockFunction icons.Highlight]}
onClick={[MockFunction this.handleClick]}
variant="primary"
>
<FormattedMessage
defaultMessage="Start grading"
description="Review pane button text to start grading"
id="ora-grading.ReviewActions.StartGradingButton.startGrading"
/>
</Button>
<OverrideGradeConfirmModal
isOpen={false}
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
onConfirm={[MockFunction this.confirmOverrideGrade]}
/>
<StopGradingConfirmModal
isOpen={false}
isOverride={false}
onCancel={[MockFunction this.hideConfirmStopGrading]}
onConfirm={[MockFunction this.confirmStopGrading]}
/>
</React.Fragment>
`;
exports[`StartGradingButton component component snapshotes snapshot: graded, confirmOverride (startGrading callback) 1`] = `
<React.Fragment>
<Button
disabled={false}
iconAfter={[MockFunction icons.Highlight]}
onClick={[MockFunction this.handleClick]}
variant="primary"
>
<FormattedMessage
defaultMessage="Override grade"
description="Review pane button text to start grading an already graded submission"
id="ora-grading.ReviewActions.StartGradingButton.overrideGrade"
/>
</Button>
<OverrideGradeConfirmModal
isOpen={true}
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
onConfirm={[MockFunction this.confirmOverrideGrade]}
/>
<StopGradingConfirmModal
isOpen={false}
isOverride={true}
onCancel={[MockFunction this.hideConfirmStopGrading]}
onConfirm={[MockFunction this.confirmStopGrading]}
/>
</React.Fragment>
`;
exports[`StartGradingButton component component snapshotes snapshot: inProgress, isOverride, confirmStop (stopGrading callback) 1`] = `
<React.Fragment>
<Button
disabled={false}
iconAfter={[MockFunction icons.Cancel]}
onClick={[MockFunction this.handleClick]}
variant="primary"
>
<FormattedMessage
defaultMessage="Stop grading this response"
description="Review pane button text to stop grading"
id="ora-grading.ReviewActions.StartGradingButton.stopGrading"
/>
</Button>
<OverrideGradeConfirmModal
isOpen={false}
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
onConfirm={[MockFunction this.confirmOverrideGrade]}
/>
<StopGradingConfirmModal
isOpen={true}
isOverride={true}
onCancel={[MockFunction this.hideConfirmStopGrading]}
onConfirm={[MockFunction this.confirmStopGrading]}
/>
</React.Fragment>
`;
exports[`StartGradingButton component component snapshotes snapshot: lock pending (disabled) 1`] = `
<React.Fragment>
<Button
disabled={true}
iconAfter={[MockFunction icons.Highlight]}
onClick={[MockFunction this.handleClick]}
variant="primary"
>
<FormattedMessage
defaultMessage="Start grading"
description="Review pane button text to start grading"
id="ora-grading.ReviewActions.StartGradingButton.startGrading"
/>
</Button>
<OverrideGradeConfirmModal
isOpen={false}
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
onConfirm={[MockFunction this.confirmOverrideGrade]}
/>
<StopGradingConfirmModal
isOpen={false}
isOverride={false}
onCancel={[MockFunction this.hideConfirmStopGrading]}
onConfirm={[MockFunction this.confirmStopGrading]}
/>
</React.Fragment>
`;
exports[`StartGradingButton component component snapshotes snapshot: locked (null) 1`] = `null`;
exports[`StartGradingButton component component snapshotes snapshot: ungraded (startGrading callback) 1`] = `
<React.Fragment>
<Button
disabled={false}
iconAfter={[MockFunction icons.Highlight]}
onClick={[MockFunction this.handleClick]}
variant="primary"
>
<FormattedMessage
defaultMessage="Start grading"
description="Review pane button text to start grading"
id="ora-grading.ReviewActions.StartGradingButton.startGrading"
/>
</Button>
<OverrideGradeConfirmModal
isOpen={false}
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
onConfirm={[MockFunction this.confirmOverrideGrade]}
/>
<StopGradingConfirmModal
isOpen={false}
isOverride={false}
onCancel={[MockFunction this.hideConfirmStopGrading]}
onConfirm={[MockFunction this.confirmStopGrading]}
/>
</React.Fragment>
`;

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