Compare commits
255 Commits
jkantor/ti
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4d7d95e490 | ||
|
|
0a90024de9 | ||
|
|
91d06e9788 | ||
|
|
74423bf359 | ||
|
|
7e9eab24b0 | ||
|
|
91dd10917f |
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
|
||||
|
||||
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
#check package-lock file version
|
||||
|
||||
name: Lockfile Version check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
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}
|
||||
71344
package-lock.json
generated
71344
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
83
package.json
83
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,68 +27,66 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
|
||||
"@edx/frontend-component-footer": "10.1.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.6.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us" dir="ltr">
|
||||
<head>
|
||||
<title>ORA Enhanced Staff Grader | <%= process.env.SITE_NAME %></title>
|
||||
<title>ORA staff grading | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
|
||||
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"]
|
||||
}
|
||||
13
src/App.jsx
13
src/App.jsx
@@ -3,29 +3,32 @@ 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';
|
||||
|
||||
import DemoWarning from 'containers/DemoWarning';
|
||||
import CourseHeader from 'containers/CourseHeader';
|
||||
import ListView from 'containers/ListView';
|
||||
|
||||
import './App.scss';
|
||||
import Head from './components/Head';
|
||||
|
||||
export const App = ({ courseMetadata, isEnabled }) => (
|
||||
<Router>
|
||||
<div>
|
||||
<CourseHeader
|
||||
<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>
|
||||
);
|
||||
|
||||
49
src/App.scss
49
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;
|
||||
@@ -42,39 +40,28 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
|
||||
}
|
||||
}
|
||||
|
||||
.course-header {
|
||||
min-width: 0;
|
||||
border-bottom: 1px solid black;
|
||||
|
||||
.course-title-lockup {
|
||||
min-width: 0;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-bottom: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
.btn {
|
||||
height: 3rem;
|
||||
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#paragon-portal-root {
|
||||
.pgn__modal-layer {
|
||||
.pgn__modal-close-container {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
126
src/App.test.jsx
126
src/App.test.jsx
@@ -1,61 +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 { 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-footer', () => 'Footer');
|
||||
|
||||
jest.mock('containers/DemoWarning', () => 'DemoWarning');
|
||||
jest.mock('containers/ListView', () => 'ListView');
|
||||
jest.mock('containers/CourseHeader', () => 'CourseHeader');
|
||||
|
||||
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();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('snapshot: disabled (show demo warning)', () => {
|
||||
expect(shallow(<App {...props} isEnabled={false} />)).toMatchSnapshot();
|
||||
|
||||
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();
|
||||
});
|
||||
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);
|
||||
|
||||
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>
|
||||
<CourseHeader
|
||||
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>
|
||||
<CourseHeader
|
||||
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 };
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import message from './messages';
|
||||
|
||||
export const getRegisterUrl = () => {
|
||||
const { LMS_BASE_URL } = getConfig();
|
||||
const locationHref = encodeURIComponent(global.location.href);
|
||||
return `${LMS_BASE_URL}/register?next=${locationHref}`;
|
||||
};
|
||||
|
||||
export const AnonymousUserMenu = ({ intl }) => (
|
||||
<div>
|
||||
<Button
|
||||
className="mr-3"
|
||||
variant="outline-primary"
|
||||
href={getRegisterUrl()}
|
||||
>
|
||||
{intl.formatMessage(message.registerSentenceCase)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
href={`${getLoginRedirectUrl(global.location.href)}`}
|
||||
>
|
||||
{intl.formatMessage(message.signInSentenceCase)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
AnonymousUserMenu.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AnonymousUserMenu);
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { AnonymousUserMenu } from './AnonymousUserMenu';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: () => ({
|
||||
LMS_BASE_URL: '<LMS_BASE_URL>',
|
||||
}),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getLoginRedirectUrl: (url) => `redirect:${url}`,
|
||||
}));
|
||||
|
||||
describe('Header AnonymousUserMenu component', () => {
|
||||
const props = {
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
};
|
||||
test('snapshot', () => {
|
||||
expect(
|
||||
shallow(<AnonymousUserMenu {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
|
||||
export const UserAvatar = ({ username }) => (
|
||||
<Dropdown.Toggle variant="outline-primary">
|
||||
<FontAwesomeIcon
|
||||
icon={faUserCircle}
|
||||
className="d-md-none"
|
||||
size="lg"
|
||||
/>
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{username}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
);
|
||||
UserAvatar.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
UserAvatar.defaultProps = {};
|
||||
|
||||
export default UserAvatar;
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import UserAvatar from './UserAvatar';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: () => ({
|
||||
LMS_BASE_URL: '<LMS_BASE_URL>',
|
||||
LOGOUT_URL: '<LOGOUT_URL>',
|
||||
SUPPORT_URL: '<SUPPORT_URL>',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Header AuthenticatedUserDropdown UserAvatar component', () => {
|
||||
const props = {
|
||||
username: 'test-username',
|
||||
};
|
||||
test('snapshot', () => {
|
||||
expect(
|
||||
shallow(<UserAvatar {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
export class UserMenu extends React.Component {
|
||||
menuItem(href, message) {
|
||||
return (
|
||||
<Dropdown.Item href={href}>
|
||||
{this.props.intl.formatMessage(message)}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { username } = this.props;
|
||||
const { LMS_BASE_URL, LOGOUT_URL } = getConfig();
|
||||
return (
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
{this.menuItem(`${LMS_BASE_URL}/dashboard`, messages.dashboard)}
|
||||
{this.menuItem(`${LMS_BASE_URL}/u/${username}`, messages.profile)}
|
||||
{this.menuItem(`${LMS_BASE_URL}/account/settings`, messages.account)}
|
||||
{this.menuItem(LOGOUT_URL, messages.signOut)}
|
||||
</Dropdown.Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserMenu.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
UserMenu.defaultProps = {};
|
||||
|
||||
export default injectIntl(UserMenu);
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { UserMenu } from './UserMenu';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: () => ({
|
||||
LMS_BASE_URL: '<LMS_BASE_URL>',
|
||||
LOGOUT_URL: '<LOGOUT_URL>',
|
||||
SUPPORT_URL: '<SUPPORT_URL>',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Header AuthenticatedUserDropdown UserMenu component', () => {
|
||||
const props = {
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
username: 'test-username',
|
||||
};
|
||||
test('snapshot', () => {
|
||||
expect(
|
||||
shallow(<UserMenu {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header AuthenticatedUserDropdown UserAvatar component snapshot 1`] = `
|
||||
<Dropdown.Toggle
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="d-md-none"
|
||||
icon={
|
||||
Object {
|
||||
"icon": Array [
|
||||
496,
|
||||
512,
|
||||
Array [],
|
||||
"f2bd",
|
||||
"M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 96c48.6 0 88 39.4 88 88s-39.4 88-88 88-88-39.4-88-88 39.4-88 88-88zm0 344c-58.7 0-111.3-26.6-146.5-68.2 18.8-35.4 55.6-59.8 98.5-59.8 2.4 0 4.8.4 7.1 1.1 13 4.2 26.6 6.9 40.9 6.9 14.3 0 28-2.7 40.9-6.9 2.3-.7 4.7-1.1 7.1-1.1 42.9 0 79.7 24.4 98.5 59.8C359.3 421.4 306.7 448 248 448z",
|
||||
],
|
||||
"iconName": "user-circle",
|
||||
"prefix": "fas",
|
||||
}
|
||||
}
|
||||
size="lg"
|
||||
/>
|
||||
<span
|
||||
className="d-none d-md-inline"
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
test-username
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
`;
|
||||
@@ -1,28 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header AuthenticatedUserDropdown UserMenu component snapshot 1`] = `
|
||||
<Dropdown.Menu
|
||||
className="dropdown-menu-right"
|
||||
>
|
||||
<Dropdown.Item
|
||||
href="<LMS_BASE_URL>/dashboard"
|
||||
>
|
||||
Dashboard
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="<LMS_BASE_URL>/u/test-username"
|
||||
>
|
||||
Profile
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="<LMS_BASE_URL>/account/settings"
|
||||
>
|
||||
Account
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="<LOGOUT_URL>"
|
||||
>
|
||||
Sign Out
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
`;
|
||||
@@ -1,22 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header AuthenticatedUserDropdown component snapshot 1`] = `
|
||||
<Fragment>
|
||||
<a
|
||||
className="text-gray-700 mr-3"
|
||||
href="<SUPPORT_URL>"
|
||||
>
|
||||
Help
|
||||
</a>
|
||||
<Dropdown
|
||||
className="user-dropdown"
|
||||
>
|
||||
<UserAvatar
|
||||
username="test-username"
|
||||
/>
|
||||
<UserMenu
|
||||
username="test-username"
|
||||
/>
|
||||
</Dropdown>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
|
||||
import UserMenu from './UserMenu';
|
||||
import UserAvatar from './UserAvatar';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
export const AuthenticatedUserDropdown = ({
|
||||
intl,
|
||||
username,
|
||||
}) => (
|
||||
<>
|
||||
<a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>
|
||||
{intl.formatMessage(messages.help)}
|
||||
</a>
|
||||
<Dropdown className="user-dropdown">
|
||||
<UserAvatar username={username} />
|
||||
<UserMenu username={username} />
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
|
||||
AuthenticatedUserDropdown.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
AuthenticatedUserDropdown.defaultProps = {};
|
||||
|
||||
export default injectIntl(AuthenticatedUserDropdown);
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { AuthenticatedUserDropdown } from '.';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: () => ({
|
||||
SUPPORT_URL: '<SUPPORT_URL>',
|
||||
}),
|
||||
}));
|
||||
jest.mock('./UserAvatar', () => 'UserAvatar');
|
||||
jest.mock('./UserMenu', () => 'UserMenu');
|
||||
|
||||
describe('Header AuthenticatedUserDropdown component', () => {
|
||||
const props = {
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
username: 'test-username',
|
||||
};
|
||||
test('snapshot', () => {
|
||||
expect(
|
||||
shallow(<AuthenticatedUserDropdown {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const CourseLabel = ({
|
||||
courseOrg,
|
||||
courseNumber,
|
||||
courseTitle,
|
||||
}) => (
|
||||
<div
|
||||
className="flex-grow-1 course-title-lockup"
|
||||
style={{ lineHeight: 1 }}
|
||||
>
|
||||
<span className="d-block small m-0">
|
||||
{courseOrg} {courseNumber}
|
||||
</span>
|
||||
<span className="d-block m-0 font-weight-bold course-title">
|
||||
{courseTitle}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
CourseLabel.propTypes = {
|
||||
courseOrg: PropTypes.string,
|
||||
courseNumber: PropTypes.string,
|
||||
courseTitle: PropTypes.string,
|
||||
};
|
||||
CourseLabel.defaultProps = {
|
||||
courseOrg: null,
|
||||
courseNumber: null,
|
||||
courseTitle: null,
|
||||
};
|
||||
|
||||
export default CourseLabel;
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import CourseLabel from './CourseLabel';
|
||||
|
||||
const courseData = {
|
||||
courseOrg: 'course-org',
|
||||
courseNumber: 'course-number',
|
||||
courseTitle: 'course-title',
|
||||
};
|
||||
|
||||
describe('Header CourseLabel component', () => {
|
||||
test('snapshot', () => {
|
||||
expect(
|
||||
shallow(<CourseLabel {...courseData} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
const LinkedLogo = () => (
|
||||
<a
|
||||
className="logo"
|
||||
href={`${getConfig().LMS_BASE_URL}/dashboard`}
|
||||
>
|
||||
<img
|
||||
className="d-block"
|
||||
src={getConfig().LOGO_URL}
|
||||
alt={getConfig().SITE_NAME}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
||||
export default LinkedLogo;
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import LinkedLogo from './LinkedLogo';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: () => ({
|
||||
LMS_BASE_URL: '<getConfig().LMS_BASE_URL>',
|
||||
LOGO_URL: '<getConfig().LOGO_URL>',
|
||||
SITE_NAME: '<getConfig().SITE_NAME>',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Header CourseLabel component', () => {
|
||||
test('snapshot', () => {
|
||||
expect(
|
||||
shallow(<LinkedLogo />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header AnonymousUserMenu component snapshot 1`] = `
|
||||
<div>
|
||||
<Button
|
||||
className="mr-3"
|
||||
href="<LMS_BASE_URL>/register?next=http%3A%2F%2Flocalhost%2F"
|
||||
variant="outline-primary"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
<Button
|
||||
href="redirect:http://localhost/"
|
||||
variant="primary"
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,25 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header CourseLabel component snapshot 1`] = `
|
||||
<div
|
||||
className="flex-grow-1 course-title-lockup"
|
||||
style={
|
||||
Object {
|
||||
"lineHeight": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="d-block small m-0"
|
||||
>
|
||||
course-org
|
||||
|
||||
course-number
|
||||
</span>
|
||||
<span
|
||||
className="d-block m-0 font-weight-bold course-title"
|
||||
>
|
||||
course-title
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,14 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header CourseLabel component snapshot 1`] = `
|
||||
<a
|
||||
className="logo"
|
||||
href="<getConfig().LMS_BASE_URL>/dashboard"
|
||||
>
|
||||
<img
|
||||
alt="<getConfig().SITE_NAME>"
|
||||
className="d-block"
|
||||
src="<getConfig().LOGO_URL>"
|
||||
/>
|
||||
</a>
|
||||
`;
|
||||
@@ -1,51 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header component snapshot 1`] = `
|
||||
<header
|
||||
className="course-header"
|
||||
>
|
||||
<a
|
||||
className="sr-only sr-only-focusable"
|
||||
href="#main-content"
|
||||
>
|
||||
Skip to main content.
|
||||
</a>
|
||||
<div
|
||||
className="container-xl py-2 d-flex align-items-center"
|
||||
>
|
||||
<LinkedLogo />
|
||||
<CourseLabel
|
||||
courseNumber="course-number"
|
||||
courseOrg="course-org"
|
||||
courseTitle="course-title"
|
||||
/>
|
||||
<AnonymousUserMenu />
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
|
||||
exports[`Header component snapshot with authenticatedUser 1`] = `
|
||||
<header
|
||||
className="course-header"
|
||||
>
|
||||
<a
|
||||
className="sr-only sr-only-focusable"
|
||||
href="#main-content"
|
||||
>
|
||||
Skip to main content.
|
||||
</a>
|
||||
<div
|
||||
className="container-xl py-2 d-flex align-items-center"
|
||||
>
|
||||
<LinkedLogo />
|
||||
<CourseLabel
|
||||
courseNumber="course-number"
|
||||
courseOrg="course-org"
|
||||
courseTitle="course-title"
|
||||
/>
|
||||
<AuthenticatedUserDropdown
|
||||
username="test"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
@@ -1,47 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import AnonymousUserMenu from './AnonymousUserMenu';
|
||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||
import LinkedLogo from './LinkedLogo';
|
||||
import CourseLabel from './CourseLabel';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const Header = ({
|
||||
courseOrg,
|
||||
courseNumber,
|
||||
courseTitle,
|
||||
intl,
|
||||
}) => {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
return (
|
||||
<header className="course-header">
|
||||
<a className="sr-only sr-only-focusable" href="#main-content">
|
||||
{intl.formatMessage(messages.skipNavLink)}
|
||||
</a>
|
||||
<div className="container-xl py-2 d-flex align-items-center">
|
||||
<LinkedLogo />
|
||||
<CourseLabel {...{ courseOrg, courseNumber, courseTitle }} />
|
||||
{authenticatedUser
|
||||
? (<AuthenticatedUserDropdown username={authenticatedUser.username} />)
|
||||
: (<AnonymousUserMenu />)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
Header.propTypes = {
|
||||
courseOrg: PropTypes.string,
|
||||
courseNumber: PropTypes.string,
|
||||
courseTitle: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
Header.defaultProps = {
|
||||
courseOrg: null,
|
||||
courseNumber: null,
|
||||
courseTitle: null,
|
||||
};
|
||||
|
||||
export default injectIntl(Header);
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { Header } from '.';
|
||||
|
||||
jest.mock('./AnonymousUserMenu', () => 'AnonymousUserMenu');
|
||||
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
|
||||
jest.mock('./LinkedLogo', () => 'LinkedLogo');
|
||||
jest.mock('./CourseLabel', () => 'CourseLabel');
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppContext: { authenticatedUser: null },
|
||||
}));
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useContext: (context) => context,
|
||||
}));
|
||||
|
||||
const courseData = {
|
||||
courseOrg: 'course-org',
|
||||
courseNumber: 'course-number',
|
||||
courseTitle: 'course-title',
|
||||
};
|
||||
|
||||
describe('Header component', () => {
|
||||
const props = {
|
||||
...courseData,
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
};
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<Header {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot with authenticatedUser', () => {
|
||||
AppContext.authenticatedUser = { username: 'test' };
|
||||
expect(shallow(<Header {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user