Compare commits
250 Commits
jenkins/ve
...
renovate/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ac93d2b75 | ||
|
|
8a2c6aae3b | ||
|
|
d4c6d93c26 | ||
|
|
0ff61e11d5 | ||
|
|
d9e5ae0c80 | ||
|
|
3c03358d4e | ||
|
|
f7e6e30d99 | ||
|
|
ae365b6951 | ||
|
|
729cb40c66 | ||
|
|
bc4abcdeef | ||
|
|
508c91d487 | ||
|
|
28634843d0 | ||
|
|
b7b9b9d81d | ||
|
|
bab0962b6d | ||
|
|
c564150cb5 | ||
|
|
f438360fdb | ||
|
|
4ce7209230 | ||
|
|
266589bca6 | ||
|
|
a579455e58 | ||
|
|
b10aa63723 | ||
|
|
377bb6bbc3 | ||
|
|
ac03594943 | ||
|
|
8e7bba5365 | ||
|
|
e4c0b1843d | ||
|
|
480262a7a2 | ||
|
|
66d5b01a6e | ||
|
|
57022ed294 | ||
|
|
3115fc275c | ||
|
|
f49c6a55f2 | ||
|
|
d71edbd2f2 | ||
|
|
715cc60c1c | ||
|
|
ec3c25f54a | ||
|
|
b54e8ffc85 | ||
|
|
5a383479ff | ||
|
|
cc4b1c8169 | ||
|
|
fc7370c593 | ||
|
|
01bc2cb545 | ||
|
|
4825c3d68c | ||
|
|
4e27a35e10 | ||
|
|
ad0e2af8c8 | ||
|
|
d03e7c40d8 | ||
|
|
5a7063c123 | ||
|
|
9348c4bb4c | ||
|
|
5c5ff1190b | ||
|
|
7aee8562a8 | ||
|
|
ebca59a38f | ||
|
|
f87d521bc0 | ||
|
|
5280cef554 | ||
|
|
08430571ed | ||
|
|
3de6821c5d | ||
|
|
fb06133a27 | ||
|
|
12f1c72b7e | ||
|
|
ca8d08c8a0 | ||
|
|
30a4ca17ac | ||
|
|
25d76c0e59 | ||
|
|
6527f505f1 | ||
|
|
400950cff8 | ||
|
|
0219f5cd25 | ||
|
|
fdcab456e8 | ||
|
|
5283e7c7c6 | ||
|
|
e39533c56a | ||
|
|
212014fed9 | ||
|
|
9600301a62 | ||
|
|
1d5f64e1db | ||
|
|
f5208c58aa | ||
|
|
c15680cb8c | ||
|
|
76f41439e9 | ||
|
|
4581cf8698 | ||
|
|
95fa32eaaa | ||
|
|
1f729becbe | ||
|
|
7ad1df8bd0 | ||
|
|
c9d0abe968 | ||
|
|
91e33748ab | ||
|
|
8809f4cf16 | ||
|
|
7482765aa4 | ||
|
|
7003b6102d | ||
|
|
d608daccb1 | ||
|
|
2208737a69 | ||
|
|
567a020061 | ||
|
|
c5d9bfb2f6 | ||
|
|
f433d33f9d | ||
|
|
c0816e0818 | ||
|
|
c83e86bf06 | ||
|
|
b6f1a9739e | ||
|
|
f2b6cd4cac | ||
|
|
3092bd3980 | ||
|
|
20dc736278 | ||
|
|
d72beeb2d4 | ||
|
|
64b57df976 | ||
|
|
346b371ba2 | ||
|
|
650d708a1c | ||
|
|
1d924b4812 | ||
|
|
31ed9410a4 | ||
|
|
0ba3cac532 | ||
|
|
b23effdb7f | ||
|
|
5bcb6fe6f3 | ||
|
|
a67f201f4d | ||
|
|
9cfab58663 | ||
|
|
59a7d0751b | ||
|
|
9d673e803e | ||
|
|
8438915f72 | ||
|
|
cc6dd20f12 | ||
|
|
f67ffdd480 | ||
|
|
836df49829 | ||
|
|
5bed90b659 | ||
|
|
3b39c79fbf | ||
|
|
df7a189bcd | ||
|
|
08c51b6492 | ||
|
|
3bb3d90f3a | ||
|
|
2f48cc5767 | ||
|
|
ea43ebb031 | ||
|
|
4a708da50c | ||
|
|
f135f9a111 | ||
|
|
44d6dc616c | ||
|
|
a9771fbf49 | ||
|
|
8f58b72919 | ||
|
|
8da2a60b37 | ||
|
|
ff3e56c3f7 | ||
|
|
204586e79b | ||
|
|
31be6daac3 | ||
|
|
9f1c950080 | ||
|
|
66b793a1d4 | ||
|
|
72db51b65c | ||
|
|
9121b3f1e7 | ||
|
|
f7e51fd1d0 | ||
|
|
77b4f9b47e | ||
|
|
3ef24a626b | ||
|
|
d810913038 | ||
|
|
a6436997bb | ||
|
|
7959a39267 | ||
|
|
3a1dbfdee5 | ||
|
|
9b326f1ee8 | ||
|
|
3a87ebda1a | ||
|
|
25389ff296 | ||
|
|
80f782b87f | ||
|
|
8a2d767263 | ||
|
|
264bed987e | ||
|
|
738d460505 | ||
|
|
b8f43b92a1 | ||
|
|
7afffa4509 | ||
|
|
b9ad13e354 | ||
|
|
8ceb9e308f | ||
|
|
b58cab1249 | ||
|
|
f6d8c324d9 | ||
|
|
0c8d2017db | ||
|
|
c644da3dcc | ||
|
|
cc11ce0f81 | ||
|
|
549252038f | ||
|
|
192c8b4601 | ||
|
|
70e13eccfa | ||
|
|
e25a5a9549 | ||
|
|
358263de3c | ||
|
|
dc7fc94ab5 | ||
|
|
b643afd1b8 | ||
|
|
5ac1868d30 | ||
|
|
65063df731 | ||
|
|
9aee97dccb | ||
|
|
be1ce502c8 | ||
|
|
19e2f35522 | ||
|
|
d24ab3358b | ||
|
|
56803fb874 | ||
|
|
b7b94531aa | ||
|
|
78ada8ce34 | ||
|
|
194e61380c | ||
|
|
fa36e20de9 | ||
|
|
61cf386ee6 | ||
|
|
445cd15d9a | ||
|
|
db1cf48257 | ||
|
|
942d471097 | ||
|
|
1a769a4e70 | ||
|
|
b5cb2af513 | ||
|
|
6666c0df83 | ||
|
|
fa60d7d234 | ||
|
|
78cce21f10 | ||
|
|
e21c2a63e7 | ||
|
|
514792786d | ||
|
|
5ef2f1ba4f | ||
|
|
b986849c85 | ||
|
|
f8565c30d1 | ||
|
|
3a7e103317 | ||
|
|
f977e14ea6 | ||
|
|
ab4f1864f2 | ||
|
|
6e4d4c479c | ||
|
|
d74532f988 | ||
|
|
d577bc79f7 | ||
|
|
43ab328545 | ||
|
|
3ddfdf34d0 | ||
|
|
b751d41caf | ||
|
|
4f4b28e6f5 | ||
|
|
038bd117e1 | ||
|
|
7bb31a9aa0 | ||
|
|
b2f59fc3a1 | ||
|
|
60b63944bd | ||
|
|
2893a9e698 | ||
|
|
de4d0fb7f2 | ||
|
|
ca7254c3b0 | ||
|
|
6095869271 | ||
|
|
8fe67f918f | ||
|
|
b28e58e7cd | ||
|
|
a1436c3266 | ||
|
|
9b5e85a236 | ||
|
|
fe1388666a | ||
|
|
a07d6f9b80 | ||
|
|
3e685be116 | ||
|
|
0bb5f50917 | ||
|
|
49357a4e87 | ||
|
|
33ba1cdd08 | ||
|
|
7012fa82c9 | ||
|
|
7b418ff6e3 | ||
|
|
cc349faeb2 | ||
|
|
455ca15af9 | ||
|
|
f992331bf4 | ||
|
|
4158231d7a | ||
|
|
2fa46ab00e | ||
|
|
adade6e48d | ||
|
|
06aea1ff68 | ||
|
|
054304902f | ||
|
|
ba9bddbda1 | ||
|
|
706d69aeca | ||
|
|
6d3ed03cac | ||
|
|
21a35cde82 | ||
|
|
66f85ee17e | ||
|
|
140cfc1639 | ||
|
|
26906d45f7 | ||
|
|
a753170cb7 | ||
|
|
690140ce46 | ||
|
|
6764a9766c | ||
|
|
c646b88543 | ||
|
|
b1d11119db | ||
|
|
35532fed92 | ||
|
|
15952d808a | ||
|
|
3a928e42bc | ||
|
|
15e756673f | ||
|
|
cba03d305c | ||
|
|
956dee9a6d | ||
|
|
4f7d3aeb57 | ||
|
|
d4f1383822 | ||
|
|
5efd1466bf | ||
|
|
36bd27517c | ||
|
|
6c884ce215 | ||
|
|
8b4f554cf6 | ||
|
|
0b1b079abd | ||
|
|
b2c52111d7 | ||
|
|
18bc94e2ff | ||
|
|
0f41df2cf3 | ||
|
|
91fbb8978a | ||
|
|
5aecd88c70 | ||
|
|
2bf499fb43 | ||
|
|
c217c32196 | ||
|
|
5f12c4fb8e |
5
.env
5
.env
@@ -30,3 +30,8 @@ ENTERPRISE_MARKETING_URL=''
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -7,7 +7,6 @@ LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
@@ -36,3 +35,8 @@ 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=''
|
||||
ACCOUNT_SETTINGS_URL=http://localhost:1997
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -7,7 +7,6 @@ LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
@@ -36,3 +35,5 @@ ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
ACCOUNT_SETTINGS_URL=http://localhost:1997
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
25
.eslintrc.js
25
.eslintrc.js
@@ -1,19 +1,34 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
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': ['*'] } }],
|
||||
'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',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.test.{js,jsx,ts,tsx}'],
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
config.settings = {
|
||||
"import/resolver": {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
paths: ["src", "node_modules"],
|
||||
extensions: [".js", ".jsx"],
|
||||
paths: ['src', 'node_modules'],
|
||||
extensions: ['.js', '.jsx'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal 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 }}
|
||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal 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
|
||||
|
||||
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@@ -10,18 +10,18 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14, 16]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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@v4
|
||||
# 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-file: '.nvmrc'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -39,7 +39,10 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
|
||||
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -7,4 +7,4 @@ on:
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
||||
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -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
|
||||
|
||||
32
.github/workflows/npm-publish.yml
vendored
32
.github/workflows/npm-publish.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: Release CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Create Build
|
||||
run: npm run build
|
||||
|
||||
- name: Release Package
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
|
||||
run: npm semantic-release
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal 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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,3 +25,5 @@ module.config.js
|
||||
### transifex ###
|
||||
src/i18n/transifex_input.json
|
||||
temp
|
||||
|
||||
src/i18n/messages
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
27
.releaserc
27
.releaserc
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"branch": "master",
|
||||
"tagFormat": "v${version}",
|
||||
"verifyConditions": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"analyzeCommits": "@semantic-release/commit-analyzer",
|
||||
"generateNotes": "@semantic-release/release-notes-generator",
|
||||
"prepare": "@semantic-release/npm",
|
||||
"publish": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"success": [],
|
||||
"fail": []
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-ora-grading]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
34
Makefile
34
Makefile
@@ -2,19 +2,15 @@ npm-install-%: ## install specified % npm package
|
||||
npm install $* --save-dev
|
||||
git add package.json
|
||||
|
||||
transifex_resource = frontend-app-ora-grading
|
||||
transifex_langs = "ar,fr,es_419,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
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
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
|
||||
@@ -44,20 +40,18 @@ detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-app-ora-grading/src/i18n/messages:frontend-app-ora-grading
|
||||
|
||||
$(intl_imports) frontend-component-footer frontend-component-header frontend-platform paragon frontend-app-ora-grading
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
235
README.rst
Normal file
235
README.rst
Normal file
@@ -0,0 +1,235 @@
|
||||
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
|
||||
=============
|
||||
|
||||
`Tutor`_ is currently recommended as a development environment for your
|
||||
new MFE. Please refer
|
||||
to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||
|
||||
.. _Tutor: https://github.com/overhangio/tutor
|
||||
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
||||
|
||||
|
||||
Plugins
|
||||
=======
|
||||
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||
|
||||
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||
|
||||
Developing
|
||||
==========
|
||||
|
||||
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 the version of Node specified in the ``.nvmrc`` file.
|
||||
|
||||
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
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
|
||||
21
catalog-info.yaml
Normal file
21
catalog-info.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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"
|
||||
annotations:
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: "user:codewithemad"
|
||||
type: 'website'
|
||||
lifecycle: 'production'
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFilesAfterEnv: [
|
||||
@@ -6,12 +6,12 @@ module.exports = createConfig('jest', {
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
modulePaths: ['<rootDir>/src/'],
|
||||
snapshotSerializers: [
|
||||
'enzyme-to-json/serializer',
|
||||
],
|
||||
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',
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
tags:
|
||||
- frontend-app
|
||||
- masters
|
||||
oeps:
|
||||
oep-2: true # Repository metadata
|
||||
openedx-release: {ref: master}
|
||||
71278
package-lock.json
generated
71278
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@@ -6,18 +6,19 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-ora-grading.git"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"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",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"dev": "PUBLIC_PATH=/ora-grading/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"watch-tests": "jest --watch",
|
||||
"prepare": "husky install"
|
||||
"watch-tests": "jest --watch"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -26,69 +27,66 @@
|
||||
"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": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^8.0.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@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",
|
||||
"@openedx/paragon": "^23.4.5",
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@redux-devtools/extension": "3.0.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.28.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",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"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",
|
||||
"react-pdf": "^5.5.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"moment": "^2.29.3",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "6.4.7",
|
||||
"react-pdf": "^7.0.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.3.0",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.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-environment-jsdom": "^29.7.0",
|
||||
"jest-expect-message": "^1.0.2",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
"react-test-renderer": "^16.14.0",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"semantic-release": "^17.4.5"
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"redux-mock-store": "^1.5.5"
|
||||
}
|
||||
}
|
||||
|
||||
34
renovate.json
Normal file
34
renovate.json
Normal 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"]
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
@@ -12,20 +12,23 @@ import DemoWarning from 'containers/DemoWarning';
|
||||
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}
|
||||
data-testid="header"
|
||||
/>
|
||||
{!isEnabled && <DemoWarning />}
|
||||
<main>
|
||||
<main data-testid="main">
|
||||
<ListView />
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
<FooterSlot />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
|
||||
23
src/App.scss
23
src/App.scss
@@ -1,15 +1,13 @@
|
||||
// frontend-app-*/src/index.scss
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
|
||||
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
|
||||
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
@@ -48,7 +46,22 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
|
||||
right: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-modal .pgn__modal-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pgn__modal-body-content {
|
||||
& img {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
& blockquote > p {
|
||||
border-left: 2px solid var(--pgn-color-gray-200);
|
||||
margin-left: 1.5rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
142
src/App.test.jsx
142
src/App.test.jsx
@@ -1,75 +1,101 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
import { renderWithIntl } from './testUtils';
|
||||
import { App, mapStateToProps } from './App';
|
||||
|
||||
import ListView from 'containers/ListView';
|
||||
// we want to scope these tests to the App component, so we mock some child components to reduce complexity
|
||||
|
||||
import { App } from './App';
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
getLoginRedirectUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => ({
|
||||
FooterSlot: () => <div data-testid="footer">Footer</div>,
|
||||
}));
|
||||
|
||||
jest.mock('containers/ListView', () => function ListView() {
|
||||
return <div data-testid="list-view">List View</div>;
|
||||
});
|
||||
|
||||
jest.mock('containers/DemoWarning', () => function DemoWarning() {
|
||||
return <div role="alert" data-testid="demo-warning">Demo Warning</div>;
|
||||
});
|
||||
|
||||
jest.mock('@edx/frontend-component-header', () => ({
|
||||
LearningHeader: ({ courseTitle, courseNumber, courseOrg }) => (
|
||||
<div data-testid="header">
|
||||
Header - {courseTitle} {courseNumber} {courseOrg}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
app: {
|
||||
selectors: {
|
||||
courseMetadata: (state) => ({ courseMetadata: state }),
|
||||
isEnabled: (state) => ({ isEnabled: state }),
|
||||
selectors: {
|
||||
app: {
|
||||
courseMetadata: jest.fn((state) => state.courseMetadata || {
|
||||
org: 'test-org',
|
||||
number: 'test-101',
|
||||
title: 'Test Course',
|
||||
}),
|
||||
isEnabled: jest.fn((state) => (state.isEnabled !== undefined ? state.isEnabled : true)),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-component-header', () => ({
|
||||
LearningHeader: 'Header',
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
|
||||
jest.mock('containers/DemoWarning', () => 'DemoWarning');
|
||||
jest.mock('containers/ListView', () => 'ListView');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
let router;
|
||||
|
||||
describe('App router component', () => {
|
||||
const props = {
|
||||
describe('App component', () => {
|
||||
const defaultProps = {
|
||||
courseMetadata: {
|
||||
org: 'course-org',
|
||||
number: 'course-number',
|
||||
title: 'course-title',
|
||||
org: 'test-org',
|
||||
number: 'test-101',
|
||||
title: 'Test Course',
|
||||
},
|
||||
isEnabled: true,
|
||||
};
|
||||
test('snapshot: enabled', () => {
|
||||
expect(shallow(<App {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: disabled (show demo warning)', () => {
|
||||
expect(shallow(<App {...props} isEnabled={false} />)).toMatchSnapshot();
|
||||
});
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App {...props} />);
|
||||
router = el.childAt(0);
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('Routing - ListView is only route', () => {
|
||||
expect(router.find('main')).toEqual(shallow(
|
||||
<main><ListView /></main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(router.find(Footer).props().logo).toEqual(logo);
|
||||
});
|
||||
|
||||
test('Header to use courseMetadata props', () => {
|
||||
const {
|
||||
courseTitle,
|
||||
courseNumber,
|
||||
courseOrg,
|
||||
} = router.find(Header).props();
|
||||
expect(courseTitle).toEqual(props.courseMetadata.title);
|
||||
expect(courseNumber).toEqual(props.courseMetadata.number);
|
||||
expect(courseOrg).toEqual(props.courseMetadata.org);
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders header with course metadata', () => {
|
||||
renderWithIntl(<App {...defaultProps} />);
|
||||
const org = screen.getByText((text) => text.includes('test-org'));
|
||||
expect(org).toBeInTheDocument();
|
||||
const title = screen.getByText((content) => content.includes('Test Course'));
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders main content', () => {
|
||||
renderWithIntl(<App {...defaultProps} />);
|
||||
|
||||
const main = screen.getByTestId('main');
|
||||
expect(main).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render demo warning when enabled', () => {
|
||||
renderWithIntl(<App {...defaultProps} />);
|
||||
|
||||
const demoWarning = screen.queryByRole('alert');
|
||||
expect(demoWarning).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders demo warning when disabled', () => {
|
||||
renderWithIntl(<App {...defaultProps} isEnabled={false} />);
|
||||
|
||||
const demoWarning = screen.getByRole('alert');
|
||||
expect(demoWarning).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
it('maps state properties correctly', () => {
|
||||
const testState = { arbitraryState: 'some data' };
|
||||
const mapped = mapStateToProps(testState);
|
||||
|
||||
expect(selectors.app.courseMetadata).toHaveBeenCalledWith(testState);
|
||||
expect(selectors.app.isEnabled).toHaveBeenCalledWith(testState);
|
||||
expect(mapped.courseMetadata).toEqual(selectors.app.courseMetadata(testState));
|
||||
expect(mapped.isEnabled).toEqual(selectors.app.isEnabled(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot: disabled (show demo warning) 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Header
|
||||
courseNumber="course-number"
|
||||
courseOrg="course-org"
|
||||
courseTitle="course-title"
|
||||
/>
|
||||
<DemoWarning />
|
||||
<main>
|
||||
<ListView />
|
||||
</main>
|
||||
<Footer
|
||||
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`App router component snapshot: enabled 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Header
|
||||
courseNumber="course-number"
|
||||
courseOrg="course-org"
|
||||
courseTitle="course-title"
|
||||
/>
|
||||
<main>
|
||||
<ListView />
|
||||
</main>
|
||||
<Footer
|
||||
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { AlertModal, ActionRow, Button } from '@edx/paragon';
|
||||
import { AlertModal, ActionRow, Button } from '@openedx/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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { ConfirmModal } from './ConfirmModal';
|
||||
|
||||
describe('ConfirmModal', () => {
|
||||
@@ -12,10 +13,48 @@ describe('ConfirmModal', () => {
|
||||
onCancel: jest.fn().mockName('this.props.onCancel'),
|
||||
onConfirm: jest.fn().mockName('this.props.onConfirm'),
|
||||
};
|
||||
test('snapshot: closed', () => {
|
||||
expect(shallow(<ConfirmModal {...props} />)).toMatchSnapshot();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('snapshot: open', () => {
|
||||
expect(shallow(<ConfirmModal {...props} isOpen />)).toMatchSnapshot();
|
||||
|
||||
it('should not render content when modal is closed', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<ConfirmModal {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.queryByText(props.content)).toBeNull();
|
||||
});
|
||||
|
||||
it('should display content when modal is open', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<ConfirmModal {...props} isOpen />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByText(props.content)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onCancel when cancel button is clicked', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<ConfirmModal {...props} isOpen />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByText(props.cancelText));
|
||||
expect(props.onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onConfirm when confirm button is clicked', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<ConfirmModal {...props} isOpen />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByText(props.confirmText));
|
||||
expect(props.onConfirm).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemoAlert component snapshot 1`] = `
|
||||
<AlertModal
|
||||
footerNode={
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction props.onClose]}
|
||||
variant="primary"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</ActionRow>
|
||||
}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction props.onClose]}
|
||||
title="Demo submit prevented"
|
||||
>
|
||||
<p>
|
||||
Grade submission is disabled in the Demo mode of the new ORA Staff Grader.
|
||||
</p>
|
||||
</AlertModal>
|
||||
`;
|
||||
@@ -1,39 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
AlertModal,
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const DemoAlert = ({
|
||||
intl: { formatMessage },
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => (
|
||||
<AlertModal
|
||||
title={formatMessage(messages.title)}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
{formatMessage(messages.confirm)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<AlertModal
|
||||
title={formatMessage(messages.title)}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
{formatMessage(messages.confirm)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<p>{formatMessage(messages.warningMessage)}</p>
|
||||
</AlertModal>
|
||||
);
|
||||
>
|
||||
<p>{formatMessage(messages.warningMessage)}</p>
|
||||
</AlertModal>
|
||||
);
|
||||
};
|
||||
DemoAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DemoAlert);
|
||||
export default DemoAlert;
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import messages from './messages';
|
||||
import { DemoAlert } from '.';
|
||||
|
||||
describe('DemoAlert component', () => {
|
||||
test('snapshot', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
isOpen: true,
|
||||
onClose: jest.fn().mockName('props.onClose'),
|
||||
};
|
||||
expect(shallow(<DemoAlert {...props} />)).toMatchSnapshot();
|
||||
const props = {
|
||||
isOpen: true,
|
||||
onClose: jest.fn().mockName('props.onClose'),
|
||||
};
|
||||
|
||||
it('does not render when isOpen is false', () => {
|
||||
renderWithIntl(<DemoAlert {...props} isOpen={false} />);
|
||||
expect(screen.queryByText(messages.title.defaultMessage)).toBeNull();
|
||||
});
|
||||
|
||||
it('renders with correct title and message when isOpen is true', () => {
|
||||
renderWithIntl(<DemoAlert {...props} />);
|
||||
expect(screen.getByText(messages.title.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.warningMessage.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when confirmation button is clicked', async () => {
|
||||
renderWithIntl(<DemoAlert {...props} />);
|
||||
const user = userEvent.setup();
|
||||
const confirmButton = screen.getByText(messages.confirm.defaultMessage);
|
||||
await user.click(confirmButton);
|
||||
expect(props.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilePopoverContent component snapshot default 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Name"
|
||||
description="Popover title for file name"
|
||||
id="ora-grading.FilePopoverContent.filePopoverNameTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
some file name
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Description"
|
||||
description="Popover title for file description"
|
||||
id="ora-grading.FilePopoverCellContent.filePopoverDescriptionTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
long descriptive text...
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Size"
|
||||
description="Popover title for file size"
|
||||
id="ora-grading.FilePopoverCellContent.fileSizeTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
filesize(6000)
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`FilePopoverContent component snapshot invalid size 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Name"
|
||||
description="Popover title for file name"
|
||||
id="ora-grading.FilePopoverContent.filePopoverNameTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
some file name
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Description"
|
||||
description="Popover title for file description"
|
||||
id="ora-grading.FilePopoverCellContent.filePopoverDescriptionTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
long descriptive text...
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Size"
|
||||
description="Popover title for file size"
|
||||
id="ora-grading.FilePopoverCellContent.fileSizeTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
Unknown
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import filesize from 'filesize';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
import FilePopoverContent from '.';
|
||||
|
||||
jest.mock('filesize', () => (size) => `filesize(${size})`);
|
||||
@@ -14,25 +14,26 @@ describe('FilePopoverContent', () => {
|
||||
downloadURL: 'this-url-is.working',
|
||||
size: 6000,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilePopoverContent {...props} />);
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('default', () => expect(el).toMatchSnapshot());
|
||||
test('invalid size', () => {
|
||||
el.setProps({
|
||||
size: null,
|
||||
});
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('content', () => {
|
||||
expect(el.text()).toContain(props.name);
|
||||
expect(el.text()).toContain(props.description);
|
||||
expect(el.text()).toContain(filesize(props.size));
|
||||
it('renders file name correctly', () => {
|
||||
renderWithIntl(<FilePopoverContent {...props} />);
|
||||
expect(screen.getByText(props.name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders file description correctly', () => {
|
||||
renderWithIntl(<FilePopoverContent {...props} />);
|
||||
expect(screen.getByText(props.description)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders file size correctly', () => {
|
||||
renderWithIntl(<FilePopoverContent {...props} />);
|
||||
expect(screen.getByText(filesize(props.size))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "Unknown" when size is null', () => {
|
||||
renderWithIntl(<FilePopoverContent {...props} size={null} />);
|
||||
expect(screen.getByText('Unknown')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert, Button } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { Alert, Button } from '@openedx/paragon';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messageShape = PropTypes.shape({
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithIntl } from '../../../testUtils';
|
||||
import ErrorBanner from './ErrorBanner';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
describe('Error Banner component', () => {
|
||||
@@ -25,35 +23,29 @@ describe('Error Banner component', () => {
|
||||
children,
|
||||
};
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ErrorBanner {...props} />);
|
||||
});
|
||||
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('children node', () => {
|
||||
expect(el.containsMatchingElement(children)).toEqual(true);
|
||||
describe('behavior', () => {
|
||||
it('renders children content', () => {
|
||||
renderWithIntl(<ErrorBanner {...props} />);
|
||||
const childText = screen.getByText('Abitary Child');
|
||||
expect(childText).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('verify actions', () => {
|
||||
const actions = el.find('Alert').prop('actions');
|
||||
expect(actions).toHaveLength(props.actions.length);
|
||||
|
||||
actions.forEach((action, index) => {
|
||||
expect(action.type).toEqual('Button');
|
||||
expect(action.props.onClick).toEqual(props.actions[index].onClick);
|
||||
// action message
|
||||
expect(action.props.children.props).toEqual(props.actions[index].message);
|
||||
});
|
||||
it('renders the correct number of action buttons', () => {
|
||||
renderWithIntl(<ErrorBanner {...props} />);
|
||||
const buttons = screen.getAllByText(messages.retryButton.defaultMessage);
|
||||
expect(buttons).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('verify heading', () => {
|
||||
const heading = el.find('FormattedMessage');
|
||||
expect(heading.props()).toEqual(props.headingMessage);
|
||||
it('renders error heading with correct message', () => {
|
||||
renderWithIntl(<ErrorBanner {...props} />);
|
||||
const heading = screen.getAllByText(messages.unknownError.defaultMessage)[0];
|
||||
expect(heading).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with danger variant', () => {
|
||||
renderWithIntl(<ErrorBanner {...props} />);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveClass('alert-danger');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Alert, Spinner } from '@edx/paragon';
|
||||
import { Alert, Spinner } from '@openedx/paragon';
|
||||
|
||||
export const LoadingBanner = () => (
|
||||
<Alert variant="info">
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import LoadingBanner from './LoadingBanner';
|
||||
|
||||
describe('Loading Banner component', () => {
|
||||
test('snapshot', () => {
|
||||
const el = shallow(<LoadingBanner />);
|
||||
expect(el).toMatchSnapshot();
|
||||
describe('behavior', () => {
|
||||
it('renders an info alert', () => {
|
||||
render(<LoadingBanner />);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveClass('alert-info');
|
||||
});
|
||||
|
||||
it('renders a spinner', () => {
|
||||
const { container } = render(<LoadingBanner />);
|
||||
const spinner = container.querySelector('.pgn__spinner');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(spinner).toHaveClass('spinner-border');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Error Banner component snapshot 1`] = `
|
||||
<Alert
|
||||
actions={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction action1.onClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Retry"
|
||||
description="Retry button for error in file renderer"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
|
||||
/>
|
||||
</Button>,
|
||||
<Button
|
||||
onClick={[MockFunction action2.onClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Retry"
|
||||
description="Retry button for error in file renderer"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Unknown errors"
|
||||
description="Unknown errors message"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
Abitary Child
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Loading Banner component snapshot 1`] = `
|
||||
<Alert
|
||||
variant="info"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="d-flex m-auto"
|
||||
/>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -1,21 +1,40 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import ImageRenderer from './ImageRenderer';
|
||||
|
||||
describe('Image Renderer Component', () => {
|
||||
const props = {
|
||||
url: 'some_url.jpg',
|
||||
fileName: 'test-image.jpg',
|
||||
onError: jest.fn().mockName('this.props.onError'),
|
||||
onSuccess: jest.fn().mockName('this.props.onSuccess'),
|
||||
};
|
||||
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ImageRenderer {...props} />);
|
||||
it('renders an image with the correct src and alt attributes', () => {
|
||||
render(<ImageRenderer {...props} />);
|
||||
const imgElement = screen.getByRole('img');
|
||||
expect(imgElement).toBeInTheDocument();
|
||||
expect(imgElement).toHaveAttribute('src', props.url);
|
||||
expect(imgElement).toHaveAttribute('alt', props.fileName);
|
||||
expect(imgElement).toHaveClass('image-renderer');
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
|
||||
it('calls onSuccess when image loads successfully', () => {
|
||||
render(<ImageRenderer {...props} />);
|
||||
|
||||
const imgElement = screen.getByRole('img');
|
||||
imgElement.dispatchEvent(new Event('load'));
|
||||
|
||||
expect(props.onSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onError when image fails to load', () => {
|
||||
render(<ImageRenderer {...props} />);
|
||||
|
||||
const imgElement = screen.getByRole('img');
|
||||
imgElement.dispatchEvent(new Event('error'));
|
||||
|
||||
expect(props.onError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,149 +1,83 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { pdfjs, Document, Page } from 'react-pdf';
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
import {
|
||||
Icon, Form, ActionRow, IconButton,
|
||||
} from '@edx/paragon';
|
||||
import { ChevronLeft, ChevronRight } from '@edx/paragon/icons';
|
||||
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
|
||||
} from '@openedx/paragon';
|
||||
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
|
||||
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
import { rendererHooks } from './pdfHooks';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
|
||||
|
||||
/**
|
||||
* <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 = {};
|
||||
|
||||
|
||||
@@ -1,221 +1,83 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import { Form, IconButton } from '@edx/paragon';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import PDFRenderer from './PDFRenderer';
|
||||
import * as hooks from './pdfHooks';
|
||||
|
||||
jest.mock('react-pdf', () => ({
|
||||
pdfjs: { GlobalWorkerOptions: {} },
|
||||
Document: () => 'Document',
|
||||
Page: () => 'Page',
|
||||
Document: jest.fn(),
|
||||
Page: jest.fn(),
|
||||
}));
|
||||
|
||||
Document.mockImplementation((props) => <div data-testid="pdf-document">{props.children}</div>);
|
||||
Document.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
Page.mockImplementation(() => <div data-testid="pdf-page">Page Content</div>);
|
||||
|
||||
jest.mock('./pdfHooks', () => ({
|
||||
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,
|
||||
hasPrev: 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('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('rendering', () => {
|
||||
it('should render the PDF document with navigation controls', () => {
|
||||
hooks.rendererHooks.mockReturnValue(hookProps);
|
||||
const { getByTestId, getAllByText, container } = render(<PDFRenderer {...props} />);
|
||||
expect(getByTestId('pdf-document')).toBeInTheDocument();
|
||||
expect(getByTestId('pdf-page')).toBeInTheDocument();
|
||||
expect(container.querySelector('input[type="number"]')).toBeInTheDocument();
|
||||
expect(getAllByText(/Page/).length).toBeGreaterThan(0);
|
||||
expect(getAllByText(`of ${hookProps.numPages}`).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('initial state', () => {
|
||||
expect(el.instance().state).toEqual(PDFRenderer.INITIAL_STATE);
|
||||
it('should have disabled previous button when on the first page', () => {
|
||||
hooks.rendererHooks.mockReturnValue({
|
||||
...hookProps,
|
||||
hasPrev: false,
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
const { container } = render(<PDFRenderer {...props} />);
|
||||
const prevButton = container.querySelector('button[aria-label="previous pdf page"]');
|
||||
expect(prevButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should have disabled next button when on the last page', () => {
|
||||
hooks.rendererHooks.mockReturnValue({
|
||||
...hookProps,
|
||||
hasNext: false,
|
||||
hasPrev: true,
|
||||
});
|
||||
|
||||
const { container } = render(<PDFRenderer {...props} />);
|
||||
const nextButton = container.querySelector('button[aria-label="next pdf page"]');
|
||||
expect(nextButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import TXTRenderer from './TXTRenderer';
|
||||
|
||||
jest.mock('axios', () => ({
|
||||
get: jest.fn((...args) => Promise.resolve({ data: `Content of ${args}` })),
|
||||
}));
|
||||
jest.mock('./textHooks', () => {
|
||||
const mockRendererHooks = jest.fn().mockReturnValue({ content: 'test-content' });
|
||||
return {
|
||||
rendererHooks: mockRendererHooks,
|
||||
};
|
||||
});
|
||||
|
||||
const textHooks = require('./textHooks');
|
||||
|
||||
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} />);
|
||||
textHooks.rendererHooks.mockClear();
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
|
||||
it('renders the text content in a pre element', () => {
|
||||
const { getByText, container } = render(<TXTRenderer {...props} />);
|
||||
expect(getByText('test-content')).toBeInTheDocument();
|
||||
expect(container.querySelector('pre')).toHaveClass('txt-renderer');
|
||||
});
|
||||
|
||||
it('passes the correct props to rendererHooks', () => {
|
||||
render(<TXTRenderer {...props} />);
|
||||
expect(textHooks.rendererHooks).toHaveBeenCalledWith({
|
||||
url: props.url,
|
||||
onError: props.onError,
|
||||
onSuccess: props.onSuccess,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Image Renderer Component snapshot 1`] = `
|
||||
<img
|
||||
alt=""
|
||||
className="image-renderer"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onLoad={[MockFunction this.props.onSuccess]}
|
||||
src="some_url.jpg"
|
||||
/>
|
||||
`;
|
||||
@@ -1,69 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PDF Renderer Component snapshots snapshot 1`] = `
|
||||
<div
|
||||
className="pdf-renderer"
|
||||
>
|
||||
<Document
|
||||
file="some_url.pdf"
|
||||
onLoadError={[MockFunction onDocumentLoadError]}
|
||||
onLoadSuccess={[MockFunction onDocumentLoadSuccess]}
|
||||
>
|
||||
<div
|
||||
className="page-wrapper"
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Page
|
||||
onLoadSuccess={[MockFunction onLoadPageSuccess]}
|
||||
pageNumber={1}
|
||||
/>
|
||||
</div>
|
||||
</Document>
|
||||
<ActionRow
|
||||
className="d-flex justify-content-center m-0"
|
||||
>
|
||||
<IconButton
|
||||
alt="previous pdf page"
|
||||
disabled={true}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction 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={1}
|
||||
min={0}
|
||||
onChange={[MockFunction onInputPageChange]}
|
||||
type="number"
|
||||
value={1}
|
||||
/>
|
||||
<Form.Label
|
||||
isInline={true}
|
||||
>
|
||||
of
|
||||
1
|
||||
</Form.Label>
|
||||
</Form.Group>
|
||||
<IconButton
|
||||
alt="next pdf page"
|
||||
disabled={true}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction onNextPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronRight]}
|
||||
/>
|
||||
</ActionRow>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,9 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TXT Renderer Component snapshot 1`] = `
|
||||
<pre
|
||||
className="txt-renderer"
|
||||
>
|
||||
Content of some_url.txt
|
||||
</pre>
|
||||
`;
|
||||
76
src/components/FilePreview/BaseRenderers/pdfHooks.jsx
Normal file
76
src/components/FilePreview/BaseRenderers/pdfHooks.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
|
||||
import { ErrorStatuses } from 'data/constants/requests';
|
||||
import { StrictDict } from 'utils';
|
||||
import * as module from './pdfHooks';
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
153
src/components/FilePreview/BaseRenderers/pdfHooks.test.js
Normal file
153
src/components/FilePreview/BaseRenderers/pdfHooks.test.js
Normal file
@@ -0,0 +1,153 @@
|
||||
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',
|
||||
}));
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useRef: jest.fn((val) => ({ current: val, useRef: true })),
|
||||
}));
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
34
src/components/FilePreview/BaseRenderers/textHooks.js
Normal file
34
src/components/FilePreview/BaseRenderers/textHooks.js
Normal 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 };
|
||||
};
|
||||
99
src/components/FilePreview/BaseRenderers/textHooks.test.js
Normal file
99
src/components/FilePreview/BaseRenderers/textHooks.test.js
Normal file
@@ -0,0 +1,99 @@
|
||||
/* 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(),
|
||||
}));
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
|
||||
}));
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Card, Collapsible } from '@edx/paragon';
|
||||
import { Card, Collapsible } from '@openedx/paragon';
|
||||
import FilePopoverContent from 'components/FilePopoverContent';
|
||||
import FileInfo from './FileInfo';
|
||||
|
||||
@@ -12,8 +12,12 @@ 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>}>
|
||||
<div className="preview-panel">
|
||||
<Collapsible
|
||||
className="file-collapsible"
|
||||
defaultOpen
|
||||
title={<h3 className="file-card-title">{file.name}</h3>}
|
||||
>
|
||||
<div className="preview-panel" data-testid="preview-panel">
|
||||
<FileInfo><FilePopoverContent {...file} /></FileInfo>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
@import "@edx/paragon/scss/core/core";
|
||||
|
||||
.file-card {
|
||||
margin: map-get($spacers, 1) 0;
|
||||
margin: var(--pgn-spacing-spacer-1) 0;
|
||||
|
||||
.file-card-title {
|
||||
text-overflow: ellipsis;
|
||||
@@ -26,8 +24,8 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
@media (--pgn-size-breakpoint-max-width-sm) {
|
||||
.file-card-title {
|
||||
width: map-get($container-max-widths, "sm")/2;
|
||||
width: calc(var(--pgn-size-container-max-width-sm)/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
|
||||
import FilePopoverContent from 'components/FilePopoverContent';
|
||||
import FileInfo from './FileInfo';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import FileCard from './FileCard';
|
||||
|
||||
jest.mock('components/FilePopoverContent', () => 'FilePopoverContent');
|
||||
@@ -19,24 +13,27 @@ describe('File Preview Card component', () => {
|
||||
},
|
||||
};
|
||||
const children = (<h1>some children</h1>);
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FileCard {...props}>{children}</FileCard>);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
test('collapsible title is name header', () => {
|
||||
const title = el.find(Collapsible).prop('title');
|
||||
expect(title).toEqual(<h3 className="file-card-title">{props.file.name}</h3>);
|
||||
it('renders with the file name in the title', () => {
|
||||
render(<FileCard {...props}>{children}</FileCard>);
|
||||
expect(screen.getByText(props.file.name)).toBeInTheDocument();
|
||||
expect(screen.getByText(props.file.name)).toHaveClass('file-card-title');
|
||||
});
|
||||
test('forwards children into preview-panel', () => {
|
||||
const previewPanelChildren = el.find('.preview-panel').children();
|
||||
expect(previewPanelChildren.at(0).equals(
|
||||
<FileInfo><FilePopoverContent file={props.file} /></FileInfo>,
|
||||
));
|
||||
expect(previewPanelChildren.at(1).equals(children)).toEqual(true);
|
||||
|
||||
it('renders the preview panel with file info', () => {
|
||||
render(<FileCard {...props}>{children}</FileCard>);
|
||||
const previewPanel = screen.getByTestId('preview-panel');
|
||||
expect(previewPanel).toBeInTheDocument();
|
||||
expect(document.querySelector('FileInfo')).toBeInTheDocument();
|
||||
expect(document.querySelector('FilePopoverContent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders children in the preview panel', () => {
|
||||
render(<FileCard {...props}>{children}</FileCard>);
|
||||
const previewPanel = screen.getByTestId('preview-panel');
|
||||
expect(previewPanel).toBeInTheDocument();
|
||||
expect(screen.getByText('some children')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,9 +5,10 @@ import {
|
||||
Button,
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
} from '@edx/paragon';
|
||||
import { InfoOutline } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { InfoOutline } from '@openedx/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,
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Popover } from '@edx/paragon';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
import FileInfo from './FileInfo';
|
||||
import messages from './messages';
|
||||
|
||||
describe('File Preview Card component', () => {
|
||||
describe('FileInfo component', () => {
|
||||
const children = (<h1>some Children</h1>);
|
||||
const props = { onClick: jest.fn().mockName('this.props.onClick') };
|
||||
let el;
|
||||
|
||||
beforeEach(() => {
|
||||
el = shallow(<FileInfo {...props}>{children}</FileInfo>);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
describe('Component', () => {
|
||||
test('overlay with passed children', () => {
|
||||
const { overlay } = el.at(0).props();
|
||||
expect(overlay.type).toEqual(Popover);
|
||||
expect(overlay.props.children).toEqual(<Popover.Content>{children}</Popover.Content>);
|
||||
|
||||
describe('Component rendering', () => {
|
||||
it('renders the FileInfo button with correct text', () => {
|
||||
renderWithIntl(<FileInfo {...props}>{children}</FileInfo>);
|
||||
expect(screen.getByText(messages.fileInfo.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClick when button is clicked', async () => {
|
||||
renderWithIntl(<FileInfo {...props}>{children}</FileInfo>);
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByText(messages.fileInfo.defaultMessage));
|
||||
expect(props.onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 { useIntl } 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,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
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 = {
|
||||
|
||||
@@ -1,133 +1,79 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { keyStore } from 'utils';
|
||||
import { ErrorStatuses } from 'data/constants/requests';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { FileRenderer } from './FileRenderer';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
import {
|
||||
ImageRenderer,
|
||||
PDFRenderer,
|
||||
TXTRenderer,
|
||||
} from 'components/FilePreview/BaseRenderers';
|
||||
import {
|
||||
FileRenderer,
|
||||
getFileType,
|
||||
ERROR_STATUSES,
|
||||
RENDERERS,
|
||||
} from './FileRenderer';
|
||||
|
||||
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',
|
||||
description: 'A text file',
|
||||
},
|
||||
};
|
||||
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}`,
|
||||
})),
|
||||
];
|
||||
it('renders loading banner when isLoading is true', () => {
|
||||
const hookProps = {
|
||||
Renderer: () => <div data-testid="mock-renderer">Renderer Component</div>,
|
||||
isLoading: true,
|
||||
errorStatus: null,
|
||||
error: null,
|
||||
rendererProps: { prop: 'hooks.rendererProps' },
|
||||
};
|
||||
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
|
||||
renderWithIntl(<FileRenderer {...props} />);
|
||||
|
||||
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;
|
||||
expect(screen.getByText('filename.txt')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('mock-renderer')).toBeInTheDocument();
|
||||
const spinner = document.querySelector('.spinner-border');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
});
|
||||
it('renders error banner when there is an error status', () => {
|
||||
const errorProps = {
|
||||
headingMessage: { id: 'error.heading', defaultMessage: 'Error Heading' },
|
||||
children: 'Error Message',
|
||||
actions: [{ id: 'retry', onClick: jest.fn(), message: { id: 'retry', defaultMessage: 'Retry' } }],
|
||||
};
|
||||
|
||||
const hookProps = {
|
||||
Renderer: () => <div data-testid="mock-renderer">Renderer Component</div>,
|
||||
isLoading: false,
|
||||
errorStatus: ErrorStatuses.serverError,
|
||||
error: errorProps,
|
||||
rendererProps: { prop: 'hooks.rendererProps' },
|
||||
};
|
||||
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
|
||||
|
||||
renderWithIntl(<FileRenderer {...props} />);
|
||||
|
||||
expect(screen.getByText('filename.txt')).toBeInTheDocument();
|
||||
expect(screen.getByText('Error Message')).toBeInTheDocument();
|
||||
expect(document.querySelector('.alert-heading')).toBeInTheDocument();
|
||||
expect(document.querySelector('.btn.btn-outline-primary')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
els.forEach((el) => {
|
||||
const file = el.prop('file');
|
||||
const fileType = getFileType(file.name);
|
||||
it('renders renderer component when not loading and no error', () => {
|
||||
const hookProps = {
|
||||
Renderer: () => <div data-testid="mock-renderer">Renderer Component</div>,
|
||||
isLoading: false,
|
||||
errorStatus: null,
|
||||
error: null,
|
||||
rendererProps: { prop: 'hooks.rendererProps' },
|
||||
};
|
||||
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
|
||||
|
||||
test(`successful rendering ${fileType}`, () => {
|
||||
el.setState({ isLoading: false });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
renderWithIntl(<FileRenderer {...props} />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
expect(screen.getByText('filename.txt')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('mock-renderer')).toBeInTheDocument();
|
||||
expect(screen.getByText('Renderer Component')).toBeInTheDocument();
|
||||
|
||||
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);
|
||||
};
|
||||
/**
|
||||
* 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));
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
const spinner = document.querySelector('.spinner-border');
|
||||
expect(spinner).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`File Preview Card component snapshot 1`] = `
|
||||
<Card
|
||||
className="file-card"
|
||||
key="test-file-name.pdf"
|
||||
>
|
||||
<Collapsible
|
||||
className="file-collapsible"
|
||||
defaultOpen={true}
|
||||
title={
|
||||
<h3
|
||||
className="file-card-title"
|
||||
>
|
||||
test-file-name.pdf
|
||||
</h3>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="preview-panel"
|
||||
>
|
||||
<FileInfo>
|
||||
<FilePopoverContent
|
||||
description="test-file description"
|
||||
downloadUrl="destination/test-file-name.pdf"
|
||||
name="test-file-name.pdf"
|
||||
/>
|
||||
</FileInfo>
|
||||
<h1>
|
||||
some children
|
||||
</h1>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
`;
|
||||
@@ -1,33 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`File Preview Card component snapshot 1`] = `
|
||||
<OverlayTrigger
|
||||
flip={true}
|
||||
overlay={
|
||||
<Popover
|
||||
className="overlay-help-popover"
|
||||
>
|
||||
<Popover.Content>
|
||||
<h1>
|
||||
some Children
|
||||
</h1>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
}
|
||||
placement="right-end"
|
||||
trigger="focus"
|
||||
>
|
||||
<Button
|
||||
iconAfter={[MockFunction icons.InfoOutline]}
|
||||
onClick={[MockFunction this.props.onClick]}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="File info"
|
||||
description="Popover trigger button text for file preview card"
|
||||
id="ora-grading.InfoPopover.fileInfo"
|
||||
/>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
`;
|
||||
@@ -1,292 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileRenderer component snapshot has error 404 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": "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"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering gif 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 6",
|
||||
"downloadUrl": "/url-path/fake_file_6.gif",
|
||||
"name": "fake_file_6.gif",
|
||||
}
|
||||
}
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
102
src/components/FilePreview/hooks.js
Normal file
102
src/components/FilePreview/hooks.js
Normal 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 = {
|
||||
headingMessage: 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,
|
||||
};
|
||||
};
|
||||
117
src/components/FilePreview/hooks.test.js
Normal file
117
src/components/FilePreview/hooks.test.js
Normal 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.headingMessage).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.headingMessage).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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
export { default as FileRenderer, isSupported } from './FileRenderer';
|
||||
export { default as FileRenderer } from './FileRenderer';
|
||||
export { isSupported } from './hooks';
|
||||
|
||||
20
src/components/Head/index.jsx
Normal file
20
src/components/Head/index.jsx
Normal 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;
|
||||
45
src/components/Head/index.test.jsx
Normal file
45
src/components/Head/index.test.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Head from '.';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
useIntl: () => ({
|
||||
formatMessage: (message, values) => {
|
||||
if (message.defaultMessage && values) {
|
||||
return message.defaultMessage.replace('{siteName}', values.siteName);
|
||||
}
|
||||
return message.defaultMessage || message.id;
|
||||
},
|
||||
}),
|
||||
defineMessages: (messages) => messages,
|
||||
}));
|
||||
|
||||
jest.mock('react-helmet', () => ({
|
||||
Helmet: jest.fn(),
|
||||
}));
|
||||
|
||||
Helmet.mockImplementation(({ children }) => <div data-testid="helmet-mock">{children}</div>);
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn().mockReturnValue({
|
||||
SITE_NAME: 'site-name',
|
||||
FAVICON_URL: 'favicon-url',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Head', () => {
|
||||
it('should render page title with site name from config', () => {
|
||||
const { container } = render(<Head />);
|
||||
const titleElement = container.querySelector('title');
|
||||
expect(titleElement).toBeInTheDocument();
|
||||
expect(titleElement.textContent).toContain('ORA staff grading | site-name');
|
||||
});
|
||||
|
||||
it('should render favicon link with URL from config', () => {
|
||||
const { container } = render(<Head />);
|
||||
const faviconLink = container.querySelector('link[rel="shortcut icon"]');
|
||||
expect(faviconLink).toBeInTheDocument();
|
||||
expect(faviconLink.getAttribute('href')).toEqual('favicon-url');
|
||||
expect(faviconLink.getAttribute('type')).toEqual('image/x-icon');
|
||||
});
|
||||
});
|
||||
11
src/components/Head/messages.js
Normal file
11
src/components/Head/messages.js
Normal 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;
|
||||
@@ -1,28 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Info Popover Component snapshot 1`] = `
|
||||
<OverlayTrigger
|
||||
flip={true}
|
||||
overlay={
|
||||
<Popover
|
||||
className="overlay-help-popover"
|
||||
>
|
||||
<Popover.Content>
|
||||
<div>
|
||||
Children component
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
}
|
||||
placement="right-end"
|
||||
trigger="focus"
|
||||
>
|
||||
<IconButton
|
||||
alt="Display more info"
|
||||
className="esg-help-icon"
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction this.props.onClick]}
|
||||
src={[MockFunction icons.InfoOutline]}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
`;
|
||||
@@ -6,38 +6,49 @@ import {
|
||||
Popover,
|
||||
Icon,
|
||||
IconButton,
|
||||
} from '@edx/paragon';
|
||||
import { InfoOutline } from '@edx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
} from '@openedx/paragon';
|
||||
import { InfoOutline } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { nullMethod } from 'hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <InfoPopover />
|
||||
*/
|
||||
export const InfoPopover = ({ onClick, children, intl }) => (
|
||||
<OverlayTrigger
|
||||
trigger="focus"
|
||||
placement="right-end"
|
||||
flip
|
||||
overlay={(
|
||||
<Popover className="overlay-help-popover">
|
||||
<Popover.Content>{children}</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
className="esg-help-icon"
|
||||
src={InfoOutline}
|
||||
alt={intl.formatMessage(messages.altText)}
|
||||
iconAs={Icon}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
export const InfoPopover = (
|
||||
{
|
||||
onClick,
|
||||
children,
|
||||
},
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<OverlayTrigger
|
||||
trigger="focus"
|
||||
placement="left-end"
|
||||
flip
|
||||
overlay={(
|
||||
<Popover id="info-popover" className="overlay-help-popover">
|
||||
<Popover.Content>{children}</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
className="esg-help-icon"
|
||||
data-testid="esg-help-icon"
|
||||
src={InfoOutline}
|
||||
alt={intl.formatMessage(messages.altText)}
|
||||
iconAs={Icon}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
InfoPopover.defaultProps = {
|
||||
onClick: () => {},
|
||||
onClick: nullMethod,
|
||||
};
|
||||
InfoPopover.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
@@ -45,7 +56,6 @@ InfoPopover.propTypes = {
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(InfoPopover);
|
||||
export default InfoPopover;
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { InfoPopover } from '.';
|
||||
|
||||
describe('Info Popover Component', () => {
|
||||
const child = <div>Children component</div>;
|
||||
const onClick = jest.fn().mockName('this.props.onClick');
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<InfoPopover onClick={onClick} intl={{ formatMessage }}>{child}</InfoPopover>);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
test('Test component render', () => {
|
||||
expect(el.length).toEqual(1);
|
||||
expect(el.find('.esg-help-icon').length).toEqual(1);
|
||||
it('renders the help icon button', () => {
|
||||
renderWithIntl(
|
||||
<InfoPopover onClick={onClick}>
|
||||
{child}
|
||||
</InfoPopover>,
|
||||
);
|
||||
expect(screen.getByTestId('esg-help-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClick when the help icon is clicked', async () => {
|
||||
renderWithIntl(
|
||||
<InfoPopover onClick={onClick}>
|
||||
{child}
|
||||
</InfoPopover>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByTestId('esg-help-icon'));
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Spinner } from '@edx/paragon';
|
||||
import { Spinner } from '@openedx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Badge } from '@edx/paragon';
|
||||
import { Badge } from '@openedx/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 (
|
||||
|
||||
37
src/components/StatusBadge.test.jsx
Normal file
37
src/components/StatusBadge.test.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { gradingStatuses } from 'data/services/lms/constants';
|
||||
import messages from '../data/services/lms/messages';
|
||||
import { renderWithIntl } from '../testUtils';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
|
||||
const className = 'test-className';
|
||||
describe('StatusBadge component', () => {
|
||||
describe('behavior', () => {
|
||||
it('does not render if status does not have configured variant', () => {
|
||||
const { container } = renderWithIntl(<StatusBadge className={className} status="arbitrary" />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
describe('status rendering: loads badge with configured variant and message', () => {
|
||||
it('`ungraded` shows primary button variant and message', () => {
|
||||
renderWithIntl(<StatusBadge className={className} status={gradingStatuses.ungraded} />);
|
||||
const badge = screen.getByText(messages.ungraded.defaultMessage);
|
||||
expect(badge).toHaveClass('badge-primary');
|
||||
});
|
||||
it('`locked` shows light button variant and message', () => {
|
||||
renderWithIntl(<StatusBadge className={className} status={gradingStatuses.locked} />);
|
||||
const badge = screen.getByText(messages.locked.defaultMessage);
|
||||
expect(badge).toHaveClass('badge-light');
|
||||
});
|
||||
it('`graded` shows success button variant and message', () => {
|
||||
renderWithIntl(<StatusBadge className={className} status={gradingStatuses.graded} />);
|
||||
const badge = screen.getByText(messages.graded.defaultMessage);
|
||||
expect(badge).toHaveClass('badge-success');
|
||||
});
|
||||
it('`inProgress` shows warning button variant and message', () => {
|
||||
renderWithIntl(<StatusBadge className={className} status={gradingStatuses.inProgress} />);
|
||||
const badge = screen.getByText(messages['in-progress'].defaultMessage);
|
||||
expect(badge).toHaveClass('badge-warning');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConfirmModal snapshot: closed 1`] = `
|
||||
<AlertModal
|
||||
className="confirm-modal"
|
||||
footerNode={
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction this.props.onCancel]}
|
||||
variant="tertiary"
|
||||
>
|
||||
test-cancel-text
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[MockFunction this.props.onConfirm]}
|
||||
variant="primary"
|
||||
>
|
||||
test-confirm-text
|
||||
</Button>
|
||||
</ActionRow>
|
||||
}
|
||||
isOpen={false}
|
||||
onClose={[Function]}
|
||||
title="test-title"
|
||||
>
|
||||
<p>
|
||||
test-content
|
||||
</p>
|
||||
</AlertModal>
|
||||
`;
|
||||
|
||||
exports[`ConfirmModal snapshot: open 1`] = `
|
||||
<AlertModal
|
||||
className="confirm-modal"
|
||||
footerNode={
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction this.props.onCancel]}
|
||||
variant="tertiary"
|
||||
>
|
||||
test-cancel-text
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[MockFunction this.props.onConfirm]}
|
||||
variant="primary"
|
||||
>
|
||||
test-confirm-text
|
||||
</Button>
|
||||
</ActionRow>
|
||||
}
|
||||
isOpen={true}
|
||||
onClose={[Function]}
|
||||
title="test-title"
|
||||
>
|
||||
<p>
|
||||
test-content
|
||||
</p>
|
||||
</AlertModal>
|
||||
`;
|
||||
@@ -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 };
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { feedbackRequirement } from 'data/services/lms/constants';
|
||||
import { actions, selectors } from 'data/redux';
|
||||
@@ -12,59 +12,56 @@ import messages from './messages';
|
||||
/**
|
||||
* <CriterionFeedback />
|
||||
*/
|
||||
export class CriterionFeedback extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
export const CriterionFeedback = ({
|
||||
orderNum,
|
||||
isGrading,
|
||||
config,
|
||||
setValue,
|
||||
value,
|
||||
isInvalid,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
onChange(event) {
|
||||
this.props.setValue({
|
||||
const onChange = (event) => {
|
||||
setValue({
|
||||
value: event.target.value,
|
||||
orderNum: this.props.orderNum,
|
||||
orderNum,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
get commentMessage() {
|
||||
const { config, isGrading } = this.props;
|
||||
let commentMessage = this.translate(isGrading ? messages.addComments : messages.comments);
|
||||
const translate = (msg) => intl.formatMessage(msg);
|
||||
|
||||
const getCommentMessage = () => {
|
||||
let commentMessage = translate(isGrading ? messages.addComments : messages.comments);
|
||||
if (config === feedbackRequirement.optional) {
|
||||
commentMessage += ` ${this.translate(messages.optional)}`;
|
||||
commentMessage += ` ${translate(messages.optional)}`;
|
||||
}
|
||||
return commentMessage;
|
||||
};
|
||||
|
||||
if (config === feedbackRequirement.disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
translate = (msg) => this.props.intl.formatMessage(msg);
|
||||
|
||||
render() {
|
||||
const {
|
||||
config,
|
||||
isGrading,
|
||||
value,
|
||||
isInvalid,
|
||||
} = this.props;
|
||||
if (config === feedbackRequirement.disabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Form.Group isInvalid={this.feedbackIsInvalid}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
floatingLabel={this.commentMessage}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
|
||||
{this.translate(messages.criterionFeedbackError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Form.Group isInvalid={isInvalid}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
data-testid="criterion-feedback-input"
|
||||
floatingLabel={getCommentMessage()}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={!isGrading}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<Form.Control.Feedback type="invalid" className="feedback-error-msg" data-testid="criterion-feedback-error-msg">
|
||||
{translate(messages.criterionFeedbackError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
CriterionFeedback.defaultProps = {
|
||||
value: '',
|
||||
@@ -73,8 +70,6 @@ CriterionFeedback.defaultProps = {
|
||||
CriterionFeedback.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
config: PropTypes.string.isRequired,
|
||||
setValue: PropTypes.func.isRequired,
|
||||
@@ -92,6 +87,4 @@ export const mapDispatchToProps = {
|
||||
setValue: actions.grading.setCriterionFeedback,
|
||||
};
|
||||
|
||||
export default injectIntl(
|
||||
connect(mapStateToProps, mapDispatchToProps)(CriterionFeedback),
|
||||
);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CriterionFeedback);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { actions, selectors } from 'data/redux';
|
||||
import {
|
||||
feedbackRequirement,
|
||||
gradeStatuses,
|
||||
} from 'data/services/lms/constants';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import {
|
||||
CriterionFeedback,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './CriterionFeedback';
|
||||
import messages from './messages';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -36,7 +36,6 @@ jest.mock('data/redux/grading/selectors', () => ({
|
||||
|
||||
describe('Criterion Feedback', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
orderNum: 1,
|
||||
config: 'config string',
|
||||
isGrading: true,
|
||||
@@ -45,141 +44,72 @@ describe('Criterion Feedback', () => {
|
||||
setValue: jest.fn().mockName('this.props.setValue'),
|
||||
isInvalid: false,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<CriterionFeedback {...props} />);
|
||||
el.instance().onChange = jest.fn().mockName('this.onChange');
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('is grading', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('is graded', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
gradeStatus: gradeStatuses.graded,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('feedback value is invalid', () => {
|
||||
el.setProps({
|
||||
isInvalid: true,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
Object.values(feedbackRequirement).forEach((requirement) => {
|
||||
test(`feedback is configured to ${requirement}`, () => {
|
||||
el.setProps({
|
||||
config: requirement,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('render', () => {
|
||||
test('is grading (the feedback input is not disabled)', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
expect(el.instance().props.value).toEqual(props.value);
|
||||
const controlEl = el.find('.feedback-input');
|
||||
expect(controlEl.prop('disabled')).toEqual(false);
|
||||
expect(controlEl.prop('value')).toEqual(props.value);
|
||||
it('shows a non-disabled input when grading', () => {
|
||||
renderWithIntl(<CriterionFeedback {...props} />);
|
||||
const input = screen.getByTestId('criterion-feedback-input');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).not.toBeDisabled();
|
||||
expect(input).toHaveValue(props.value);
|
||||
});
|
||||
test('is graded (the input is disabled)', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
gradeStatus: gradeStatuses.graded,
|
||||
});
|
||||
expect(el.instance().props.value).toEqual(props.value);
|
||||
const controlEl = el.find('.feedback-input');
|
||||
expect(controlEl.prop('disabled')).toEqual(true);
|
||||
expect(controlEl.prop('value')).toEqual(props.value);
|
||||
|
||||
it('shows a disabled input when not grading', () => {
|
||||
renderWithIntl(
|
||||
<CriterionFeedback {...props} isGrading={false} gradeStatus={gradeStatuses.graded} />,
|
||||
);
|
||||
const input = screen.getByTestId('criterion-feedback-input');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toBeDisabled();
|
||||
expect(input).toHaveValue(props.value);
|
||||
});
|
||||
test('is having invalid feedback (feedback get render)', () => {
|
||||
el.setProps({
|
||||
isInvalid: true,
|
||||
});
|
||||
const feedbackErrorEl = el.find('.feedback-error-msg');
|
||||
expect(el.instance().props.isInvalid).toEqual(true);
|
||||
expect(feedbackErrorEl).toBeDefined();
|
||||
|
||||
it('displays an error message when feedback is invalid', () => {
|
||||
renderWithIntl(<CriterionFeedback {...props} isInvalid />);
|
||||
expect(screen.getByTestId('criterion-feedback-error-msg')).toBeInTheDocument();
|
||||
});
|
||||
test('is configure to disabled (the input does not get render)', () => {
|
||||
el.setProps({
|
||||
config: feedbackRequirement.disabled,
|
||||
});
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
|
||||
it('does not render anything when config is set to disabled', () => {
|
||||
const { container } = renderWithIntl(
|
||||
<CriterionFeedback {...props} config={feedbackRequirement.disabled} />,
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('onChange call set value', () => {
|
||||
el = shallow(<CriterionFeedback {...props} />);
|
||||
el.instance().onChange({
|
||||
target: {
|
||||
value: 'some value',
|
||||
},
|
||||
it('calls setValue when input value changes', async () => {
|
||||
renderWithIntl(<CriterionFeedback {...props} />);
|
||||
const user = userEvent.setup();
|
||||
const input = screen.getByTestId('criterion-feedback-input');
|
||||
await user.clear(input);
|
||||
expect(props.setValue).toHaveBeenCalledWith({
|
||||
value: '',
|
||||
orderNum: props.orderNum,
|
||||
});
|
||||
expect(props.setValue).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getter commentMessage', () => {
|
||||
test('is grading', () => {
|
||||
el.setProps({ config: feedbackRequirement.optional, isGrading: true });
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
el.setProps({ config: feedbackRequirement.required });
|
||||
expect(el.instance().commentMessage).not.toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.addComments.defaultMessage,
|
||||
);
|
||||
});
|
||||
|
||||
test('is not grading', () => {
|
||||
el.setProps({ config: feedbackRequirement.optional, isGrading: false });
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
el.setProps({ config: feedbackRequirement.required });
|
||||
expect(el.instance().commentMessage).not.toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.comments.defaultMessage,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { abitaryState: 'some data' };
|
||||
const testState = { arbitraryState: 'some data' };
|
||||
const ownProps = { orderNum: props.orderNum };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
test('selectors.app.rubric.criterionFeedbackConfig', () => {
|
||||
it('gets config from selectors.app.rubric.criterionFeedbackConfig', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionFeedbackConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
test('selector.grading.selected.criterionFeedback', () => {
|
||||
it('gets value from selectors.grading.selected.criterionFeedback', () => {
|
||||
expect(mapped.value).toEqual(
|
||||
selectors.grading.selected.criterionFeedback(testState, ownProps),
|
||||
);
|
||||
});
|
||||
test('selector.grading.validation.criterionFeedbackIsInvalid', () => {
|
||||
it('gets isInvalid from selectors.grading.validation.criterionFeedbackIsInvalid', () => {
|
||||
expect(mapped.isInvalid).toEqual(
|
||||
selectors.grading.validation.criterionFeedbackIsInvalid(
|
||||
testState,
|
||||
@@ -190,7 +120,7 @@ describe('Criterion Feedback', () => {
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('maps actions.grading.setCriterionFeedback to setValue prop', () => {
|
||||
it('maps actions.grading.setCriterionFeedback to setValue prop', () => {
|
||||
expect(mapDispatchToProps.setValue).toEqual(
|
||||
actions.grading.setCriterionFeedback,
|
||||
);
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { actions, selectors } from 'data/redux';
|
||||
import messages from './messages';
|
||||
@@ -11,52 +11,46 @@ import messages from './messages';
|
||||
/**
|
||||
* <RadioCriterion />
|
||||
*/
|
||||
export class RadioCriterion extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
export const RadioCriterion = ({
|
||||
orderNum,
|
||||
isGrading,
|
||||
config,
|
||||
data,
|
||||
setCriterionOption,
|
||||
isInvalid,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
onChange(event) {
|
||||
this.props.setCriterionOption({
|
||||
orderNum: this.props.orderNum,
|
||||
const onChange = (event) => {
|
||||
setCriterionOption({
|
||||
orderNum,
|
||||
value: event.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
config,
|
||||
data,
|
||||
intl,
|
||||
isGrading,
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Form.RadioSet name={config.name} value={data}>
|
||||
{config.options.map((option) => (
|
||||
<Form.Radio
|
||||
className="criteria-option align-items-center"
|
||||
key={option.name}
|
||||
value={option.name}
|
||||
description={intl.formatMessage(messages.optionPoints, { points: option.points })}
|
||||
onChange={onChange}
|
||||
disabled={!isGrading}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{option.label}
|
||||
</Form.Radio>
|
||||
))}
|
||||
{isInvalid && (
|
||||
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
|
||||
{intl.formatMessage(messages.rubricSelectedError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.RadioSet>
|
||||
);
|
||||
};
|
||||
|
||||
RadioCriterion.defaultProps = {
|
||||
data: {
|
||||
@@ -68,8 +62,6 @@ RadioCriterion.defaultProps = {
|
||||
RadioCriterion.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
config: PropTypes.shape({
|
||||
prompt: PropTypes.string,
|
||||
@@ -100,4 +92,4 @@ export const mapDispatchToProps = {
|
||||
setCriterionOption: actions.grading.setCriterionOption,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(RadioCriterion));
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RadioCriterion);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import { actions, selectors } from 'data/redux';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import {
|
||||
RadioCriterion,
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
} from './RadioCriterion';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -31,7 +30,6 @@ jest.mock('data/redux/grading/selectors', () => ({
|
||||
|
||||
describe('Radio Criterion Container', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
orderNum: 1,
|
||||
isGrading: true,
|
||||
config: {
|
||||
@@ -40,14 +38,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',
|
||||
@@ -55,98 +53,70 @@ describe('Radio Criterion Container', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
data: 'selected radio option',
|
||||
data: 'option name',
|
||||
setCriterionOption: jest.fn().mockName('this.props.setCriterionOption'),
|
||||
isInvalid: false,
|
||||
};
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<RadioCriterion {...props} />);
|
||||
el.instance().onChange = jest.fn().mockName('this.onChange');
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('is grading', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
describe('component rendering', () => {
|
||||
it('should render radio buttons that are enabled when in grading mode', () => {
|
||||
const { container } = renderWithIntl(<RadioCriterion {...props} />);
|
||||
|
||||
test('is not grading', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
const radioButtons = container.querySelectorAll('input[type="radio"]');
|
||||
expect(radioButtons.length).toEqual(props.config.options.length);
|
||||
|
||||
test('radio contain invalid response', () => {
|
||||
el.setProps({
|
||||
isInvalid: true,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('rendering', () => {
|
||||
test('is grading (all options are not disabled)', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.find('.criteria-option');
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl) => expect(optionEl.prop('disabled')).toEqual(false));
|
||||
});
|
||||
|
||||
test('is not grading (all options are disabled)', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
});
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.find('.criteria-option');
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl) => expect(optionEl.prop('disabled')).toEqual(true));
|
||||
});
|
||||
|
||||
test('radio contain invalid response (error response get render)', () => {
|
||||
el.setProps({
|
||||
isInvalid: true,
|
||||
});
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const radioErrorEl = el.find('.feedback-error-msg');
|
||||
expect(el.instance().props.isInvalid).toEqual(true);
|
||||
expect(radioErrorEl).toBeDefined();
|
||||
radioButtons.forEach(button => {
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('onChange call set crition option', () => {
|
||||
el = shallow(<RadioCriterion {...props} />);
|
||||
el.instance().onChange({
|
||||
target: {
|
||||
value: 'some value',
|
||||
},
|
||||
});
|
||||
expect(props.setCriterionOption).toBeCalledTimes(1);
|
||||
it('should render radio buttons that are disabled when not in grading mode', () => {
|
||||
renderWithIntl(<RadioCriterion {...props} isGrading={false} />);
|
||||
|
||||
const radioButtons = screen.queryAllByRole('radio');
|
||||
expect(radioButtons.length).toEqual(props.config.options.length);
|
||||
|
||||
radioButtons.forEach(button => {
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render an error message when the criterion is invalid', () => {
|
||||
const { container } = renderWithIntl(<RadioCriterion {...props} isInvalid />);
|
||||
|
||||
const errorMessage = container.querySelector('.feedback-error-msg');
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render an error message when the criterion is valid', () => {
|
||||
const { container } = renderWithIntl(<RadioCriterion {...props} />);
|
||||
|
||||
const errorMessage = container.querySelector('.feedback-error-msg');
|
||||
expect(errorMessage).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { arbitary: 'some data' };
|
||||
const testState = { arbitrary: 'some data' };
|
||||
const ownProps = { orderNum: props.orderNum };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
test('selectors.app.rubric.criterionConfig', () => {
|
||||
|
||||
it('should properly map config from rubric criterion config selector', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
|
||||
test('selectors.grading.selected.criterionSelectedOption', () => {
|
||||
it('should properly map data from selected criterion option selector', () => {
|
||||
expect(mapped.data).toEqual(
|
||||
selectors.grading.selected.criterionSelectedOption(testState, ownProps),
|
||||
);
|
||||
});
|
||||
test('selectors.grading.validation.criterionSelectedOptionIsInvalid', () => {
|
||||
|
||||
it('should properly map isInvalid from criterion validation selector', () => {
|
||||
expect(mapped.isInvalid).toEqual(
|
||||
selectors.grading.validation.criterionSelectedOptionIsInvalid(testState, ownProps),
|
||||
);
|
||||
@@ -154,7 +124,7 @@ describe('Radio Criterion Container', () => {
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('maps actions.grading.setCriterionFeedback to setValue prop', () => {
|
||||
it('should map setCriterionOption action to props', () => {
|
||||
expect(mapDispatchToProps.setCriterionOption).toEqual(
|
||||
actions.grading.setCriterionOption,
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form, FormControlFeedback } from '@edx/paragon';
|
||||
import { Form, FormControlFeedback } from '@openedx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
@@ -14,10 +14,10 @@ import messages from './messages';
|
||||
export const ReviewCriterion = ({ config }) => (
|
||||
<div className="review-criterion">
|
||||
{config.options.map((option) => (
|
||||
<div key={option.name} className="criteria-option">
|
||||
<div key={option.name} className="criteria-option" data-testid="criteria-option">
|
||||
<div>
|
||||
<Form.Label className="option-label">{option.label}</Form.Label>
|
||||
<FormControlFeedback className="option-points">
|
||||
<Form.Label className="option-label" data-testid="option-label">{option.label}</Form.Label>
|
||||
<FormControlFeedback className="option-points" data-testid="option-points">
|
||||
<FormattedMessage {...messages.optionPoints} values={{ points: option.points }} />
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { ReviewCriterion, mapStateToProps } from './ReviewCriterion';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -20,7 +20,7 @@ jest.mock('data/redux/grading/selectors', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Review Crition Container', () => {
|
||||
describe('Review Criterion Container', () => {
|
||||
const props = {
|
||||
orderNum: 1,
|
||||
config: {
|
||||
@@ -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',
|
||||
@@ -50,47 +50,40 @@ describe('Review Crition Container', () => {
|
||||
},
|
||||
};
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ReviewCriterion {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('rendering (everything show up)', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.find('.criteria-option');
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl, i) => {
|
||||
const option = props.config.options[i];
|
||||
expect(optionEl.key()).toEqual(option.name);
|
||||
expect(optionEl.find('.option-label').childAt(0).text()).toEqual(
|
||||
option.label,
|
||||
);
|
||||
expect(optionEl.find('.option-points').childAt(0).props()).toEqual({
|
||||
...messages.optionPoints,
|
||||
values: { points: option.points },
|
||||
});
|
||||
it('renders all criteria options with correct labels and points', () => {
|
||||
renderWithIntl(<ReviewCriterion {...props} />);
|
||||
|
||||
const optionsElements = screen.getAllByTestId('criteria-option');
|
||||
expect(optionsElements.length).toEqual(props.config.options.length);
|
||||
|
||||
props.config.options.forEach((option, index) => {
|
||||
const optionElement = optionsElements[index];
|
||||
const labelElement = optionElement.querySelector('[data-testid="option-label"]');
|
||||
const pointsElement = optionElement.querySelector('[data-testid="option-points"]');
|
||||
|
||||
expect(labelElement.textContent).toEqual(option.label);
|
||||
expect(pointsElement.textContent).toEqual(`${props.config.options[index].points} points`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { arbitary: 'some data' };
|
||||
const testState = { arbitrary: 'some data' };
|
||||
const ownProps = { orderNum: props.orderNum };
|
||||
let mapped;
|
||||
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
test('selectors.app.rubric.criterionConfig', () => {
|
||||
|
||||
it('should map criterion config from state', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
|
||||
test('selectors.grading.selected.criterionGradeData', () => {
|
||||
it('should map criterion grade data from state', () => {
|
||||
expect(mapped.data).toEqual(
|
||||
selectors.grading.selected.criterionGradeData(testState, ownProps),
|
||||
);
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback is configured to disabled 1`] = `null`;
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback is configured to optional 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments (Optional)"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback is configured to required 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback value is invalid 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="criterion value"
|
||||
/>
|
||||
<Form.Control.Feedback
|
||||
className="feedback-error-msg"
|
||||
type="invalid"
|
||||
>
|
||||
The feedback is required
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot is graded 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={true}
|
||||
floatingLabel="Comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot is grading 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
@@ -1,91 +0,0 @@
|
||||
// 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.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>
|
||||
`;
|
||||
|
||||
exports[`Radio Criterion Container snapshot is not grading 1`] = `
|
||||
<React.Fragment>
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="1 points"
|
||||
disabled={true}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={true}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`Radio Criterion Container snapshot radio contain invalid response 1`] = `
|
||||
<React.Fragment>
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="1 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
<Form.Control.Feedback
|
||||
className="feedback-error-msg"
|
||||
type="invalid"
|
||||
>
|
||||
Rubric selection is required
|
||||
</Form.Control.Feedback>
|
||||
</Form.RadioSet>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -1,60 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Review Crition Container snapshot 1`] = `
|
||||
<div
|
||||
className="review-criterion"
|
||||
>
|
||||
<div
|
||||
className="criteria-option"
|
||||
key="option name"
|
||||
>
|
||||
<div>
|
||||
<Form.Label
|
||||
className="option-label"
|
||||
>
|
||||
this label
|
||||
</Form.Label>
|
||||
<FormControlFeedback
|
||||
className="option-points"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="{points} points"
|
||||
description="criterion option point value display"
|
||||
id="ora-grading.RadioCriterion.optionPoints"
|
||||
values={
|
||||
Object {
|
||||
"points": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="criteria-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<div>
|
||||
<Form.Label
|
||||
className="option-label"
|
||||
>
|
||||
this label 2
|
||||
</Form.Label>
|
||||
<FormControlFeedback
|
||||
className="option-points"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="{points} points"
|
||||
description="criterion option point value display"
|
||||
id="ora-grading.RadioCriterion.optionPoints"
|
||||
values={
|
||||
Object {
|
||||
"points": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,144 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Criterion Container snapshot is graded and is not grading 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
prompt
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explaination
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explaination 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div
|
||||
className="rubric-criteria"
|
||||
>
|
||||
<RadioCriterion
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
</div>
|
||||
<CriterionFeedback
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Container snapshot is ungraded and is grading 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
prompt
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explaination
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explaination 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div
|
||||
className="rubric-criteria"
|
||||
>
|
||||
<RadioCriterion
|
||||
isGrading={true}
|
||||
orderNum={1}
|
||||
/>
|
||||
</div>
|
||||
<CriterionFeedback
|
||||
isGrading={true}
|
||||
orderNum={1}
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Container snapshot is ungraded and is not grading 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
prompt
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explaination
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explaination 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div
|
||||
className="rubric-criteria"
|
||||
>
|
||||
<ReviewCriterion
|
||||
orderNum={1}
|
||||
/>
|
||||
</div>
|
||||
<CriterionFeedback
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@edx/paragon';
|
||||
import { Form } from '@openedx/paragon';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { gradeStatuses } from 'data/services/lms/constants';
|
||||
@@ -25,7 +25,7 @@ export const CriterionContainer = (props) => {
|
||||
<span className="criteria-title">{config.prompt}</span>
|
||||
<InfoPopover>
|
||||
{config.options.map((option) => (
|
||||
<div key={option.name} className="help-popover-option">
|
||||
<div key={option.name} className="help-popover-option" data-testid="help-popover-option">
|
||||
<strong>{option.label}</strong>
|
||||
<br />
|
||||
{option.explanation}
|
||||
@@ -33,7 +33,7 @@ export const CriterionContainer = (props) => {
|
||||
))}
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div className="rubric-criteria">
|
||||
<div className="rubric-criteria" data-testid="rubric-criteria">
|
||||
{isGrading || gradeStatus === gradeStatuses.graded ? (
|
||||
<RadioCriterion orderNum={orderNum} isGrading={isGrading} />
|
||||
) : (
|
||||
|
||||
@@ -1,15 +1,50 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { gradeStatuses } from 'data/services/lms/constants';
|
||||
|
||||
import { CriterionContainer, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('components/InfoPopover', () => 'InfoPopover');
|
||||
jest.mock('./RadioCriterion', () => 'RadioCriterion');
|
||||
jest.mock('./CriterionFeedback', () => 'CriterionFeedback');
|
||||
jest.mock('./ReviewCriterion', () => 'ReviewCriterion');
|
||||
const MockRadioCriterion = ({ orderNum, isGrading }) => (
|
||||
<div data-testid="radio-criterion-component">
|
||||
RadioCriterion Component (orderNum={orderNum}, isGrading={String(isGrading)})
|
||||
</div>
|
||||
);
|
||||
|
||||
MockRadioCriterion.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const MockReviewCriterion = ({ orderNum }) => (
|
||||
<div data-testid="review-criterion-component">
|
||||
ReviewCriterion Component (orderNum={orderNum})
|
||||
</div>
|
||||
);
|
||||
|
||||
MockReviewCriterion.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const MockCriterionFeedback = ({ orderNum, isGrading }) => (
|
||||
<div data-testid="criterion-feedback-component">
|
||||
CriterionFeedback Component (orderNum={orderNum}, isGrading={String(isGrading)})
|
||||
</div>
|
||||
);
|
||||
|
||||
MockCriterionFeedback.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const MockInfoPopover = ({ children }) => (
|
||||
<div data-testid="info-popover">{children}</div>
|
||||
);
|
||||
|
||||
MockInfoPopover.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -18,12 +53,18 @@ jest.mock('data/redux/app/selectors', () => ({
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('data/redux/grading/selectors', () => ({
|
||||
selected: {
|
||||
gradeStatus: jest.fn((...args) => ({ selectedGradeStatus: args })),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./RadioCriterion', () => jest.fn((props) => MockRadioCriterion(props)));
|
||||
jest.mock('./ReviewCriterion', () => jest.fn((props) => MockReviewCriterion(props)));
|
||||
jest.mock('./CriterionFeedback', () => jest.fn((props) => MockCriterionFeedback(props)));
|
||||
jest.mock('components/InfoPopover', () => jest.fn((props) => MockInfoPopover(props)));
|
||||
|
||||
describe('Criterion Container', () => {
|
||||
const props = {
|
||||
isGrading: true,
|
||||
@@ -34,14 +75,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',
|
||||
@@ -51,80 +92,62 @@ describe('Criterion Container', () => {
|
||||
},
|
||||
gradeStatus: gradeStatuses.ungraded,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<CriterionContainer {...props} />);
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('is ungraded and is grading', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
describe('component rendering', () => {
|
||||
it('displays the criterion prompt', () => {
|
||||
render(<CriterionContainer {...props} />);
|
||||
expect(screen.getByText('prompt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('is ungraded and is not grading', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
});
|
||||
expect(el).toMatchSnapshot();
|
||||
it('displays all option explanations in the info popover', () => {
|
||||
render(<CriterionContainer {...props} />);
|
||||
const infoPopover = screen.getByTestId('info-popover');
|
||||
expect(infoPopover).toHaveTextContent('explanation');
|
||||
expect(infoPopover).toHaveTextContent('explanation 2');
|
||||
expect(infoPopover).toHaveTextContent('this label');
|
||||
expect(infoPopover).toHaveTextContent('this label 2');
|
||||
});
|
||||
|
||||
test('is graded and is not grading', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
gradeStatus: gradeStatuses.graded,
|
||||
});
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('rendering and all of the option show up', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.find('.help-popover-option');
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl, i) => {
|
||||
expect(optionEl.key()).toEqual(props.config.options[i].name);
|
||||
expect(optionEl.text()).toContain(props.config.options[i].explanation);
|
||||
});
|
||||
it('renders RadioCriterion when is ungraded and is grading', () => {
|
||||
render(<CriterionContainer {...props} />);
|
||||
expect(screen.getByTestId('radio-criterion-component')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('review-criterion-component')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('is ungraded and is grading (Radio criterion get render)', () => {
|
||||
const rubricCritera = el.find('.rubric-criteria');
|
||||
expect(rubricCritera.children(0).name()).toEqual('RadioCriterion');
|
||||
it('renders ReviewCriterion when is ungraded and is not grading', () => {
|
||||
render(<CriterionContainer {...props} isGrading={false} />);
|
||||
expect(screen.getByTestId('review-criterion-component')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('radio-criterion-component')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('is ungraded and is not grading (Review criterion get render)', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
});
|
||||
const rubricCritera = el.find('.rubric-criteria');
|
||||
expect(rubricCritera.children(0).name()).toEqual('ReviewCriterion');
|
||||
it('renders RadioCriterion when is graded and is not grading', () => {
|
||||
render(<CriterionContainer {...props} isGrading={false} gradeStatus={gradeStatuses.graded} />);
|
||||
expect(screen.getByTestId('radio-criterion-component')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('review-criterion-component')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('is graded and is not grading (Radio criterion get render)', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
gradeStatus: gradeStatuses.graded,
|
||||
});
|
||||
const rubricCritera = el.find('.rubric-criteria');
|
||||
expect(rubricCritera.children(0).name()).toEqual('RadioCriterion');
|
||||
it('renders CriterionFeedback component', () => {
|
||||
render(<CriterionContainer {...props} />);
|
||||
expect(screen.getByTestId('criterion-feedback-component')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { abitaryState: 'some data' };
|
||||
const testState = { arbitraryState: 'some data' };
|
||||
const ownProps = { orderNum: props.orderNum };
|
||||
let mapped;
|
||||
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
test('selectors.app.rubric.criterionConfig', () => {
|
||||
|
||||
it('maps rubric criterion config to props', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
|
||||
test('selectors.grading.selected.gradeStatus', () => {
|
||||
it('maps grading status to props', () => {
|
||||
expect(mapped.gradeStatus).toEqual(
|
||||
selectors.grading.selected.gradeStatus(testState),
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { selectors } from 'data/redux';
|
||||
import { DemoWarning, mapStateToProps } from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
@@ -10,24 +10,26 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
let el;
|
||||
|
||||
describe('DemoWarning component', () => {
|
||||
describe('snapshots', () => {
|
||||
test('does not render if disabled flag is missing', () => {
|
||||
el = shallow(<DemoWarning hide />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
describe('behavior', () => {
|
||||
it('does not render when hide prop is true', () => {
|
||||
const { container } = render(<IntlProvider locale="en"><DemoWarning hide /></IntlProvider>);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
test('snapshot: disabled flag is present', () => {
|
||||
el = shallow(<DemoWarning hide={false} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
|
||||
it('renders alert with warning message when hide prop is false', () => {
|
||||
render(<IntlProvider locale="en"><DemoWarning hide={false} /></IntlProvider>);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
expect(alert).toHaveClass('alert-warning');
|
||||
expect(alert).toHaveTextContent(messages.demoModeMessage.defaultMessage);
|
||||
expect(alert).toHaveTextContent(messages.demoModeHeading.defaultMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'test-state' };
|
||||
test('hide is forwarded from app.isEnabled', () => {
|
||||
it('maps hide prop from app.isEnabled selector', () => {
|
||||
const testState = { some: 'test-state' };
|
||||
expect(mapStateToProps(testState).hide).toEqual(
|
||||
selectors.app.isEnabled(testState),
|
||||
);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemoWarning component snapshots does not render if disabled flag is missing 1`] = `""`;
|
||||
|
||||
exports[`DemoWarning component snapshots snapshot: disabled flag is present 1`] = `
|
||||
<Alert
|
||||
className="mb-0 rounded-0"
|
||||
variant="warning"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Demo Mode"
|
||||
description="Demo mode heading"
|
||||
id="ora-grading.ReviewModal.demoHeading"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are demoing the new ORA staff grading experience. You will be unable to submit grades until you activate the feature. This will become the default grading experience on May 9th (05/09/2022). To opt-in early, or opt-out, please contact Partner Support."
|
||||
description="Demo mode message"
|
||||
id="ora-grading.ReviewModal.demoMessage"
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -3,8 +3,8 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Button } from '@edx/paragon';
|
||||
import { Hyperlink, Button } from '@openedx/paragon';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
import emptyStateSVG from './assets/empty-state.svg';
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import urls from 'data/services/lms/urls';
|
||||
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import EmptySubmission from './EmptySubmission';
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
openResponse: (courseId) => `openResponseUrl(${courseId})`,
|
||||
}));
|
||||
|
||||
jest.mock('./assets/emptyState.svg', () => './assets/emptyState.svg');
|
||||
|
||||
let el;
|
||||
jest.mock('./assets/empty-state.svg', () => './assets/empty-state.svg');
|
||||
|
||||
describe('EmptySubmission component', () => {
|
||||
describe('component', () => {
|
||||
const props = { courseId: 'test-course-id' };
|
||||
beforeEach(() => {
|
||||
el = shallow(<EmptySubmission {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('openResponse destination', () => {
|
||||
expect(
|
||||
el.find(Hyperlink).at(0).props().destination,
|
||||
).toEqual(urls.openResponse(props.courseId));
|
||||
});
|
||||
const props = { courseId: 'test-course-id' };
|
||||
|
||||
it('renders the empty state image with correct alt text', () => {
|
||||
renderWithIntl(<EmptySubmission {...props} />);
|
||||
expect(screen.getByAltText('empty state')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the no results found title message', () => {
|
||||
renderWithIntl(<EmptySubmission {...props} />);
|
||||
expect(screen.getByText('Nothing here yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders hyperlink with correct destination URL', () => {
|
||||
renderWithIntl(<EmptySubmission {...props} />);
|
||||
const hyperlink = screen.getByRole('link');
|
||||
expect(hyperlink).toHaveAttribute(
|
||||
'href',
|
||||
urls.openResponse(props.courseId),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the back to responses button', () => {
|
||||
renderWithIntl(<EmptySubmission {...props} />);
|
||||
expect(screen.getByText('Back to all open responses')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, DataTableContext } from '@edx/paragon';
|
||||
import { Button, DataTableContext } from '@openedx/paragon';
|
||||
|
||||
import * as module from './FilterStatusComponent';
|
||||
|
||||
@@ -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 }),
|
||||
{},
|
||||
|
||||
@@ -1,54 +1,18 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import PropTypes from 'prop-types';
|
||||
import { render } from '@testing-library/react';
|
||||
import { DataTableContext } from '@openedx/paragon';
|
||||
|
||||
import * as module from './FilterStatusComponent';
|
||||
|
||||
const fieldIds = [
|
||||
'field-id-0',
|
||||
'field-id-1',
|
||||
'field-id-2',
|
||||
'field-id-3',
|
||||
];
|
||||
const fieldIds = ['field-id-0', 'field-id-1', 'field-id-2', 'field-id-3'];
|
||||
const filterOrder = [1, 0, 3, 2];
|
||||
const filters = filterOrder.map(v => ({ id: fieldIds[v] }));
|
||||
const headers = [0, 1, 2, 3].map(v => ({
|
||||
const filters = filterOrder.map((v) => ({ id: fieldIds[v] }));
|
||||
const headers = [0, 1, 2, 3].map((v) => ({
|
||||
id: fieldIds[v],
|
||||
Header: `HeaDer-${v}`,
|
||||
}));
|
||||
|
||||
describe('FilterStatusComponent hooks', () => {
|
||||
const context = { headers, state: { filters } };
|
||||
const mockTableContext = (newContext) => {
|
||||
React.useContext.mockReturnValueOnce(newContext);
|
||||
};
|
||||
beforeEach(() => {
|
||||
context.setAllFilters = jest.fn();
|
||||
});
|
||||
it('returns empty dict if setAllFilters or state.filters is falsey', () => {
|
||||
mockTableContext({ ...context, setAllFilters: null });
|
||||
expect(module.filterHooks()).toEqual({});
|
||||
mockTableContext({ ...context, state: { filters: null } });
|
||||
expect(module.filterHooks()).toEqual({});
|
||||
});
|
||||
describe('clearFilters', () => {
|
||||
it('uses React.useCallback to clear filters, only once', () => {
|
||||
mockTableContext(context);
|
||||
const { cb, prereqs } = module.filterHooks().clearFilters.useCallback;
|
||||
expect(prereqs).toEqual([]);
|
||||
expect(context.setAllFilters).not.toHaveBeenCalled();
|
||||
cb();
|
||||
expect(context.setAllFilters).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
describe('filterNames', () => {
|
||||
it('returns list of Header values by filter order', () => {
|
||||
mockTableContext(context);
|
||||
expect(module.filterHooks().filterNames).toEqual(
|
||||
filterOrder.map(v => headers[v].Header),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('FilterStatusComponent component', () => {
|
||||
const props = {
|
||||
className: 'css-class-name',
|
||||
@@ -58,34 +22,98 @@ describe('FilterStatusComponent component', () => {
|
||||
buttonClassName: 'css-class-name-for-button',
|
||||
showFilteredFields: true,
|
||||
};
|
||||
const hookProps = {
|
||||
clearFilters: jest.fn().mockName('hookProps.clearFilters'),
|
||||
filterNames: ['filter-name-0', 'filter-name-1'],
|
||||
};
|
||||
const { FilterStatusComponent } = module;
|
||||
const mockHooks = (value) => {
|
||||
jest.spyOn(module, 'filterHooks').mockReturnValueOnce(value);
|
||||
|
||||
const renderWithContext = (contextValue, componentProps = props) => {
|
||||
const TestWrapper = ({ children }) => (
|
||||
<DataTableContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</DataTableContext.Provider>
|
||||
);
|
||||
TestWrapper.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
return render(
|
||||
<TestWrapper>
|
||||
<FilterStatusComponent {...componentProps} />
|
||||
</TestWrapper>,
|
||||
);
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
describe('with filters', () => {
|
||||
test('showFilteredFields', () => {
|
||||
mockHooks(hookProps);
|
||||
const el = shallow(<FilterStatusComponent {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('showFilteredFields=false - hide filterTexts', () => {
|
||||
mockHooks(hookProps);
|
||||
const el = shallow(
|
||||
<FilterStatusComponent {...props} showFilteredFields={false} />,
|
||||
);
|
||||
expect(el).toMatchSnapshot();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('does not render when there are no filters', () => {
|
||||
const contextValue = {
|
||||
headers,
|
||||
state: { filters: null },
|
||||
setAllFilters: jest.fn(),
|
||||
};
|
||||
const { container } = renderWithContext(contextValue);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('does not render when setAllFilters is not available', () => {
|
||||
const contextValue = { headers, state: { filters }, setAllFilters: null };
|
||||
const { container } = renderWithContext(contextValue);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders clear filters button with correct text when filters exist', () => {
|
||||
const contextValue = {
|
||||
headers,
|
||||
state: { filters },
|
||||
setAllFilters: jest.fn(),
|
||||
};
|
||||
const { getByText } = renderWithContext(contextValue);
|
||||
expect(getByText(props.clearFiltersText)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays filtered field names when showFilteredFields is true', () => {
|
||||
const contextValue = {
|
||||
headers,
|
||||
state: { filters },
|
||||
setAllFilters: jest.fn(),
|
||||
};
|
||||
const { getByText } = renderWithContext(contextValue);
|
||||
const expectedFilterNames = filterOrder.map((v) => headers[v].Header);
|
||||
expectedFilterNames.forEach((name) => {
|
||||
expect(getByText(name, { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
test('without filters', () => {
|
||||
mockHooks({});
|
||||
const el = shallow(<FilterStatusComponent {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
|
||||
it('does not display filtered field names when showFilteredFields is false', () => {
|
||||
const contextValue = {
|
||||
headers,
|
||||
state: { filters },
|
||||
setAllFilters: jest.fn(),
|
||||
};
|
||||
const { queryByText } = renderWithContext(contextValue, {
|
||||
...props,
|
||||
showFilteredFields: false,
|
||||
});
|
||||
expect(queryByText(/Filtered by/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies correct CSS classes to the component', () => {
|
||||
const contextValue = {
|
||||
headers,
|
||||
state: { filters },
|
||||
setAllFilters: jest.fn(),
|
||||
};
|
||||
const { container } = renderWithContext(contextValue);
|
||||
expect(container.firstChild).toHaveClass(props.className);
|
||||
});
|
||||
|
||||
it('calls setAllFilters with empty array when clear button is clicked', () => {
|
||||
const setAllFilters = jest.fn();
|
||||
const contextValue = { headers, state: { filters }, setAllFilters };
|
||||
const { getByText } = renderWithContext(contextValue);
|
||||
const clearButton = getByText(props.clearFiltersText);
|
||||
clearButton.click();
|
||||
expect(setAllFilters).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
Alert,
|
||||
Button,
|
||||
Hyperlink,
|
||||
} from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import {
|
||||
ListError,
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
} from './ListError';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { ListError, mapDispatchToProps, mapStateToProps } from './ListError';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
courseId: (...args) => ({ courseId: args }),
|
||||
courseId: jest.fn((state) => state.courseId || 'test-course-id'),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
@@ -27,41 +22,60 @@ jest.mock('data/services/lms/urls', () => ({
|
||||
openResponse: (courseId) => `api/openResponse/${courseId}`,
|
||||
}));
|
||||
|
||||
let el;
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
describe('ListError component', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
courseId: 'test-course-id',
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.loadSelectionForReview = jest.fn();
|
||||
props.intl = { formatMessage };
|
||||
props.initializeApp = jest.fn();
|
||||
const props = {
|
||||
courseId: 'test-course-id',
|
||||
initializeApp: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('renders error alert with proper styling', () => {
|
||||
renderWithIntl(<ListError {...props} />);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
expect(alert).toHaveClass('alert-danger');
|
||||
});
|
||||
describe('render tests', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<ListError {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('displays error heading and message', () => {
|
||||
renderWithIntl(<ListError {...props} />);
|
||||
const heading = screen.getByRole('alert').querySelector('.alert-heading');
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent(messages.loadErrorHeading.defaultMessage);
|
||||
});
|
||||
|
||||
it('displays try again button', () => {
|
||||
renderWithIntl(<ListError {...props} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveClass('btn-primary');
|
||||
});
|
||||
|
||||
it('calls initializeApp when try again button is clicked', async () => {
|
||||
renderWithIntl(<ListError {...props} />);
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button');
|
||||
await user.click(button);
|
||||
expect(props.initializeApp).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('courseId loads from app.courseId', () => {
|
||||
it('maps courseId from app.courseId selector', () => {
|
||||
const mapped = mapStateToProps(testState);
|
||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads initializeApp from thunkActions.app.initialize', () => {
|
||||
expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize);
|
||||
it('maps initializeApp from thunkActions.app.initialize', () => {
|
||||
expect(mapDispatchToProps.initializeApp).toEqual(
|
||||
thunkActions.app.initialize,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user