Compare commits
42 Commits
jenkins/ve
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54c6c57b42 | ||
|
|
6722dbdce5 | ||
|
|
fed06641df | ||
|
|
689b8b48f0 | ||
|
|
258f4377d8 | ||
|
|
dcdc96778c | ||
|
|
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 |
2
.env
2
.env
@@ -30,3 +30,5 @@ ENTERPRISE_MARKETING_URL=''
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -36,3 +36,5 @@ ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -4,8 +4,13 @@ const config = createConfig('eslint', {
|
||||
rules: {
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-import-module-exports': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
"react/forbid-prop-types": ["error", { "forbid": ["any", "array"] }], // arguable object proptype is use when I do not care about the shape of the object
|
||||
'no-import-assign': 'off',
|
||||
'no-promise-executor-return': 'off',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
6
.github/CODEOWNERS
vendored
Normal file
6
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Code owners for frontend-app-ora-grading
|
||||
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence, they will
|
||||
# be requested for review when someone opens a pull request.
|
||||
* @edx/content-aurora
|
||||
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
|
||||
|
||||
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -11,17 +11,16 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14, 16]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
# Because of node 18 bug (https://github.com/nodejs/node/issues/47563), Pinning node version 18.15 until the next release of node
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version: 18.15
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -39,7 +38,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
|
||||
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -7,4 +7,4 @@ on:
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
||||
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
|
||||
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
|
||||
14
Makefile
14
Makefile
@@ -3,18 +3,16 @@ npm-install-%: ## install specified % npm package
|
||||
git add package.json
|
||||
|
||||
transifex_resource = frontend-app-ora-grading
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
transifex_langs = "ar,fr,es_419,zh_CN,fr_CA,it_IT,pt_PT,de_DE,uk,ru,hi"
|
||||
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test is-es5
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
.PHONY: test
|
||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||
@@ -49,15 +47,15 @@ push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
21
README.md
Normal file
21
README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# frontend-app-ora-grading
|
||||
|
||||
The ORA Staff Grading App is a microfrontend (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.
|
||||
|
||||
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.
|
||||
|
||||
## Quickstart
|
||||
|
||||
To start the MFE and enable the feature in LMS:
|
||||
|
||||
1. Start the MFE with `npm run start`. Take a note of the path/port (defaults to `http://localhost:1993`).
|
||||
|
||||
2. Add the route root to `edx-platform` settings: In `edx-platform/lms/envs/private.py` or similar, add `ORA_GRADING_MICROFRONTEND_URL = 'http://localhost:1993'`
|
||||
|
||||
3. Enable the feature: In Django Admin go to django-waffle > Flags and add/enable a new flag called `openresponseassessment.enhanced_staff_grader`.
|
||||
|
||||
From there, visit the new experience by going to the Instructor Dashboard > Open Responses or an ORA with a Staff Graded Step and click a link to begin grading.
|
||||
|
||||
## Resources
|
||||
|
||||
See the [ORA Staff Grading](https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/open_response_assessments/ORA_Staff_Grading.html#ora-staff-grading) section on ReadTheDocs for usage information.
|
||||
@@ -12,6 +12,9 @@ module.exports = createConfig('jest', {
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
'testUtils', // don't unit test jest mocking tools
|
||||
'src/data/services/lms/fakeData', // don't unit test mock data
|
||||
'src/test', // don't unit test integration test utils
|
||||
],
|
||||
testTimeout: 120000,
|
||||
testEnvironment: 'jsdom',
|
||||
|
||||
24909
package-lock.json
generated
24909
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -8,8 +8,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
@@ -27,10 +25,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
|
||||
"@edx/frontend-component-footer": "10.1.6",
|
||||
"@edx/frontend-component-header": "^2.4.6",
|
||||
"@edx/frontend-platform": "^1.15.6",
|
||||
"@edx/paragon": "16.14.4",
|
||||
"@edx/frontend-component-footer": "^11.1.1",
|
||||
"@edx/frontend-component-header": "^3.1.1",
|
||||
"@edx/frontend-platform": "^2.5.1",
|
||||
"@edx/paragon": "^19.9.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
@@ -52,10 +50,12 @@
|
||||
"history": "5.0.1",
|
||||
"html-react-parser": "^1.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.3",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "7.0.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^5.20.9",
|
||||
"react-pdf": "^5.5.0",
|
||||
"react-redux": "^7.2.4",
|
||||
@@ -73,22 +73,21 @@
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "^9.1.4",
|
||||
"@edx/frontend-build": "12.4",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.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-expect-message": "^1.0.2",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-test-renderer": "^16.14.0",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"semantic-release": "^17.4.5"
|
||||
"semantic-release": "^19.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,23 @@ import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import DemoWarning from 'containers/DemoWarning';
|
||||
import CTA from 'containers/CTA';
|
||||
import ListView from 'containers/ListView';
|
||||
|
||||
import './App.scss';
|
||||
import Head from './components/Head';
|
||||
|
||||
export const App = ({ courseMetadata, isEnabled }) => (
|
||||
<Router>
|
||||
<div>
|
||||
<Head />
|
||||
<Header
|
||||
courseTitle={courseMetadata.title}
|
||||
courseNumber={courseMetadata.number}
|
||||
courseOrg={courseMetadata.org}
|
||||
/>
|
||||
{!isEnabled && <DemoWarning />}
|
||||
<CTA />
|
||||
<main>
|
||||
<ListView />
|
||||
</main>
|
||||
|
||||
@@ -23,7 +23,9 @@ jest.mock('@edx/frontend-component-header', () => ({
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
|
||||
jest.mock('containers/DemoWarning', () => 'DemoWarning');
|
||||
jest.mock('containers/CTA', () => 'CTA');
|
||||
jest.mock('containers/ListView', () => 'ListView');
|
||||
jest.mock('components/Head', () => 'Head');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
exports[`App router component snapshot: disabled (show demo warning) 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Head />
|
||||
<Header
|
||||
courseNumber="course-number"
|
||||
courseOrg="course-org"
|
||||
courseTitle="course-title"
|
||||
/>
|
||||
<DemoWarning />
|
||||
<CTA />
|
||||
<main>
|
||||
<ListView />
|
||||
</main>
|
||||
@@ -22,11 +24,13 @@ exports[`App router component snapshot: disabled (show demo warning) 1`] = `
|
||||
exports[`App router component snapshot: enabled 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Head />
|
||||
<Header
|
||||
courseNumber="course-number"
|
||||
courseOrg="course-org"
|
||||
courseTitle="course-title"
|
||||
/>
|
||||
<CTA />
|
||||
<main>
|
||||
<ListView />
|
||||
</main>
|
||||
|
||||
35
src/__snapshots__/index.test.jsx.snap
Normal file
35
src/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,35 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<IntlProvider
|
||||
defaultFormats={Object {}}
|
||||
defaultLocale="en"
|
||||
fallbackOnEmptyString={true}
|
||||
formats={Object {}}
|
||||
locale="en"
|
||||
messages={Object {}}
|
||||
onError={[Function]}
|
||||
onWarn={[Function]}
|
||||
textComponent={Symbol(react.fragment)}
|
||||
>
|
||||
<AppProvider
|
||||
store={
|
||||
Object {
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(Symbol.observable): [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<App />
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
`;
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { AlertModal, ActionRow, Button } from '@edx/paragon';
|
||||
import { nullMethod } from 'hooks';
|
||||
|
||||
export const ConfirmModal = ({
|
||||
title,
|
||||
@@ -15,7 +16,7 @@ export const ConfirmModal = ({
|
||||
<AlertModal
|
||||
className="confirm-modal"
|
||||
title={title}
|
||||
onClose={() => ({})}
|
||||
onClose={nullMethod}
|
||||
isOpen={isOpen}
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
|
||||
@@ -9,141 +9,77 @@ import { ChevronLeft, ChevronRight } from '@edx/paragon/icons';
|
||||
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
|
||||
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
import { rendererHooks } from './pdfHooks';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
|
||||
|
||||
/**
|
||||
* <PDFRenderer />
|
||||
*/
|
||||
export class PDFRenderer extends React.Component {
|
||||
static INITIAL_STATE = {
|
||||
pageNumber: 1,
|
||||
numPages: 1,
|
||||
relativeHeight: 0,
|
||||
};
|
||||
export const PDFRenderer = ({
|
||||
onError,
|
||||
onSuccess,
|
||||
url,
|
||||
}) => {
|
||||
const {
|
||||
pageNumber,
|
||||
numPages,
|
||||
relativeHeight,
|
||||
wrapperRef,
|
||||
onDocumentLoadSuccess,
|
||||
onLoadPageSuccess,
|
||||
onDocumentLoadError,
|
||||
onInputPageChange,
|
||||
onNextPageButtonClick,
|
||||
onPrevPageButtonClick,
|
||||
hasNext,
|
||||
hasPrev,
|
||||
} = rendererHooks({ onError, onSuccess });
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { ...PDFRenderer.INITIAL_STATE };
|
||||
|
||||
this.wrapperRef = React.createRef();
|
||||
this.onDocumentLoadSuccess = this.onDocumentLoadSuccess.bind(this);
|
||||
this.onDocumentLoadError = this.onDocumentLoadError.bind(this);
|
||||
this.onLoadPageSuccess = this.onLoadPageSuccess.bind(this);
|
||||
this.onPrevPageButtonClick = this.onPrevPageButtonClick.bind(this);
|
||||
this.onNextPageButtonClick = this.onNextPageButtonClick.bind(this);
|
||||
this.onInputPageChange = this.onInputPageChange.bind(this);
|
||||
}
|
||||
|
||||
onDocumentLoadSuccess = ({ numPages }) => {
|
||||
this.props.onSuccess();
|
||||
this.setState({ numPages });
|
||||
};
|
||||
|
||||
onLoadPageSuccess = (page) => {
|
||||
const pageWidth = page.view[2];
|
||||
const pageHeight = page.view[3];
|
||||
const wrapperHeight = this.wrapperRef.current.getBoundingClientRect().width;
|
||||
const relativeHeight = (wrapperHeight * pageHeight) / pageWidth;
|
||||
if (relativeHeight !== this.state.relativeHeight) {
|
||||
this.setState({ relativeHeight });
|
||||
}
|
||||
};
|
||||
|
||||
onDocumentLoadError = (error) => {
|
||||
let status;
|
||||
switch (error.name) {
|
||||
case 'MissingPDFException':
|
||||
status = 404;
|
||||
break;
|
||||
default:
|
||||
status = 500;
|
||||
break;
|
||||
}
|
||||
this.props.onError(status);
|
||||
};
|
||||
|
||||
onInputPageChange = ({ target: { value } }) => {
|
||||
this.setPageNumber(parseInt(value, 10));
|
||||
}
|
||||
|
||||
onPrevPageButtonClick = () => {
|
||||
this.setPageNumber(this.state.pageNumber - 1);
|
||||
}
|
||||
|
||||
onNextPageButtonClick = () => {
|
||||
this.setPageNumber(this.state.pageNumber + 1);
|
||||
}
|
||||
|
||||
setPageNumber(pageNumber) {
|
||||
if (pageNumber > 0 && pageNumber <= this.state.numPages) {
|
||||
this.setState({ pageNumber });
|
||||
}
|
||||
}
|
||||
|
||||
get hasNext() {
|
||||
return this.state.pageNumber < this.state.numPages;
|
||||
}
|
||||
|
||||
get hasPrev() {
|
||||
return this.state.pageNumber > 1;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.wrapperRef} className="pdf-renderer">
|
||||
<Document
|
||||
file={this.props.url}
|
||||
onLoadSuccess={this.onDocumentLoadSuccess}
|
||||
onLoadError={this.onDocumentLoadError}
|
||||
>
|
||||
{/* <Outline /> */}
|
||||
<div
|
||||
className="page-wrapper"
|
||||
style={{
|
||||
height: this.state.relativeHeight,
|
||||
}}
|
||||
>
|
||||
<Page
|
||||
pageNumber={this.state.pageNumber}
|
||||
onLoadSuccess={this.onLoadPageSuccess}
|
||||
/>
|
||||
</div>
|
||||
</Document>
|
||||
<ActionRow className="d-flex justify-content-center m-0">
|
||||
<IconButton
|
||||
size="inline"
|
||||
alt="previous pdf page"
|
||||
iconAs={Icon}
|
||||
src={ChevronLeft}
|
||||
disabled={!this.hasPrev}
|
||||
onClick={this.onPrevPageButtonClick}
|
||||
return (
|
||||
<div ref={wrapperRef} className="pdf-renderer">
|
||||
<Document
|
||||
file={url}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
onLoadError={onDocumentLoadError}
|
||||
>
|
||||
{/* <Outline /> */}
|
||||
<div className="page-wrapper" style={{ height: relativeHeight }}>
|
||||
<Page pageNumber={pageNumber} onLoadSuccess={onLoadPageSuccess} />
|
||||
</div>
|
||||
</Document>
|
||||
<ActionRow className="d-flex justify-content-center m-0">
|
||||
<IconButton
|
||||
size="inline"
|
||||
alt="previous pdf page"
|
||||
iconAs={Icon}
|
||||
src={ChevronLeft}
|
||||
disabled={!hasPrev}
|
||||
onClick={onPrevPageButtonClick}
|
||||
/>
|
||||
<Form.Group className="d-flex align-items-center m-0">
|
||||
<Form.Label isInline>Page </Form.Label>
|
||||
<Form.Control
|
||||
type="number"
|
||||
min={0}
|
||||
max={numPages}
|
||||
value={pageNumber}
|
||||
onChange={onInputPageChange}
|
||||
/>
|
||||
<Form.Group className="d-flex align-items-center m-0">
|
||||
<Form.Label isInline>Page </Form.Label>
|
||||
<Form.Control
|
||||
type="number"
|
||||
min={0}
|
||||
max={this.state.numPages}
|
||||
value={this.state.pageNumber}
|
||||
onChange={this.onInputPageChange}
|
||||
/>
|
||||
<Form.Label isInline> of {this.state.numPages}</Form.Label>
|
||||
</Form.Group>
|
||||
<IconButton
|
||||
size="inline"
|
||||
alt="next pdf page"
|
||||
iconAs={Icon}
|
||||
src={ChevronRight}
|
||||
disabled={!this.hasNext}
|
||||
onClick={this.onNextPageButtonClick}
|
||||
/>
|
||||
</ActionRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
<Form.Label isInline> of {numPages}</Form.Label>
|
||||
</Form.Group>
|
||||
<IconButton
|
||||
size="inline"
|
||||
alt="next pdf page"
|
||||
iconAs={Icon}
|
||||
src={ChevronRight}
|
||||
disabled={!hasNext}
|
||||
onClick={onNextPageButtonClick}
|
||||
/>
|
||||
</ActionRow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PDFRenderer.defaultProps = {};
|
||||
|
||||
|
||||
@@ -1,221 +1,57 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import { Form, IconButton } from '@edx/paragon';
|
||||
|
||||
import PDFRenderer from './PDFRenderer';
|
||||
|
||||
import * as hooks from './pdfHooks';
|
||||
|
||||
jest.mock('react-pdf', () => ({
|
||||
pdfjs: { GlobalWorkerOptions: {} },
|
||||
Document: () => 'Document',
|
||||
Page: () => 'Page',
|
||||
}));
|
||||
|
||||
jest.mock('./pdfHooks', () => ({
|
||||
rendererHooks: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('PDF Renderer Component', () => {
|
||||
const props = {
|
||||
url: 'some_url.pdf',
|
||||
onError: jest.fn().mockName('this.props.onError'),
|
||||
onSuccess: jest.fn().mockName('this.props.onSuccess'),
|
||||
};
|
||||
const hookProps = {
|
||||
pageNumber: 1,
|
||||
numPages: 10,
|
||||
relativeHeight: 200,
|
||||
wrapperRef: { current: 'hooks.wrapperRef' },
|
||||
onDocumentLoadSuccess: jest.fn().mockName('hooks.onDocumentLoadSuccess'),
|
||||
onLoadPageSuccess: jest.fn().mockName('hooks.onLoadPageSuccess'),
|
||||
onDocumentLoadError: jest.fn().mockName('hooks.onDocumentLoadError'),
|
||||
onInputPageChange: jest.fn().mockName('hooks.onInputPageChange'),
|
||||
onNextPageButtonClick: jest.fn().mockName('hooks.onNextPageButtonClick'),
|
||||
onPrevPageButtonClick: jest.fn().mockName('hooks.onPrevPageButtonClick'),
|
||||
hasNext: true,
|
||||
hasPref: false,
|
||||
};
|
||||
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<PDFRenderer {...props} />);
|
||||
el.instance().onDocumentLoadSuccess = jest
|
||||
.fn()
|
||||
.mockName('onDocumentLoadSuccess');
|
||||
el.instance().onDocumentLoadError = jest
|
||||
.fn()
|
||||
.mockName('onDocumentLoadError');
|
||||
el.instance().onLoadPageSuccess = jest.fn().mockName('onLoadPageSuccess');
|
||||
});
|
||||
test('snapshot', () => {
|
||||
el.instance().onPrevPageButtonClick = jest
|
||||
.fn()
|
||||
.mockName('onPrevPageButtonClick');
|
||||
el.instance().onNextPageButtonClick = jest
|
||||
.fn()
|
||||
.mockName('onNextPageButtonClick');
|
||||
el.instance().onInputPageChange = jest.fn().mockName('onInputPageChange');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
const numPages = 99;
|
||||
const pageNumber = 234;
|
||||
beforeEach(() => {
|
||||
el = shallow(<PDFRenderer {...props} />);
|
||||
describe('snapshots', () => {
|
||||
test('first page, prev is disabled', () => {
|
||||
hooks.rendererHooks.mockReturnValue(hookProps);
|
||||
expect(shallow(<PDFRenderer {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('Top-level document', () => {
|
||||
let documentEl;
|
||||
beforeEach(() => { documentEl = el.find(Document); });
|
||||
it('displays file from props.url', () => {
|
||||
expect(documentEl.props().file).toEqual(props.url);
|
||||
});
|
||||
it('calls this.onDocumentLoadSuccess onLoadSuccess', () => {
|
||||
expect(documentEl.props().onLoadSuccess).toEqual(el.instance().onDocumentLoadSuccess);
|
||||
});
|
||||
it('calls this.onDocumentLoadError onLoadError', () => {
|
||||
expect(documentEl.props().onLoadError).toEqual(el.instance().onDocumentLoadError);
|
||||
});
|
||||
});
|
||||
describe('Page', () => {
|
||||
let pageProps;
|
||||
beforeEach(() => {
|
||||
el.instance().setState({ pageNumber });
|
||||
pageProps = el.find(Page).props();
|
||||
});
|
||||
it('loads pageNumber from state', () => {
|
||||
expect(pageProps.pageNumber).toEqual(pageNumber);
|
||||
});
|
||||
it('calls onLoadPageSuccess onLoadSuccess', () => {
|
||||
expect(pageProps.onLoadSuccess).toEqual(el.instance().onLoadPageSuccess);
|
||||
});
|
||||
});
|
||||
describe('pagination ActionRow', () => {
|
||||
describe('Previous page button', () => {
|
||||
let hasPrev;
|
||||
beforeEach(() => {
|
||||
hasPrev = jest.spyOn(el.instance(), 'hasPrev', 'get').mockReturnValue(false);
|
||||
});
|
||||
const btn = () => shallow(el.instance().render()).find(IconButton).at(0).props();
|
||||
test('disabled iff not this.hasPrev', () => {
|
||||
expect(btn().disabled).toEqual(true);
|
||||
hasPrev.mockReturnValue(true);
|
||||
expect(btn().disabled).toEqual(false);
|
||||
});
|
||||
it('calls onPrevPageButtonClick onClick', () => {
|
||||
expect(btn().onClick).toEqual(el.instance().onPrevPageButtonClick);
|
||||
});
|
||||
});
|
||||
describe('page indicator', () => {
|
||||
const control = () => el.find(Form.Control).at(0).props();
|
||||
const labels = () => {
|
||||
const flat = el.find({ isInline: true });
|
||||
return [0, 1].map(i => flat.at(i).text());
|
||||
};
|
||||
beforeEach(() => { el.instance().setState({ numPages, pageNumber }); });
|
||||
test('labels: Page <state.pageNumber> of <state.numPages>', () => {
|
||||
expect(`${labels()[0]}${control().value}${labels()[1]}`).toEqual(
|
||||
`Page ${pageNumber} of ${numPages}`,
|
||||
);
|
||||
});
|
||||
it('loads max from state.numPages', () => expect(control().max).toEqual(numPages));
|
||||
it('loads value from state.pageNumber', () => {
|
||||
expect(control().value).toEqual(pageNumber);
|
||||
});
|
||||
it('calls onInputPageChange onChange', () => {
|
||||
expect(control().onChange).toEqual(el.instance().onInputPageChange);
|
||||
});
|
||||
});
|
||||
describe('Next page button', () => {
|
||||
let hasNext;
|
||||
beforeEach(() => {
|
||||
hasNext = jest.spyOn(el.instance(), 'hasNext', 'get').mockReturnValue(false);
|
||||
});
|
||||
const btn = () => shallow(el.instance().render()).find(IconButton).at(1).props();
|
||||
test('disabled iff not this.hasNext', () => {
|
||||
expect(btn().disabled).toEqual(true);
|
||||
hasNext.mockReturnValue(true);
|
||||
expect(btn().disabled).toEqual(false);
|
||||
});
|
||||
it('calls onNextPageButtonClick onClick', () => {
|
||||
expect(btn().onClick).toEqual(el.instance().onNextPageButtonClick);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('initial state', () => {
|
||||
expect(el.instance().state).toEqual(PDFRenderer.INITIAL_STATE);
|
||||
});
|
||||
describe('onDocumentLoadSuccess', () => {
|
||||
test('loads numPages into state', () => {
|
||||
el.instance().onDocumentLoadSuccess({ numPages });
|
||||
expect(el.instance().state.numPages).toEqual(numPages);
|
||||
});
|
||||
});
|
||||
describe('onLoadPageSuccess', () => {
|
||||
const [pageHeight, pageWidth] = [23, 34];
|
||||
const page = { view: [1, 2, pageWidth, pageHeight] };
|
||||
const wrapperWidth = 20;
|
||||
const expected = (wrapperWidth * pageHeight) / pageWidth;
|
||||
beforeEach(() => {
|
||||
el.instance().wrapperRef = {
|
||||
current: {
|
||||
getBoundingClientRect: () => ({ width: wrapperWidth }),
|
||||
},
|
||||
};
|
||||
});
|
||||
it('sets relative height if it has changes', () => {
|
||||
el.instance().onLoadPageSuccess(page);
|
||||
expect(el.instance().state.relativeHeight).toEqual(expected);
|
||||
});
|
||||
it('does not try to set height if has not changes', () => {
|
||||
el.instance().setState({ relativeHeight: expected });
|
||||
el.instance().setState = jest.fn();
|
||||
el.instance().onLoadPageSuccess(page);
|
||||
expect(el.instance().setState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('setPageNumber inheritors', () => {
|
||||
beforeEach(() => {
|
||||
el.instance().setPageNumber = jest.fn();
|
||||
el.instance().setState({ pageNumber });
|
||||
});
|
||||
describe('onInputChange', () => {
|
||||
it('calls setPageNumber with int value of event target value', () => {
|
||||
el.instance().onInputPageChange({ target: { value: '23' } });
|
||||
expect(el.instance().setPageNumber).toHaveBeenCalledWith(23);
|
||||
});
|
||||
});
|
||||
describe('onPrevPageButtonClick', () => {
|
||||
it('calls setPageNumber with state.pageNumber - 1', () => {
|
||||
el.instance().onPrevPageButtonClick();
|
||||
expect(el.instance().setPageNumber).toHaveBeenCalledWith(pageNumber - 1);
|
||||
});
|
||||
});
|
||||
describe('onNextPageButtonClick', () => {
|
||||
it('calls setPageNumber with state.pageNumber + 1', () => {
|
||||
el.instance().onNextPageButtonClick();
|
||||
expect(el.instance().setPageNumber).toHaveBeenCalledWith(pageNumber + 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('setPageNumber', () => {
|
||||
it('calls setState with pageNumber iff valid', () => {
|
||||
el.instance().setState({ numPages });
|
||||
const setState = jest.spyOn(el.instance(), 'setState');
|
||||
el.instance().setPageNumber(0);
|
||||
expect(setState).not.toHaveBeenCalled();
|
||||
el.instance().setPageNumber(numPages + 1);
|
||||
expect(setState).not.toHaveBeenCalled();
|
||||
el.instance().setPageNumber(2);
|
||||
expect(setState).toHaveBeenCalledWith({ pageNumber: 2 });
|
||||
});
|
||||
});
|
||||
describe('hasNext getter', () => {
|
||||
it('returns true iff state.pageNumber < state.numPages', () => {
|
||||
el.instance().setState({ pageNumber: 1, numPages: 1 });
|
||||
expect(el.instance().hasNext).toEqual(false);
|
||||
el.instance().setState({ pageNumber: 1, numPages: 2 });
|
||||
expect(el.instance().hasNext).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('hasPrev getter', () => {
|
||||
it('returns true iff state.pageNumber > 1', () => {
|
||||
el.instance().setState({ pageNumber: 1 });
|
||||
expect(el.instance().hasPrev).toEqual(false);
|
||||
el.instance().setState({ pageNumber: 2 });
|
||||
expect(el.instance().hasPrev).toEqual(true);
|
||||
});
|
||||
test('on last page, next is disabled', () => {
|
||||
hooks.rendererHooks.mockReturnValue({
|
||||
...hookProps,
|
||||
pageNumber: hookProps.numPages,
|
||||
hasNext: false,
|
||||
hasPrev: true,
|
||||
});
|
||||
expect(shallow(<PDFRenderer {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -3,23 +3,21 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import TXTRenderer from './TXTRenderer';
|
||||
|
||||
jest.mock('axios', () => ({
|
||||
get: jest.fn((...args) => Promise.resolve({ data: `Content of ${args}` })),
|
||||
}));
|
||||
jest.mock('./textHooks', () => {
|
||||
const content = 'test-content';
|
||||
return {
|
||||
content,
|
||||
rendererHooks: (args) => ({ content, rendererHooks: args }),
|
||||
};
|
||||
});
|
||||
|
||||
describe('TXT Renderer Component', () => {
|
||||
const props = {
|
||||
url: 'some_url.txt',
|
||||
onError: jest.fn().mockName('this.props.onError'),
|
||||
onSuccess: jest.fn().mockName('this.props.onSuccess'),
|
||||
};
|
||||
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<TXTRenderer {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(shallow(<TXTRenderer {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PDF Renderer Component snapshots snapshot 1`] = `
|
||||
exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = `
|
||||
<div
|
||||
className="pdf-renderer"
|
||||
>
|
||||
<Document
|
||||
file="some_url.pdf"
|
||||
onLoadError={[MockFunction onDocumentLoadError]}
|
||||
onLoadSuccess={[MockFunction onDocumentLoadSuccess]}
|
||||
onLoadError={[MockFunction hooks.onDocumentLoadError]}
|
||||
onLoadSuccess={[MockFunction hooks.onDocumentLoadSuccess]}
|
||||
>
|
||||
<div
|
||||
className="page-wrapper"
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
"height": 200,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Page
|
||||
onLoadSuccess={[MockFunction onLoadPageSuccess]}
|
||||
onLoadSuccess={[MockFunction hooks.onLoadPageSuccess]}
|
||||
pageNumber={1}
|
||||
/>
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@ exports[`PDF Renderer Component snapshots snapshot 1`] = `
|
||||
alt="previous pdf page"
|
||||
disabled={true}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction onPrevPageButtonClick]}
|
||||
onClick={[MockFunction hooks.onPrevPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronLeft]}
|
||||
/>
|
||||
@@ -43,9 +43,9 @@ exports[`PDF Renderer Component snapshots snapshot 1`] = `
|
||||
Page
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
max={1}
|
||||
max={10}
|
||||
min={0}
|
||||
onChange={[MockFunction onInputPageChange]}
|
||||
onChange={[MockFunction hooks.onInputPageChange]}
|
||||
type="number"
|
||||
value={1}
|
||||
/>
|
||||
@@ -53,14 +53,82 @@ exports[`PDF Renderer Component snapshots snapshot 1`] = `
|
||||
isInline={true}
|
||||
>
|
||||
of
|
||||
1
|
||||
10
|
||||
</Form.Label>
|
||||
</Form.Group>
|
||||
<IconButton
|
||||
alt="next pdf page"
|
||||
disabled={true}
|
||||
disabled={false}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction onNextPageButtonClick]}
|
||||
onClick={[MockFunction hooks.onNextPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronRight]}
|
||||
/>
|
||||
</ActionRow>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PDF Renderer Component snapshots on last page, next is disabled 1`] = `
|
||||
<div
|
||||
className="pdf-renderer"
|
||||
>
|
||||
<Document
|
||||
file="some_url.pdf"
|
||||
onLoadError={[MockFunction hooks.onDocumentLoadError]}
|
||||
onLoadSuccess={[MockFunction hooks.onDocumentLoadSuccess]}
|
||||
>
|
||||
<div
|
||||
className="page-wrapper"
|
||||
style={
|
||||
Object {
|
||||
"height": 200,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Page
|
||||
onLoadSuccess={[MockFunction hooks.onLoadPageSuccess]}
|
||||
pageNumber={10}
|
||||
/>
|
||||
</div>
|
||||
</Document>
|
||||
<ActionRow
|
||||
className="d-flex justify-content-center m-0"
|
||||
>
|
||||
<IconButton
|
||||
alt="previous pdf page"
|
||||
disabled={false}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction hooks.onPrevPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronLeft]}
|
||||
/>
|
||||
<Form.Group
|
||||
className="d-flex align-items-center m-0"
|
||||
>
|
||||
<Form.Label
|
||||
isInline={true}
|
||||
>
|
||||
Page
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
max={10}
|
||||
min={0}
|
||||
onChange={[MockFunction hooks.onInputPageChange]}
|
||||
type="number"
|
||||
value={10}
|
||||
/>
|
||||
<Form.Label
|
||||
isInline={true}
|
||||
>
|
||||
of
|
||||
10
|
||||
</Form.Label>
|
||||
</Form.Group>
|
||||
<IconButton
|
||||
alt="next pdf page"
|
||||
disabled={true}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction hooks.onNextPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronRight]}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,6 @@ exports[`TXT Renderer Component snapshot 1`] = `
|
||||
<pre
|
||||
className="txt-renderer"
|
||||
>
|
||||
Content of some_url.txt
|
||||
test-content
|
||||
</pre>
|
||||
`;
|
||||
|
||||
81
src/components/FilePreview/BaseRenderers/pdfHooks.jsx
Normal file
81
src/components/FilePreview/BaseRenderers/pdfHooks.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
import { pdfjs } from 'react-pdf';
|
||||
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
|
||||
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
|
||||
import { ErrorStatuses } from 'data/constants/requests';
|
||||
import { StrictDict } from 'utils';
|
||||
import * as module from './pdfHooks';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
|
||||
|
||||
export const errors = StrictDict({
|
||||
missingPDF: 'MissingPDFException',
|
||||
});
|
||||
|
||||
export const state = StrictDict({
|
||||
pageNumber: (val) => useState(val),
|
||||
numPages: (val) => useState(val),
|
||||
relativeHeight: (val) => useState(val),
|
||||
});
|
||||
|
||||
export const initialState = {
|
||||
pageNumber: 1,
|
||||
numPages: 1,
|
||||
relativeHeight: 1,
|
||||
};
|
||||
|
||||
export const safeSetPageNumber = ({ numPages, rawSetPageNumber }) => (pageNumber) => {
|
||||
if (pageNumber > 0 && pageNumber <= numPages) {
|
||||
rawSetPageNumber(pageNumber);
|
||||
}
|
||||
};
|
||||
|
||||
export const rendererHooks = ({
|
||||
onError,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [pageNumber, rawSetPageNumber] = module.state.pageNumber(initialState.pageNumber);
|
||||
const [numPages, setNumPages] = module.state.numPages(initialState.numPages);
|
||||
const [relativeHeight, setRelativeHeight] = module.state.relativeHeight(
|
||||
initialState.relativeHeight,
|
||||
);
|
||||
|
||||
const setPageNumber = module.safeSetPageNumber({ numPages, rawSetPageNumber });
|
||||
|
||||
const wrapperRef = useRef();
|
||||
|
||||
return {
|
||||
pageNumber,
|
||||
numPages,
|
||||
relativeHeight,
|
||||
wrapperRef,
|
||||
onDocumentLoadSuccess: (args) => {
|
||||
onSuccess();
|
||||
setNumPages(args.numPages);
|
||||
},
|
||||
onLoadPageSuccess: (page) => {
|
||||
const pageWidth = page.view[2];
|
||||
const pageHeight = page.view[3];
|
||||
const wrapperHeight = wrapperRef.current.getBoundingClientRect().width;
|
||||
const newHeight = (wrapperHeight * pageHeight) / pageWidth;
|
||||
setRelativeHeight(newHeight);
|
||||
},
|
||||
onDocumentLoadError: (error) => {
|
||||
let status;
|
||||
if (error.name === errors.missingPDF) {
|
||||
status = ErrorStatuses.notFound;
|
||||
} else {
|
||||
status = ErrorStatuses.serverError;
|
||||
}
|
||||
onError(status);
|
||||
},
|
||||
onInputPageChange: ({ target: { value } }) => setPageNumber(parseInt(value, 10)),
|
||||
onPrevPageButtonClick: () => setPageNumber(pageNumber - 1),
|
||||
onNextPageButtonClick: () => setPageNumber(pageNumber + 1),
|
||||
hasNext: pageNumber < numPages,
|
||||
hasPrev: pageNumber > 1,
|
||||
};
|
||||
};
|
||||
148
src/components/FilePreview/BaseRenderers/pdfHooks.test.js
Normal file
148
src/components/FilePreview/BaseRenderers/pdfHooks.test.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { keyStore } from 'utils';
|
||||
import { ErrorStatuses } from 'data/constants/requests';
|
||||
|
||||
import * as hooks from './pdfHooks';
|
||||
|
||||
jest.mock('react-pdf', () => ({
|
||||
pdfjs: { GlobalWorkerOptions: {} },
|
||||
Document: () => 'Document',
|
||||
Page: () => 'Page',
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
const hookKeys = keyStore(hooks);
|
||||
|
||||
const testValue = 'my-test-value';
|
||||
|
||||
describe('PDF Renderer hooks', () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.pageNumber);
|
||||
state.testGetter(state.keys.numPages);
|
||||
state.testGetter(state.keys.relativeHeight);
|
||||
});
|
||||
describe('non-state hooks', () => {
|
||||
beforeEach(() => state.mock());
|
||||
afterEach(() => state.restore());
|
||||
describe('safeSetPageNumber', () => {
|
||||
it('returns value handler that sets page number if valid', () => {
|
||||
const rawSetPageNumber = jest.fn();
|
||||
const numPages = 10;
|
||||
hooks.safeSetPageNumber({ numPages, rawSetPageNumber })(0);
|
||||
expect(rawSetPageNumber).not.toHaveBeenCalled();
|
||||
hooks.safeSetPageNumber({ numPages, rawSetPageNumber })(numPages + 1);
|
||||
expect(rawSetPageNumber).not.toHaveBeenCalled();
|
||||
hooks.safeSetPageNumber({ numPages, rawSetPageNumber })(numPages - 1);
|
||||
expect(rawSetPageNumber).toHaveBeenCalledWith(numPages - 1);
|
||||
});
|
||||
});
|
||||
describe('rendererHooks', () => {
|
||||
const props = {
|
||||
url: 'some_url.pdf',
|
||||
onError: jest.fn().mockName('this.props.onError'),
|
||||
onSuccess: jest.fn().mockName('this.props.onSuccess'),
|
||||
};
|
||||
let setPageNumber;
|
||||
let hook;
|
||||
let mockSetPageNumber;
|
||||
let mockSafeSetPageNumber;
|
||||
beforeEach(() => {
|
||||
mockSetPageNumber = jest.fn(val => ({ setPageNumber: { val } }));
|
||||
mockSafeSetPageNumber = jest.fn(() => mockSetPageNumber);
|
||||
setPageNumber = jest.spyOn(hooks, hookKeys.safeSetPageNumber)
|
||||
.mockImplementation(mockSafeSetPageNumber);
|
||||
hook = hooks.rendererHooks(props);
|
||||
});
|
||||
afterAll(() => {
|
||||
setPageNumber.mockRestore();
|
||||
});
|
||||
describe('returned object', () => {
|
||||
Object.keys(state.keys).forEach(key => {
|
||||
test(`${key} tied to store and initialized from initialState`, () => {
|
||||
expect(hook[key]).toEqual(hooks.initialState[key]);
|
||||
expect(hook[key]).toEqual(state.stateVals[key]);
|
||||
});
|
||||
});
|
||||
});
|
||||
test('wrapperRef passed as react ref', () => {
|
||||
expect(hook.wrapperRef.useRef).toEqual(true);
|
||||
});
|
||||
describe('onDocumentLoadSuccess', () => {
|
||||
it('calls onSuccess and sets numPages based on args', () => {
|
||||
hook.onDocumentLoadSuccess({ numPages: testValue });
|
||||
expect(props.onSuccess).toHaveBeenCalled();
|
||||
expect(state.setState.numPages).toHaveBeenCalledWith(testValue);
|
||||
});
|
||||
});
|
||||
describe('onLoadPageSuccess', () => {
|
||||
it('sets relative height based on page size', () => {
|
||||
const width = 23;
|
||||
React.useRef.mockReturnValueOnce({
|
||||
current: {
|
||||
getBoundingClientRect: () => ({ width }),
|
||||
},
|
||||
});
|
||||
const [pageWidth, pageHeight] = [20, 30];
|
||||
const page = { view: [0, 0, pageWidth, pageHeight] };
|
||||
hook = hooks.rendererHooks(props);
|
||||
const height = (width * pageHeight) / pageWidth;
|
||||
hook.onLoadPageSuccess(page);
|
||||
expect(state.setState.relativeHeight).toHaveBeenCalledWith(height);
|
||||
});
|
||||
});
|
||||
describe('onDocumentLoadError', () => {
|
||||
it('calls onError with notFound error if error is missingPDF error', () => {
|
||||
hook.onDocumentLoadError({ name: hooks.errors.missingPDF });
|
||||
expect(props.onError).toHaveBeenCalledWith(ErrorStatuses.notFound);
|
||||
});
|
||||
it('calls onError with serverError by default', () => {
|
||||
hook.onDocumentLoadError({ name: testValue });
|
||||
expect(props.onError).toHaveBeenCalledWith(ErrorStatuses.serverError);
|
||||
});
|
||||
});
|
||||
describe('onInputPageChange', () => {
|
||||
it('calls setPageNumber with int event target value', () => {
|
||||
hook.onInputPageChange({ target: { value: '2.3' } });
|
||||
expect(mockSetPageNumber).toHaveBeenCalledWith(2);
|
||||
});
|
||||
});
|
||||
describe('onPrevPageButtonClick', () => {
|
||||
it('calls setPageNumber with current page number - 1', () => {
|
||||
hook.onPrevPageButtonClick();
|
||||
expect(mockSetPageNumber).toHaveBeenCalledWith(hook.pageNumber - 1);
|
||||
});
|
||||
});
|
||||
describe('onNextPageButtonClick', () => {
|
||||
it('calls setPageNumber with current page number + 1', () => {
|
||||
hook.onNextPageButtonClick();
|
||||
expect(mockSetPageNumber).toHaveBeenCalledWith(hook.pageNumber + 1);
|
||||
});
|
||||
});
|
||||
test('hasNext returns true iff pageNumber is less than total number of pages', () => {
|
||||
state.mockVal(state.keys.numPages, 10);
|
||||
state.mockVal(state.keys.pageNumber, 9);
|
||||
hook = hooks.rendererHooks(props);
|
||||
expect(hook.hasNext).toEqual(true);
|
||||
state.mockVal(state.keys.pageNumber, 10);
|
||||
hook = hooks.rendererHooks(props);
|
||||
expect(hook.hasNext).toEqual(false);
|
||||
});
|
||||
test('hasPrev returns true iff pageNumber is greater than 1', () => {
|
||||
state.mockVal(state.keys.pageNumber, 1);
|
||||
hook = hooks.rendererHooks(props);
|
||||
expect(hook.hasPrev).toEqual(false);
|
||||
state.mockVal(state.keys.pageNumber, 0);
|
||||
hook = hooks.rendererHooks(props);
|
||||
expect(hook.hasPrev).toEqual(false);
|
||||
state.mockVal(state.keys.pageNumber, 2);
|
||||
hook = hooks.rendererHooks(props);
|
||||
expect(hook.hasPrev).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
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 };
|
||||
};
|
||||
95
src/components/FilePreview/BaseRenderers/textHooks.test.js
Normal file
95
src/components/FilePreview/BaseRenderers/textHooks.test.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable prefer-promise-reject-errors */
|
||||
import { useEffect } from 'react';
|
||||
import * as axios from 'axios';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
import { MockUseState } from 'testUtils';
|
||||
import * as hooks from './textHooks';
|
||||
|
||||
jest.mock('axios', () => ({
|
||||
get: jest.fn(),
|
||||
}));
|
||||
|
||||
const hookKeys = keyStore(hooks);
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
let hook;
|
||||
|
||||
const testValue = 'test-value';
|
||||
|
||||
const props = {
|
||||
url: 'test-url',
|
||||
onError: jest.fn(),
|
||||
onSuccess: jest.fn(),
|
||||
};
|
||||
describe('Text file preview hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.content);
|
||||
});
|
||||
describe('non-state hooks', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
});
|
||||
afterEach(() => {
|
||||
state.restore();
|
||||
});
|
||||
describe('rendererHooks', () => {
|
||||
it('returns content tied to hook state', () => {
|
||||
hook = hooks.rendererHooks(props);
|
||||
expect(hook.content).toEqual(state.stateVals.content);
|
||||
expect(hook.content).toEqual('');
|
||||
});
|
||||
describe('initialization behavior', () => {
|
||||
let cb;
|
||||
let prereqs;
|
||||
const loadHook = () => {
|
||||
hook = hooks.rendererHooks(props);
|
||||
[[cb, prereqs]] = useEffect.mock.calls;
|
||||
};
|
||||
it('calls fetchFile method, predicated on setContent, url, and callbacks', () => {
|
||||
jest.spyOn(hooks, hookKeys.fetchFile).mockImplementationOnce(() => {});
|
||||
loadHook();
|
||||
expect(useEffect).toHaveBeenCalled();
|
||||
expect(prereqs).toEqual([
|
||||
props.onError,
|
||||
props.onSuccess,
|
||||
state.setState.content,
|
||||
props.url,
|
||||
]);
|
||||
expect(hooks.fetchFile).not.toHaveBeenCalled();
|
||||
cb();
|
||||
expect(hooks.fetchFile).toHaveBeenCalledWith({
|
||||
onError: props.onError,
|
||||
onSuccess: props.onSuccess,
|
||||
setContent: state.setState.content,
|
||||
url: props.url,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('fetchFile', () => {
|
||||
describe('onSuccess', () => {
|
||||
it('calls get', async () => {
|
||||
const testData = 'test-data';
|
||||
axios.get.mockReturnValueOnce(Promise.resolve({ data: testData }));
|
||||
await hooks.fetchFile({ ...props, setContent: state.setState.content });
|
||||
expect(props.onSuccess).toHaveBeenCalled();
|
||||
expect(state.setState[state.keys.content]).toHaveBeenCalledWith(testData);
|
||||
});
|
||||
});
|
||||
describe('onError', () => {
|
||||
it('calls get on the passed url when it changes', async (done) => {
|
||||
axios.get.mockReturnValueOnce(Promise.reject(
|
||||
{ response: { status: testValue } },
|
||||
));
|
||||
await hooks.fetchFile({ ...props, setContent: state.setState.content });
|
||||
expect(props.onError).toHaveBeenCalledWith(testValue);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,11 @@ import './FileCard.scss';
|
||||
*/
|
||||
export const FileCard = ({ file, children }) => (
|
||||
<Card className="file-card" key={file.name}>
|
||||
<Collapsible className="file-collapsible" defaultOpen title={<h3 className="file-card-title">{file.name}</h3>}>
|
||||
<Collapsible
|
||||
className="file-collapsible"
|
||||
defaultOpen
|
||||
title={<h3 className="file-card-title">{file.name}</h3>}
|
||||
>
|
||||
<div className="preview-panel">
|
||||
<FileInfo><FilePopoverContent {...file} /></FileInfo>
|
||||
{children}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import { InfoOutline } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { nullMethod } from 'hooks';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
@@ -19,7 +20,7 @@ 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>
|
||||
)}
|
||||
@@ -36,7 +37,7 @@ export const FileInfo = ({ onClick, children }) => (
|
||||
);
|
||||
|
||||
FileInfo.defaultProps = {
|
||||
onClick: () => {},
|
||||
onClick: nullMethod,
|
||||
};
|
||||
FileInfo.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
|
||||
@@ -1,123 +1,37 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import {
|
||||
PDFRenderer,
|
||||
ImageRenderer,
|
||||
TXTRenderer,
|
||||
} from 'components/FilePreview/BaseRenderers';
|
||||
import FileCard from './FileCard';
|
||||
|
||||
import { ErrorBanner, LoadingBanner } from './Banners';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const RENDERERS = StrictDict({
|
||||
[FileTypes.pdf]: PDFRenderer,
|
||||
[FileTypes.jpg]: ImageRenderer,
|
||||
[FileTypes.jpeg]: ImageRenderer,
|
||||
[FileTypes.bmp]: ImageRenderer,
|
||||
[FileTypes.png]: ImageRenderer,
|
||||
[FileTypes.txt]: TXTRenderer,
|
||||
[FileTypes.gif]: ImageRenderer,
|
||||
[FileTypes.jfif]: ImageRenderer,
|
||||
[FileTypes.pjpeg]: ImageRenderer,
|
||||
[FileTypes.pjp]: ImageRenderer,
|
||||
[FileTypes.svg]: ImageRenderer,
|
||||
});
|
||||
|
||||
export const ERROR_STATUSES = {
|
||||
404: {
|
||||
headingMessage: messages.fileNotFoundError,
|
||||
children: <FormattedMessage {...messages.fileNotFoundError} />,
|
||||
},
|
||||
500: {
|
||||
headingMessage: messages.unknownError,
|
||||
children: <FormattedMessage {...messages.unknownError} />,
|
||||
},
|
||||
};
|
||||
|
||||
export const SUPPORTED_TYPES = Object.keys(RENDERERS);
|
||||
|
||||
export const getFileType = (fileName) => fileName.split('.').pop()?.toLowerCase();
|
||||
export const isSupported = (file) => SUPPORTED_TYPES.includes(getFileType(file.name));
|
||||
import { renderHooks } from './hooks';
|
||||
|
||||
/**
|
||||
* <FileRenderer />
|
||||
*/
|
||||
export class FileRenderer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errorStatus: null,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
this.onError = this.onError.bind(this);
|
||||
this.onSuccess = this.onSuccess.bind(this);
|
||||
this.resetState = this.resetState.bind(this);
|
||||
}
|
||||
|
||||
onError(status) {
|
||||
this.setState({
|
||||
errorStatus: status,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
onSuccess() {
|
||||
this.setState({
|
||||
errorStatus: null,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
get error() {
|
||||
const status = this.state.errorStatus;
|
||||
return {
|
||||
...ERROR_STATUSES[status] || ERROR_STATUSES[500],
|
||||
actions: [
|
||||
{
|
||||
id: 'retry',
|
||||
onClick: this.resetState,
|
||||
message: messages.retryButton,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
resetState = () => {
|
||||
this.setState({
|
||||
errorStatus: null,
|
||||
isLoading: true,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { file } = this.props;
|
||||
const Renderer = RENDERERS[getFileType(file.name)];
|
||||
return (
|
||||
<FileCard key={file.downloadUrl} file={file}>
|
||||
{this.state.isLoading && <LoadingBanner />}
|
||||
{this.state.errorStatus ? (
|
||||
<ErrorBanner {...this.error} />
|
||||
) : (
|
||||
<Renderer
|
||||
fileName={file.name}
|
||||
url={file.downloadUrl}
|
||||
onError={this.onError}
|
||||
onSuccess={this.onSuccess}
|
||||
/>
|
||||
)}
|
||||
</FileCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const FileRenderer = ({
|
||||
file,
|
||||
intl,
|
||||
}) => {
|
||||
const {
|
||||
Renderer,
|
||||
isLoading,
|
||||
errorStatus,
|
||||
error,
|
||||
rendererProps,
|
||||
} = renderHooks({ file, intl });
|
||||
return (
|
||||
<FileCard key={file.downloadUrl} file={file}>
|
||||
{isLoading && <LoadingBanner />}
|
||||
{errorStatus ? (
|
||||
<ErrorBanner {...error} />
|
||||
) : (
|
||||
<Renderer {...rendererProps} />
|
||||
)}
|
||||
</FileCard>
|
||||
);
|
||||
};
|
||||
|
||||
FileRenderer.defaultProps = {};
|
||||
FileRenderer.propTypes = {
|
||||
@@ -125,6 +39,8 @@ FileRenderer.propTypes = {
|
||||
name: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
}).isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default FileRenderer;
|
||||
export default injectIntl(FileRenderer);
|
||||
|
||||
@@ -1,132 +1,52 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
import {
|
||||
ImageRenderer,
|
||||
PDFRenderer,
|
||||
TXTRenderer,
|
||||
} from 'components/FilePreview/BaseRenderers';
|
||||
import {
|
||||
FileRenderer,
|
||||
getFileType,
|
||||
ERROR_STATUSES,
|
||||
RENDERERS,
|
||||
} from './FileRenderer';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { keyStore } from 'utils';
|
||||
import { ErrorStatuses } from 'data/constants/requests';
|
||||
|
||||
import { FileRenderer } from './FileRenderer';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('./FileCard', () => 'FileCard');
|
||||
|
||||
jest.mock('components/FilePreview/BaseRenderers', () => ({
|
||||
PDFRenderer: () => 'PDFRenderer',
|
||||
ImageRenderer: () => 'ImageRenderer',
|
||||
TXTRenderer: () => 'TXTRenderer',
|
||||
}));
|
||||
|
||||
jest.mock('./Banners', () => ({
|
||||
ErrorBanner: () => 'ErrorBanner',
|
||||
LoadingBanner: () => 'LoadingBanner',
|
||||
}));
|
||||
|
||||
const hookKeys = keyStore(hooks);
|
||||
|
||||
const props = {
|
||||
file: {
|
||||
downloadUrl: 'file download url',
|
||||
name: 'filename.txt',
|
||||
},
|
||||
intl: { formatMessage },
|
||||
};
|
||||
describe('FileRenderer', () => {
|
||||
describe('component', () => {
|
||||
const supportedTypes = Object.keys(RENDERERS);
|
||||
const files = [
|
||||
...supportedTypes.map((fileType, index) => ({
|
||||
name: `fake_file_${index}.${fileType}`,
|
||||
description: `file description ${index}`,
|
||||
downloadUrl: `/url-path/fake_file_${index}.${fileType}`,
|
||||
})),
|
||||
];
|
||||
|
||||
const els = files.map((file) => {
|
||||
const el = shallow(<FileRenderer file={file} />);
|
||||
el.instance().onError = jest.fn().mockName('this.props.onError');
|
||||
el.instance().onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
return el;
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
els.forEach((el) => {
|
||||
const file = el.prop('file');
|
||||
const fileType = getFileType(file.name);
|
||||
|
||||
test(`successful rendering ${fileType}`, () => {
|
||||
el.setState({ isLoading: false });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(ERROR_STATUSES).forEach((status) => {
|
||||
test(`has error ${status}`, () => {
|
||||
const el = shallow(<FileRenderer file={files[0]} />);
|
||||
el.instance().setState({
|
||||
errorStatus: status,
|
||||
isLoading: false,
|
||||
});
|
||||
el.instance().resetState = jest.fn().mockName('this.resetState');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('uses the correct renderers', () => {
|
||||
const checkFile = (index, expectedRenderer) => {
|
||||
const file = files[index];
|
||||
const el = shallow(<FileRenderer file={file} />);
|
||||
const renderer = el.find(expectedRenderer);
|
||||
const { url, fileName } = renderer.props();
|
||||
|
||||
expect(renderer).toBeDefined();
|
||||
expect(url).toEqual(file.downloadUrl);
|
||||
expect(fileName).toEqual(file.name);
|
||||
test('isLoading, no Error', () => {
|
||||
const hookProps = {
|
||||
Renderer: () => 'Renderer',
|
||||
isloading: true,
|
||||
errorStatus: null,
|
||||
error: null,
|
||||
rendererProps: { prop: 'hooks.rendererProps' },
|
||||
};
|
||||
/**
|
||||
* The manual process for this is prefer. I want to be more explicit
|
||||
* of which file correspond to which renderer. If I use RENDERERS dicts,
|
||||
* this wouldn't be a test.
|
||||
*/
|
||||
|
||||
test(FileTypes.pdf, () => checkFile(0, PDFRenderer));
|
||||
test(FileTypes.jpg, () => checkFile(1, ImageRenderer));
|
||||
test(FileTypes.jpeg, () => checkFile(2, ImageRenderer));
|
||||
test(FileTypes.bmp, () => checkFile(3, ImageRenderer));
|
||||
test(FileTypes.png, () => checkFile(4, ImageRenderer));
|
||||
test(FileTypes.txt, () => checkFile(5, TXTRenderer));
|
||||
test(FileTypes.gif, () => checkFile(6, ImageRenderer));
|
||||
test(FileTypes.jfif, () => checkFile(7, ImageRenderer));
|
||||
test(FileTypes.pjpeg, () => checkFile(8, ImageRenderer));
|
||||
test(FileTypes.pjp, () => checkFile(9, ImageRenderer));
|
||||
test(FileTypes.svg, () => checkFile(10, ImageRenderer));
|
||||
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
|
||||
expect(shallow(<FileRenderer {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('getter for error', () => {
|
||||
const el = els[0];
|
||||
Object.keys(ERROR_STATUSES).forEach((status) => {
|
||||
el.setState({
|
||||
isLoading: false,
|
||||
errorStatus: status,
|
||||
});
|
||||
const { actions, ...expectedError } = el.instance().error;
|
||||
expect(ERROR_STATUSES[status]).toEqual(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderer constraints', () => {
|
||||
els.forEach((el) => {
|
||||
const file = el.prop('file');
|
||||
const fileType = getFileType(file.name);
|
||||
const RendererComponent = RENDERERS[fileType];
|
||||
const ActualRendererComponent = jest.requireActual(
|
||||
'components/FilePreview/BaseRenderers',
|
||||
)[RendererComponent.name];
|
||||
|
||||
test(`${fileType} renderer must have onError and onSuccess props`, () => {
|
||||
/* eslint-disable react/forbid-foreign-prop-types */
|
||||
expect(ActualRendererComponent.propTypes.onError).toBeDefined();
|
||||
expect(ActualRendererComponent.propTypes.onSuccess).toBeDefined();
|
||||
});
|
||||
test('is not loading, with error', () => {
|
||||
const hookProps = {
|
||||
Renderer: () => 'Renderer',
|
||||
isloading: false,
|
||||
errorStatus: ErrorStatuses.serverError,
|
||||
error: { prop: 'hooks.errorProps' },
|
||||
rendererProps: { prop: 'hooks.rendererProps' },
|
||||
};
|
||||
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
|
||||
expect(shallow(<FileRenderer {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ exports[`File Preview Card component snapshot 1`] = `
|
||||
overlay={
|
||||
<Popover
|
||||
className="overlay-help-popover"
|
||||
id="file-popover"
|
||||
>
|
||||
<Popover.Content>
|
||||
<h1>
|
||||
|
||||
@@ -1,292 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileRenderer component snapshot has error 404 1`] = `
|
||||
exports[`FileRenderer component snapshot is not loading, with error 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
"downloadUrl": "/url-path/fake_file_0.pdf",
|
||||
"name": "fake_file_0.pdf",
|
||||
"downloadUrl": "file download url",
|
||||
"name": "filename.txt",
|
||||
}
|
||||
}
|
||||
key="file download url"
|
||||
>
|
||||
<ErrorBanner
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"id": "retry",
|
||||
"message": Object {
|
||||
"defaultMessage": "Retry",
|
||||
"description": "Retry button for error in file renderer",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.retryButton",
|
||||
},
|
||||
"onClick": [MockFunction this.resetState],
|
||||
},
|
||||
]
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "File not found",
|
||||
"description": "File not found error message",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.fileNotFound",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="File not found"
|
||||
description="File not found error message"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.fileNotFound"
|
||||
/>
|
||||
</ErrorBanner>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot has error 500 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
"downloadUrl": "/url-path/fake_file_0.pdf",
|
||||
"name": "fake_file_0.pdf",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ErrorBanner
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"id": "retry",
|
||||
"message": Object {
|
||||
"defaultMessage": "Retry",
|
||||
"description": "Retry button for error in file renderer",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.retryButton",
|
||||
},
|
||||
"onClick": [MockFunction this.resetState],
|
||||
},
|
||||
]
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "Unknown errors",
|
||||
"description": "Unknown errors message",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.unknownError",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Unknown errors"
|
||||
description="Unknown errors message"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
|
||||
/>
|
||||
</ErrorBanner>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering bmp 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 3",
|
||||
"downloadUrl": "/url-path/fake_file_3.bmp",
|
||||
"name": "fake_file_3.bmp",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_3.bmp"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_3.bmp"
|
||||
prop="hooks.errorProps"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering gif 1`] = `
|
||||
exports[`FileRenderer component snapshot isLoading, no Error 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 6",
|
||||
"downloadUrl": "/url-path/fake_file_6.gif",
|
||||
"name": "fake_file_6.gif",
|
||||
"downloadUrl": "file download url",
|
||||
"name": "filename.txt",
|
||||
}
|
||||
}
|
||||
key="file download url"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_6.gif"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_6.gif"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering jfif 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 7",
|
||||
"downloadUrl": "/url-path/fake_file_7.jfif",
|
||||
"name": "fake_file_7.jfif",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_7.jfif"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_7.jfif"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering jpeg 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 2",
|
||||
"downloadUrl": "/url-path/fake_file_2.jpeg",
|
||||
"name": "fake_file_2.jpeg",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_2.jpeg"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_2.jpeg"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering jpg 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 1",
|
||||
"downloadUrl": "/url-path/fake_file_1.jpg",
|
||||
"name": "fake_file_1.jpg",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_1.jpg"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_1.jpg"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering pdf 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
"downloadUrl": "/url-path/fake_file_0.pdf",
|
||||
"name": "fake_file_0.pdf",
|
||||
}
|
||||
}
|
||||
>
|
||||
<PDFRenderer
|
||||
fileName="fake_file_0.pdf"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_0.pdf"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering pjp 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 9",
|
||||
"downloadUrl": "/url-path/fake_file_9.pjp",
|
||||
"name": "fake_file_9.pjp",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_9.pjp"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_9.pjp"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering pjpeg 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 8",
|
||||
"downloadUrl": "/url-path/fake_file_8.pjpeg",
|
||||
"name": "fake_file_8.pjpeg",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_8.pjpeg"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_8.pjpeg"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering png 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 4",
|
||||
"downloadUrl": "/url-path/fake_file_4.png",
|
||||
"name": "fake_file_4.png",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_4.png"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_4.png"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering svg 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 10",
|
||||
"downloadUrl": "/url-path/fake_file_10.svg",
|
||||
"name": "fake_file_10.svg",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_10.svg"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_10.svg"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering txt 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 5",
|
||||
"downloadUrl": "/url-path/fake_file_5.txt",
|
||||
"name": "fake_file_5.txt",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TXTRenderer
|
||||
fileName="fake_file_5.txt"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_5.txt"
|
||||
<Renderer
|
||||
prop="hooks.rendererProps"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
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 = {
|
||||
headerMessage: errorMessage,
|
||||
children: intl.formatMessage(errorMessage),
|
||||
actions: [errorAction],
|
||||
};
|
||||
|
||||
const Renderer = module.RENDERERS[module.getFileType(file.name)];
|
||||
const rendererProps = {
|
||||
fileName: file.name,
|
||||
url: file.downloadUrl,
|
||||
onError: stopLoading,
|
||||
onSuccess: () => stopLoading(),
|
||||
};
|
||||
|
||||
return {
|
||||
errorStatus,
|
||||
isLoading,
|
||||
error,
|
||||
Renderer,
|
||||
rendererProps,
|
||||
};
|
||||
};
|
||||
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.headerMessage).toEqual(
|
||||
hooks.ERROR_STATUSES[ErrorStatuses.serverError],
|
||||
);
|
||||
expect(hook.error.children).toEqual(
|
||||
formatMessage(hooks.ERROR_STATUSES[ErrorStatuses.serverError]),
|
||||
);
|
||||
state.mockVal(state.keys.errorStatus, ErrorStatuses.notFound);
|
||||
hook = hooks.renderHooks({ intl: { formatMessage }, file });
|
||||
expect(hook.error.headerMessage).toEqual(
|
||||
hooks.ERROR_STATUSES[ErrorStatuses.notFound],
|
||||
);
|
||||
expect(hook.error.children).toEqual(
|
||||
formatMessage(hooks.ERROR_STATUSES[ErrorStatuses.notFound]),
|
||||
);
|
||||
});
|
||||
it('provides a single action', () => {
|
||||
expect(hook.error.actions.length).toEqual(1);
|
||||
});
|
||||
describe('action', () => {
|
||||
it('sets errorState to null and isLoading to true on click', () => {
|
||||
hook.error.actions[0].onClick();
|
||||
expect(state.setState.isLoading).toHaveBeenCalledWith(true);
|
||||
expect(state.setState.errorStatus).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Renderer', () => {
|
||||
it('returns configured renderer based on filetype', () => {
|
||||
hooks.SUPPORTED_TYPES.forEach(type => {
|
||||
jest.spyOn(hooks, hookKeys.getFileType).mockReturnValueOnce(type);
|
||||
hook = hooks.renderHooks({ intl: { formatMessage }, file });
|
||||
expect(hook.Renderer).toEqual(hooks.RENDERERS[type]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('rendererProps', () => {
|
||||
it('forwards url and fileName from file', () => {
|
||||
expect(hook.rendererProps.fileName).toEqual(file.name);
|
||||
expect(hook.rendererProps.url).toEqual(file.downloadUrl);
|
||||
});
|
||||
describe('onError', () => {
|
||||
it('it sets isLoading to false and loads errorStatus', () => {
|
||||
hook.rendererProps.onError(testValue);
|
||||
expect(state.setState.isLoading).toHaveBeenCalledWith(false);
|
||||
expect(state.setState.errorStatus).toHaveBeenCalledWith(testValue);
|
||||
});
|
||||
});
|
||||
describe('onSuccess', () => {
|
||||
it('it sets isLoading to false and errorStatus to null', () => {
|
||||
hook.rendererProps.onSuccess(testValue);
|
||||
expect(state.setState.isLoading).toHaveBeenCalledWith(false);
|
||||
expect(state.setState.errorStatus).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
export { default as FileRenderer, isSupported } from './FileRenderer';
|
||||
export { default as FileRenderer } from './FileRenderer';
|
||||
export { isSupported } from './hooks';
|
||||
|
||||
14
src/components/Head/__snapshots__/index.test.jsx.snap
Normal file
14
src/components/Head/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Head snapshot 1`] = `
|
||||
<Helmet>
|
||||
<title>
|
||||
ORA staff grading | site-name
|
||||
</title>
|
||||
<link
|
||||
href="favicon-url"
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</Helmet>
|
||||
`;
|
||||
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;
|
||||
25
src/components/Head/index.test.jsx
Normal file
25
src/components/Head/index.test.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { shallow } from 'enzyme';
|
||||
import Head from '.';
|
||||
|
||||
jest.mock('react-helmet', () => ({
|
||||
Helmet: 'Helmet',
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: () => ({
|
||||
SITE_NAME: 'site-name',
|
||||
FAVICON_URL: 'favicon-url',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Head', () => {
|
||||
it('snapshot', () => {
|
||||
const el = shallow(<Head />);
|
||||
expect(el).toMatchSnapshot();
|
||||
|
||||
expect(el.find('title').text()).toContain(getConfig().SITE_NAME);
|
||||
expect(el.find('link').prop('href')).toEqual(getConfig().FAVICON_URL);
|
||||
});
|
||||
});
|
||||
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;
|
||||
@@ -6,6 +6,7 @@ exports[`Info Popover Component snapshot 1`] = `
|
||||
overlay={
|
||||
<Popover
|
||||
className="overlay-help-popover"
|
||||
id="info-popover"
|
||||
>
|
||||
<Popover.Content>
|
||||
<div>
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
import { InfoOutline } from '@edx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { nullMethod } from 'hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
@@ -21,7 +23,7 @@ export const InfoPopover = ({ onClick, children, intl }) => (
|
||||
placement="right-end"
|
||||
flip
|
||||
overlay={(
|
||||
<Popover className="overlay-help-popover">
|
||||
<Popover id="info-popover" className="overlay-help-popover">
|
||||
<Popover.Content>{children}</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
@@ -37,7 +39,7 @@ export const InfoPopover = ({ onClick, children, intl }) => (
|
||||
);
|
||||
|
||||
InfoPopover.defaultProps = {
|
||||
onClick: () => {},
|
||||
onClick: nullMethod,
|
||||
};
|
||||
InfoPopover.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
|
||||
@@ -4,21 +4,29 @@ import PropTypes from 'prop-types';
|
||||
import { Badge } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { gradingStatuses as statuses } from 'data/services/lms/constants';
|
||||
import messages from 'data/services/lms/messages';
|
||||
|
||||
export const statusVariants = {
|
||||
[statuses.ungraded]: 'primary',
|
||||
[statuses.locked]: 'light',
|
||||
[statuses.graded]: 'success',
|
||||
[statuses.inProgress]: 'warning',
|
||||
};
|
||||
export const buttonVariants = StrictDict({
|
||||
primary: 'primary',
|
||||
light: 'light',
|
||||
success: 'success',
|
||||
warning: 'warning',
|
||||
});
|
||||
|
||||
export const statusVariants = StrictDict({
|
||||
[statuses.ungraded]: buttonVariants.primary,
|
||||
[statuses.locked]: buttonVariants.light,
|
||||
[statuses.graded]: buttonVariants.success,
|
||||
[statuses.inProgress]: buttonVariants.warning,
|
||||
});
|
||||
|
||||
/**
|
||||
* <StatusBadge />
|
||||
*/
|
||||
export const StatusBadge = ({ className, status }) => {
|
||||
if (statusVariants[status] === undefined) {
|
||||
if (!Object.keys(statusVariants).includes(status)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
||||
35
src/components/StatusBadge.test.jsx
Normal file
35
src/components/StatusBadge.test.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { gradingStatuses } from 'data/services/lms/constants';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
|
||||
const className = 'test-className';
|
||||
describe('StatusBadge component', () => {
|
||||
const render = (status) => shallow(<StatusBadge className={className} status={status} />);
|
||||
describe('behavior', () => {
|
||||
it('does not render if status does not have configured variant', () => {
|
||||
const el = render('arbitrary');
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
describe('status snapshots: loads badge with configured variant and message.', () => {
|
||||
test('`ungraded` shows primary button variant and message', () => {
|
||||
const el = render(gradingStatuses.ungraded);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('`locked` shows light button variant and message', () => {
|
||||
const el = render(gradingStatuses.locked);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('`graded` shows success button variant and message', () => {
|
||||
const el = render(gradingStatuses.graded);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('`inProgress` shows warning button variant and message', () => {
|
||||
const el = render(gradingStatuses.inProgress);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ exports[`ConfirmModal snapshot: closed 1`] = `
|
||||
</ActionRow>
|
||||
}
|
||||
isOpen={false}
|
||||
onClose={[Function]}
|
||||
onClose={[MockFunction hooks.nullMethod]}
|
||||
title="test-title"
|
||||
>
|
||||
<p>
|
||||
@@ -49,7 +49,7 @@ exports[`ConfirmModal snapshot: open 1`] = `
|
||||
</ActionRow>
|
||||
}
|
||||
isOpen={true}
|
||||
onClose={[Function]}
|
||||
onClose={[MockFunction hooks.nullMethod]}
|
||||
title="test-title"
|
||||
>
|
||||
<p>
|
||||
|
||||
55
src/components/__snapshots__/StatusBadge.test.jsx.snap
Normal file
55
src/components/__snapshots__/StatusBadge.test.jsx.snap
Normal file
@@ -0,0 +1,55 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusBadge component behavior does not render if status does not have configured variant 1`] = `""`;
|
||||
|
||||
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`graded\` shows success button variant and message 1`] = `
|
||||
<Badge
|
||||
className="test-className"
|
||||
variant="success"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Grading Completed"
|
||||
description="Grading status label for graded submission"
|
||||
id="ora-grading.lms-api.gradingStatusDisplay.graded"
|
||||
/>
|
||||
</Badge>
|
||||
`;
|
||||
|
||||
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`inProgress\` shows warning button variant and message 1`] = `
|
||||
<Badge
|
||||
className="test-className"
|
||||
variant="warning"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are currently grading this response"
|
||||
description="Grading status label for in-progress submission"
|
||||
id="ora-grading.lms-api.gradingStatusDisplay.inProgress"
|
||||
/>
|
||||
</Badge>
|
||||
`;
|
||||
|
||||
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`locked\` shows light button variant and message 1`] = `
|
||||
<Badge
|
||||
className="test-className"
|
||||
variant="light"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Currently being graded by someone else"
|
||||
description="Grading status label for locked submission"
|
||||
id="ora-grading.lms-api.gradingStatusDisplay.locked"
|
||||
/>
|
||||
</Badge>
|
||||
`;
|
||||
|
||||
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`ungraded\` shows primary button variant and message 1`] = `
|
||||
<Badge
|
||||
className="test-className"
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Ungraded"
|
||||
description="Grading status label for ungraded submission"
|
||||
id="ora-grading.lms-api.gradingStatusDisplay.ungraded"
|
||||
/>
|
||||
</Badge>
|
||||
`;
|
||||
@@ -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 };
|
||||
11
src/containers/CTA/CTA.test.jsx
Normal file
11
src/containers/CTA/CTA.test.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { CTA } from '.';
|
||||
|
||||
describe('CTA component', () => {
|
||||
test('snapshots', () => {
|
||||
const el = shallow(<CTA hide />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
31
src/containers/CTA/__snapshots__/CTA.test.jsx.snap
Normal file
31
src/containers/CTA/__snapshots__/CTA.test.jsx.snap
Normal file
@@ -0,0 +1,31 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CTA component snapshots 1`] = `
|
||||
<PageBanner>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Thanks for using the new ORA staff grading experience. "
|
||||
description="Thank user for using ora and ask for feed back"
|
||||
id="ora-grading.CTA.feedbackMessage"
|
||||
/>
|
||||
<Hyperlink
|
||||
destination="https://docs.google.com/forms/d/1Hu1rgJcCHl5_EtDb5Up3hiZ40sSUtkZQfRHJ3fWOvfQ/edit"
|
||||
isInline={true}
|
||||
showLaunchIcon={false}
|
||||
target="_blank"
|
||||
variant="muted"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Provide some feedback"
|
||||
description="placeholder for the feedback anchor link"
|
||||
id="ora-grading.CTA.linkMessage"
|
||||
/>
|
||||
</Hyperlink>
|
||||
<FormattedMessage
|
||||
defaultMessage=" and let us know what you think!"
|
||||
description="inform user to provide feedback"
|
||||
id="ora-grading.CTA.letUsKnowMessage"
|
||||
/>
|
||||
</span>
|
||||
</PageBanner>
|
||||
`;
|
||||
29
src/containers/CTA/index.jsx
Normal file
29
src/containers/CTA/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { PageBanner, Hyperlink } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <CTA />
|
||||
*/
|
||||
export const CTA = () => (
|
||||
<PageBanner>
|
||||
<span>
|
||||
<FormattedMessage {...messages.ctaFeedbackMessage} />
|
||||
<Hyperlink
|
||||
isInline
|
||||
variant="muted"
|
||||
destination="https://docs.google.com/forms/d/1Hu1rgJcCHl5_EtDb5Up3hiZ40sSUtkZQfRHJ3fWOvfQ/edit"
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<FormattedMessage {...messages.ctaLinkMessage} />
|
||||
</Hyperlink>
|
||||
<FormattedMessage {...messages.ctaLetUsKnowMessage} />
|
||||
</span>
|
||||
</PageBanner>
|
||||
);
|
||||
|
||||
export default CTA;
|
||||
23
src/containers/CTA/messages.js
Normal file
23
src/containers/CTA/messages.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable quotes */
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
const messages = defineMessages({
|
||||
ctaFeedbackMessage: {
|
||||
id: 'ora-grading.CTA.feedbackMessage',
|
||||
defaultMessage: 'Thanks for using the new ORA staff grading experience. ',
|
||||
description: 'Thank user for using ora and ask for feed back',
|
||||
},
|
||||
ctaLinkMessage: {
|
||||
id: 'ora-grading.CTA.linkMessage',
|
||||
defaultMessage: 'Provide some feedback',
|
||||
description: 'placeholder for the feedback anchor link',
|
||||
},
|
||||
ctaLetUsKnowMessage: {
|
||||
id: 'ora-grading.CTA.letUsKnowMessage',
|
||||
defaultMessage: ' and let us know what you think!',
|
||||
description: 'inform user to provide feedback',
|
||||
},
|
||||
});
|
||||
|
||||
export default StrictDict(messages);
|
||||
@@ -33,27 +33,25 @@ export class RadioCriterion extends React.Component {
|
||||
isInvalid,
|
||||
} = this.props;
|
||||
return (
|
||||
<>
|
||||
<Form.RadioSet name={config.name} value={data}>
|
||||
{config.options.map((option) => (
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
key={option.name}
|
||||
value={option.name}
|
||||
description={intl.formatMessage(messages.optionPoints, { points: option.points })}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
>
|
||||
{option.label}
|
||||
</Form.Radio>
|
||||
))}
|
||||
{isInvalid && (
|
||||
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
|
||||
{intl.formatMessage(messages.rubricSelectedError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.RadioSet>
|
||||
</>
|
||||
<Form.RadioSet name={config.name} value={data}>
|
||||
{config.options.map((option) => (
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
key={option.name}
|
||||
value={option.name}
|
||||
description={intl.formatMessage(messages.optionPoints, { points: option.points })}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
>
|
||||
{option.label}
|
||||
</Form.Radio>
|
||||
))}
|
||||
{isInvalid && (
|
||||
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
|
||||
{intl.formatMessage(messages.rubricSelectedError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.RadioSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,91 +1,85 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Radio Criterion Container snapshot is grading 1`] = `
|
||||
<React.Fragment>
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="1 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="1 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
</React.Fragment>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
`;
|
||||
|
||||
exports[`Radio Criterion Container snapshot is not grading 1`] = `
|
||||
<React.Fragment>
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="1 points"
|
||||
disabled={true}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="1 points"
|
||||
disabled={true}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={true}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
</React.Fragment>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={true}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
`;
|
||||
|
||||
exports[`Radio Criterion Container snapshot radio contain invalid response 1`] = `
|
||||
<React.Fragment>
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="1 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="1 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
<Form.Control.Feedback
|
||||
className="feedback-error-msg"
|
||||
type="invalid"
|
||||
>
|
||||
Rubric selection is required
|
||||
</Form.Control.Feedback>
|
||||
</Form.RadioSet>
|
||||
</React.Fragment>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
<Form.Control.Feedback
|
||||
className="feedback-error-msg"
|
||||
type="invalid"
|
||||
>
|
||||
Rubric selection is required
|
||||
</Form.Control.Feedback>
|
||||
</Form.RadioSet>
|
||||
`;
|
||||
|
||||
@@ -9,7 +9,7 @@ export const filterHooks = () => {
|
||||
if (!setAllFilters || !state.filters) {
|
||||
return {};
|
||||
}
|
||||
const clearFilters = React.useCallback(() => setAllFilters([]), []);
|
||||
const clearFilters = React.useCallback(() => setAllFilters([]), [setAllFilters]);
|
||||
const headerMap = headers.reduce(
|
||||
(obj, cur) => ({ ...obj, [cur.id]: cur.Header }),
|
||||
{},
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('FilterStatusComponent hooks', () => {
|
||||
it('uses React.useCallback to clear filters, only once', () => {
|
||||
mockTableContext(context);
|
||||
const { cb, prereqs } = module.filterHooks().clearFilters.useCallback;
|
||||
expect(prereqs).toEqual([]);
|
||||
expect(prereqs).toEqual([context.setAllFilters]);
|
||||
expect(context.setAllFilters).not.toHaveBeenCalled();
|
||||
cb();
|
||||
expect(context.setAllFilters).toHaveBeenCalledWith([]);
|
||||
|
||||
@@ -19,3 +19,10 @@ span.pgn__icon.breadcrumb-arrow {
|
||||
}
|
||||
}
|
||||
|
||||
.submissions-table {
|
||||
.pgn__data-table-filters-breakout-filter {
|
||||
.pgn__form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ListViewBreadcrumb = ({ courseId, oraName }) => (
|
||||
</Hyperlink>
|
||||
<p className="py-4">
|
||||
<span className="h3">{oraName}</span>
|
||||
<Hyperlink className="align-middle" destination={urls.ora(courseId, locationId)}>
|
||||
<Hyperlink className="align-middle" destination={urls.ora(courseId, locationId())}>
|
||||
<Icon src={Launch} className="d-inline-block" />
|
||||
</Hyperlink>
|
||||
</p>
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('ListViewBreadcrumb component', () => {
|
||||
test('ora destination', () => {
|
||||
expect(
|
||||
el.find(Hyperlink).at(1).props().destination,
|
||||
).toEqual(urls.ora(props.courseId, constants.locationId));
|
||||
).toEqual(urls.ora(props.courseId, constants.locationId()));
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
|
||||
27
src/containers/ListView/SelectedBulkAction.jsx
Normal file
27
src/containers/ListView/SelectedBulkAction.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const SelectedBulkAction = ({ selectedFlatRows, handleClick }) => (
|
||||
<Button
|
||||
onClick={handleClick(selectedFlatRows)}
|
||||
variant="primary"
|
||||
className="view-selected-responses-btn"
|
||||
>
|
||||
<FormattedMessage {...messages.viewSelectedResponses} values={{ value: selectedFlatRows.length }} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
SelectedBulkAction.defaultProps = {
|
||||
selectedFlatRows: [],
|
||||
};
|
||||
SelectedBulkAction.propTypes = {
|
||||
selectedFlatRows: PropTypes.arrayOf(PropTypes.object),
|
||||
|
||||
handleClick: PropTypes.func.isRequired,
|
||||
};
|
||||
export default SelectedBulkAction;
|
||||
20
src/containers/ListView/SelectedBulkAction.test.jsx
Normal file
20
src/containers/ListView/SelectedBulkAction.test.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { SelectedBulkAction } from './SelectedBulkAction';
|
||||
|
||||
describe('SelectedBulkAction component', () => {
|
||||
const props = {
|
||||
selectedFlatRows: [{ id: 1 }, { id: 2 }],
|
||||
handleClick: jest.fn(),
|
||||
};
|
||||
test('snapshots', () => {
|
||||
const el = shallow(<SelectedBulkAction {...props} handleClick={() => jest.fn()} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handleClick', () => {
|
||||
shallow(<SelectedBulkAction {...props} />);
|
||||
expect(props.handleClick).toHaveBeenCalledWith(props.selectedFlatRows);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
DataTable,
|
||||
@@ -16,6 +17,8 @@ import { selectors, thunkActions } from 'data/redux';
|
||||
|
||||
import StatusBadge from 'components/StatusBadge';
|
||||
import FilterStatusComponent from './FilterStatusComponent';
|
||||
import TableAction from './TableAction';
|
||||
import SelectedBulkAction from './SelectedBulkAction';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -23,12 +26,6 @@ import messages from './messages';
|
||||
* <SubmissionsTable />
|
||||
*/
|
||||
export class SubmissionsTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleViewAllResponsesClick = this.handleViewAllResponsesClick.bind(this);
|
||||
this.selectedBulkAction = this.selectedBulkAction.bind(this);
|
||||
}
|
||||
|
||||
get gradeStatusOptions() {
|
||||
return Object.keys(gradingStatuses).map(statusKey => ({
|
||||
name: this.translate(lmsMessages[gradingStatuses[statusKey]]),
|
||||
@@ -53,9 +50,9 @@ export class SubmissionsTable extends React.Component {
|
||||
}
|
||||
|
||||
formatDate = ({ value }) => {
|
||||
const date = new Date(value);
|
||||
const date = new Date(moment(value));
|
||||
return date.toLocaleString();
|
||||
}
|
||||
};
|
||||
|
||||
formatGrade = ({ value: score }) => (
|
||||
score === null ? '-' : `${score.pointsEarned}/${score.pointsPossible}`
|
||||
@@ -65,82 +62,66 @@ export class SubmissionsTable extends React.Component {
|
||||
|
||||
translate = (...args) => this.props.intl.formatMessage(...args);
|
||||
|
||||
handleViewAllResponsesClick(data) {
|
||||
handleViewAllResponsesClick = (data) => () => {
|
||||
const getsubmissionUUID = (row) => row.original.submissionUUID;
|
||||
const rows = data.selectedRows.length ? data.selectedRows : data.tableInstance.rows;
|
||||
this.props.loadSelectionForReview(rows.map(getsubmissionUUID));
|
||||
}
|
||||
|
||||
selectedBulkAction(selectedFlatRows) {
|
||||
return {
|
||||
buttonText: this.translate(
|
||||
messages.viewSelectedResponses,
|
||||
{ value: selectedFlatRows.length },
|
||||
),
|
||||
className: 'view-selected-responses-btn',
|
||||
handleClick: this.handleViewAllResponsesClick,
|
||||
variant: 'primary',
|
||||
};
|
||||
}
|
||||
this.props.loadSelectionForReview(data.map(getsubmissionUUID));
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.listData.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<DataTable
|
||||
isFilterable
|
||||
FilterStatusComponent={FilterStatusComponent}
|
||||
numBreakoutFilters={2}
|
||||
defaultColumnValues={{ Filter: TextFilter }}
|
||||
isSelectable
|
||||
isSortable
|
||||
isPaginated
|
||||
itemCount={this.props.listData.length}
|
||||
initialState={{ pageSize: 10, pageIndex: 0 }}
|
||||
data={this.props.listData}
|
||||
tableActions={[
|
||||
{
|
||||
buttonText: this.translate(messages.viewAllResponses),
|
||||
handleClick: this.handleViewAllResponsesClick,
|
||||
className: 'view-all-responses-btn',
|
||||
variant: 'primary',
|
||||
},
|
||||
]}
|
||||
bulkActions={[
|
||||
this.selectedBulkAction,
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
Header: this.userLabel,
|
||||
accessor: this.userAccessor,
|
||||
},
|
||||
{
|
||||
Header: this.dateSubmittedLabel,
|
||||
accessor: submissionFields.dateSubmitted,
|
||||
Cell: this.formatDate,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: this.translate(messages.grade),
|
||||
accessor: submissionFields.score,
|
||||
Cell: this.formatGrade,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: this.translate(messages.gradingStatus),
|
||||
accessor: submissionFields.gradingStatus,
|
||||
Cell: this.formatStatus,
|
||||
Filter: MultiSelectDropdownFilter,
|
||||
filter: 'includesValue',
|
||||
filterChoices: this.gradeStatusOptions,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
<div className="submissions-table">
|
||||
<DataTable
|
||||
isFilterable
|
||||
FilterStatusComponent={FilterStatusComponent}
|
||||
numBreakoutFilters={2}
|
||||
defaultColumnValues={{ Filter: TextFilter }}
|
||||
isSelectable
|
||||
isSortable
|
||||
isPaginated
|
||||
itemCount={this.props.listData.length}
|
||||
initialState={{ pageSize: 10, pageIndex: 0 }}
|
||||
data={this.props.listData}
|
||||
tableActions={[
|
||||
<TableAction handleClick={this.handleViewAllResponsesClick} />,
|
||||
]}
|
||||
bulkActions={[
|
||||
<SelectedBulkAction handleClick={this.handleViewAllResponsesClick} />,
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
Header: this.userLabel,
|
||||
accessor: this.userAccessor,
|
||||
},
|
||||
{
|
||||
Header: this.dateSubmittedLabel,
|
||||
accessor: submissionFields.dateSubmitted,
|
||||
Cell: this.formatDate,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: this.translate(messages.grade),
|
||||
accessor: submissionFields.score,
|
||||
Cell: this.formatGrade,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: this.translate(messages.gradingStatus),
|
||||
accessor: submissionFields.gradingStatus,
|
||||
Cell: this.formatStatus,
|
||||
Filter: MultiSelectDropdownFilter,
|
||||
filter: 'includesValue',
|
||||
filterChoices: this.gradeStatusOptions,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
} from './SubmissionsTable';
|
||||
|
||||
jest.mock('./FilterStatusComponent', () => jest.fn().mockName('FilterStatusComponent'));
|
||||
jest.mock('./TableAction', () => jest.fn().mockName('TableAction'));
|
||||
jest.mock('./SelectedBulkAction', () => jest.fn().mockName('SelectedBulkAction'));
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
@@ -43,9 +45,9 @@ let el;
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
const dates = [
|
||||
new Date(16131215154955).toLocaleTimeString(),
|
||||
new Date(16131225154955).toLocaleTimeString(),
|
||||
new Date(16131215250955).toLocaleTimeString(),
|
||||
'2021-12-08 09:06:15.319213+00:00',
|
||||
'2021-12-10 18:06:15.319213+00:00',
|
||||
'2021-12-11 07:06:15.319213+00:00',
|
||||
];
|
||||
|
||||
const individualData = [
|
||||
@@ -128,7 +130,6 @@ describe('SubmissionsTable component', () => {
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
mockMethod('handleViewAllResponsesClick');
|
||||
mockMethod('selectedBulkAction');
|
||||
mockMethod('formatDate');
|
||||
mockMethod('formatGrade');
|
||||
mockMethod('formatStatus');
|
||||
@@ -165,9 +166,6 @@ describe('SubmissionsTable component', () => {
|
||||
['itemCount', 3],
|
||||
['initialState', { pageSize: 10, pageIndex: 0 }],
|
||||
])('%s = %p', (key, value) => expect(tableProps[key]).toEqual(value));
|
||||
test('bulkActions linked to selectedBulkAction', () => {
|
||||
expect(tableProps.bulkActions).toEqual([el.instance().selectedBulkAction]);
|
||||
});
|
||||
describe('individual columns', () => {
|
||||
let columns;
|
||||
beforeEach(() => {
|
||||
@@ -277,41 +275,14 @@ describe('SubmissionsTable component', () => {
|
||||
});
|
||||
describe('handleViewAllResponsesClick', () => {
|
||||
it('calls loadSelectionForReview with submissionUUID from all rows if there are no selectedRows', () => {
|
||||
const data = {
|
||||
selectedRows: [
|
||||
],
|
||||
tableInstance: {
|
||||
rows: [
|
||||
{ original: { submissionUUID: '123' } },
|
||||
{ original: { submissionUUID: '456' } },
|
||||
{ original: { submissionUUID: '789' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
el.instance().handleViewAllResponsesClick(data);
|
||||
const data = [
|
||||
{ original: { submissionUUID: '123' } },
|
||||
{ original: { submissionUUID: '456' } },
|
||||
{ original: { submissionUUID: '789' } },
|
||||
];
|
||||
el.instance().handleViewAllResponsesClick(data)();
|
||||
expect(el.instance().props.loadSelectionForReview).toHaveBeenCalledWith(['123', '456', '789']);
|
||||
});
|
||||
it('calls loadSelectionForReview with submissionUUID from selected rows if there are any', () => {
|
||||
const data = {
|
||||
selectedRows: [
|
||||
{ original: { submissionUUID: '123' } },
|
||||
{ original: { submissionUUID: '456' } },
|
||||
{ original: { submissionUUID: '789' } },
|
||||
],
|
||||
};
|
||||
el.instance().handleViewAllResponsesClick(data);
|
||||
expect(
|
||||
el.instance().props.loadSelectionForReview,
|
||||
).toHaveBeenCalledWith(['123', '456', '789']);
|
||||
});
|
||||
});
|
||||
describe('selectedBulkAction', () => {
|
||||
it('includes selection length and triggers handleViewAllResponsesClick', () => {
|
||||
const rows = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
const action = el.instance().selectedBulkAction(rows);
|
||||
expect(action.buttonText).toEqual(expect.stringContaining(rows.length.toString()));
|
||||
expect(action.handleClick).toEqual(el.instance().handleViewAllResponsesClick);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
30
src/containers/ListView/TableAction.jsx
Normal file
30
src/containers/ListView/TableAction.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const TableAction = ({ tableInstance, handleClick }) => (
|
||||
<Button
|
||||
onClick={handleClick(tableInstance.rows)}
|
||||
variant="primary"
|
||||
className="view-all-responses-btn"
|
||||
>
|
||||
<FormattedMessage {...messages.viewAllResponses} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
TableAction.defaultProps = {
|
||||
tableInstance: {
|
||||
rows: [],
|
||||
},
|
||||
};
|
||||
TableAction.propTypes = {
|
||||
tableInstance: PropTypes.shape({
|
||||
rows: PropTypes.arrayOf(PropTypes.object),
|
||||
}),
|
||||
handleClick: PropTypes.func.isRequired,
|
||||
};
|
||||
export default TableAction;
|
||||
20
src/containers/ListView/TableAction.test.jsx
Normal file
20
src/containers/ListView/TableAction.test.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { TableAction } from './TableAction';
|
||||
|
||||
describe('TableAction component', () => {
|
||||
const props = {
|
||||
tableInstance: { rows: [{ id: 1 }, { id: 2 }] },
|
||||
handleClick: jest.fn(),
|
||||
};
|
||||
test('snapshots', () => {
|
||||
const el = shallow(<TableAction {...props} handleClick={() => jest.fn()} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handleClick', () => {
|
||||
shallow(<TableAction {...props} />);
|
||||
expect(props.handleClick).toHaveBeenCalledWith(props.tableInstance.rows);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectedBulkAction component snapshots 1`] = `
|
||||
<Button
|
||||
className="view-selected-responses-btn"
|
||||
onClick={[MockFunction]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View selected responses ({value})"
|
||||
description="Button text to load selected responses for review/grading"
|
||||
id="ora-grading.ListView.viewSelectedResponses"
|
||||
values={
|
||||
Object {
|
||||
"value": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
`;
|
||||
@@ -3,237 +3,243 @@
|
||||
exports[`SubmissionsTable component component render tests snapshots snapshot: empty (no list data) 1`] = `""`;
|
||||
|
||||
exports[`SubmissionsTable component component render tests snapshots snapshot: happy path 1`] = `
|
||||
<DataTable
|
||||
FilterStatusComponent={[MockFunction FilterStatusComponent]}
|
||||
bulkActions={
|
||||
Array [
|
||||
[MockFunction this.selectedBulkAction],
|
||||
]
|
||||
}
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": "Username",
|
||||
"accessor": "username",
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatDate],
|
||||
"Header": "Learner submission date",
|
||||
"accessor": "dateSubmitted",
|
||||
"disableFilters": true,
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatGrade],
|
||||
"Header": "Grade",
|
||||
"accessor": "score",
|
||||
"disableFilters": true,
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatStatus],
|
||||
"Filter": "MultiSelectDropdownFilter",
|
||||
"Header": "Grading status",
|
||||
"accessor": "gradingStatus",
|
||||
"filter": "includesValue",
|
||||
"filterChoices": Array [
|
||||
Object {
|
||||
"name": "Ungraded",
|
||||
"value": "ungraded",
|
||||
},
|
||||
Object {
|
||||
"name": "Grading Completed",
|
||||
"value": "graded",
|
||||
},
|
||||
Object {
|
||||
"name": "Currently being graded by someone else",
|
||||
"value": "locked",
|
||||
},
|
||||
Object {
|
||||
"name": "You are currently grading this response",
|
||||
"value": "in-progress",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"dateSubmitted": "9:05:54 PM",
|
||||
"gradingStatus": "ungraded",
|
||||
"score": Object {
|
||||
"pointsEarned": 1,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"username": "username-1",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": "11:52:34 PM",
|
||||
"gradingStatus": "graded",
|
||||
"score": Object {
|
||||
"pointsEarned": 2,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"username": "username-2",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": "9:07:30 PM",
|
||||
"gradingStatus": "in-progress",
|
||||
"score": Object {
|
||||
"pointsEarned": 3,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"username": "username-3",
|
||||
},
|
||||
]
|
||||
}
|
||||
defaultColumnValues={
|
||||
Object {
|
||||
"Filter": "TextFilter",
|
||||
}
|
||||
}
|
||||
initialState={
|
||||
Object {
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
}
|
||||
}
|
||||
isFilterable={true}
|
||||
isPaginated={true}
|
||||
isSelectable={true}
|
||||
isSortable={true}
|
||||
itemCount={3}
|
||||
numBreakoutFilters={2}
|
||||
tableActions={
|
||||
Array [
|
||||
Object {
|
||||
"buttonText": "View all responses",
|
||||
"className": "view-all-responses-btn",
|
||||
"handleClick": [MockFunction this.handleViewAllResponsesClick],
|
||||
"variant": "primary",
|
||||
},
|
||||
]
|
||||
}
|
||||
<div
|
||||
className="submissions-table"
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
<DataTable
|
||||
FilterStatusComponent={[MockFunction FilterStatusComponent]}
|
||||
bulkActions={
|
||||
Array [
|
||||
<mockConstructor
|
||||
handleClick={[MockFunction this.handleViewAllResponsesClick]}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": "Username",
|
||||
"accessor": "username",
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatDate],
|
||||
"Header": "Learner submission date",
|
||||
"accessor": "dateSubmitted",
|
||||
"disableFilters": true,
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatGrade],
|
||||
"Header": "Grade",
|
||||
"accessor": "score",
|
||||
"disableFilters": true,
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatStatus],
|
||||
"Filter": "MultiSelectDropdownFilter",
|
||||
"Header": "Grading status",
|
||||
"accessor": "gradingStatus",
|
||||
"filter": "includesValue",
|
||||
"filterChoices": Array [
|
||||
Object {
|
||||
"name": "Ungraded",
|
||||
"value": "ungraded",
|
||||
},
|
||||
Object {
|
||||
"name": "Grading Completed",
|
||||
"value": "graded",
|
||||
},
|
||||
Object {
|
||||
"name": "Currently being graded by someone else",
|
||||
"value": "locked",
|
||||
},
|
||||
Object {
|
||||
"name": "You are currently grading this response",
|
||||
"value": "in-progress",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"dateSubmitted": "2021-12-08 09:06:15.319213+00:00",
|
||||
"gradingStatus": "ungraded",
|
||||
"score": Object {
|
||||
"pointsEarned": 1,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"username": "username-1",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": "2021-12-10 18:06:15.319213+00:00",
|
||||
"gradingStatus": "graded",
|
||||
"score": Object {
|
||||
"pointsEarned": 2,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"username": "username-2",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": "2021-12-11 07:06:15.319213+00:00",
|
||||
"gradingStatus": "in-progress",
|
||||
"score": Object {
|
||||
"pointsEarned": 3,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"username": "username-3",
|
||||
},
|
||||
]
|
||||
}
|
||||
defaultColumnValues={
|
||||
Object {
|
||||
"Filter": "TextFilter",
|
||||
}
|
||||
}
|
||||
initialState={
|
||||
Object {
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
}
|
||||
}
|
||||
isFilterable={true}
|
||||
isPaginated={true}
|
||||
isSelectable={true}
|
||||
isSortable={true}
|
||||
itemCount={3}
|
||||
numBreakoutFilters={2}
|
||||
tableActions={
|
||||
Array [
|
||||
<mockConstructor
|
||||
handleClick={[MockFunction this.handleViewAllResponsesClick]}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SubmissionsTable component component render tests snapshots snapshot: team happy path 1`] = `
|
||||
<DataTable
|
||||
FilterStatusComponent={[MockFunction FilterStatusComponent]}
|
||||
bulkActions={
|
||||
Array [
|
||||
[MockFunction this.selectedBulkAction],
|
||||
]
|
||||
}
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": "Team name",
|
||||
"accessor": "teamName",
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatDate],
|
||||
"Header": "Team submission date",
|
||||
"accessor": "dateSubmitted",
|
||||
"disableFilters": true,
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatGrade],
|
||||
"Header": "Grade",
|
||||
"accessor": "score",
|
||||
"disableFilters": true,
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatStatus],
|
||||
"Filter": "MultiSelectDropdownFilter",
|
||||
"Header": "Grading status",
|
||||
"accessor": "gradingStatus",
|
||||
"filter": "includesValue",
|
||||
"filterChoices": Array [
|
||||
Object {
|
||||
"name": "Ungraded",
|
||||
"value": "ungraded",
|
||||
},
|
||||
Object {
|
||||
"name": "Grading Completed",
|
||||
"value": "graded",
|
||||
},
|
||||
Object {
|
||||
"name": "Currently being graded by someone else",
|
||||
"value": "locked",
|
||||
},
|
||||
Object {
|
||||
"name": "You are currently grading this response",
|
||||
"value": "in-progress",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"dateSubmitted": "9:05:54 PM",
|
||||
"gradingStatus": "ungraded",
|
||||
"score": Object {
|
||||
"pointsEarned": 1,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"teamName": "teamName-1",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": "11:52:34 PM",
|
||||
"gradingStatus": "graded",
|
||||
"score": Object {
|
||||
"pointsEarned": 2,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"teamName": "teamName-2",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": "9:07:30 PM",
|
||||
"gradingStatus": "in-progress",
|
||||
"score": Object {
|
||||
"pointsEarned": 3,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"teamName": "teamName-3",
|
||||
},
|
||||
]
|
||||
}
|
||||
defaultColumnValues={
|
||||
Object {
|
||||
"Filter": "TextFilter",
|
||||
}
|
||||
}
|
||||
initialState={
|
||||
Object {
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
}
|
||||
}
|
||||
isFilterable={true}
|
||||
isPaginated={true}
|
||||
isSelectable={true}
|
||||
isSortable={true}
|
||||
itemCount={3}
|
||||
numBreakoutFilters={2}
|
||||
tableActions={
|
||||
Array [
|
||||
Object {
|
||||
"buttonText": "View all responses",
|
||||
"className": "view-all-responses-btn",
|
||||
"handleClick": [MockFunction this.handleViewAllResponsesClick],
|
||||
"variant": "primary",
|
||||
},
|
||||
]
|
||||
}
|
||||
<div
|
||||
className="submissions-table"
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
<DataTable
|
||||
FilterStatusComponent={[MockFunction FilterStatusComponent]}
|
||||
bulkActions={
|
||||
Array [
|
||||
<mockConstructor
|
||||
handleClick={[MockFunction this.handleViewAllResponsesClick]}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": "Team name",
|
||||
"accessor": "teamName",
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatDate],
|
||||
"Header": "Team submission date",
|
||||
"accessor": "dateSubmitted",
|
||||
"disableFilters": true,
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatGrade],
|
||||
"Header": "Grade",
|
||||
"accessor": "score",
|
||||
"disableFilters": true,
|
||||
},
|
||||
Object {
|
||||
"Cell": [MockFunction this.formatStatus],
|
||||
"Filter": "MultiSelectDropdownFilter",
|
||||
"Header": "Grading status",
|
||||
"accessor": "gradingStatus",
|
||||
"filter": "includesValue",
|
||||
"filterChoices": Array [
|
||||
Object {
|
||||
"name": "Ungraded",
|
||||
"value": "ungraded",
|
||||
},
|
||||
Object {
|
||||
"name": "Grading Completed",
|
||||
"value": "graded",
|
||||
},
|
||||
Object {
|
||||
"name": "Currently being graded by someone else",
|
||||
"value": "locked",
|
||||
},
|
||||
Object {
|
||||
"name": "You are currently grading this response",
|
||||
"value": "in-progress",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"dateSubmitted": "2021-12-08 09:06:15.319213+00:00",
|
||||
"gradingStatus": "ungraded",
|
||||
"score": Object {
|
||||
"pointsEarned": 1,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"teamName": "teamName-1",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": "2021-12-10 18:06:15.319213+00:00",
|
||||
"gradingStatus": "graded",
|
||||
"score": Object {
|
||||
"pointsEarned": 2,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"teamName": "teamName-2",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": "2021-12-11 07:06:15.319213+00:00",
|
||||
"gradingStatus": "in-progress",
|
||||
"score": Object {
|
||||
"pointsEarned": 3,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"teamName": "teamName-3",
|
||||
},
|
||||
]
|
||||
}
|
||||
defaultColumnValues={
|
||||
Object {
|
||||
"Filter": "TextFilter",
|
||||
}
|
||||
}
|
||||
initialState={
|
||||
Object {
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
}
|
||||
}
|
||||
isFilterable={true}
|
||||
isPaginated={true}
|
||||
isSelectable={true}
|
||||
isSortable={true}
|
||||
itemCount={3}
|
||||
numBreakoutFilters={2}
|
||||
tableActions={
|
||||
Array [
|
||||
<mockConstructor
|
||||
handleClick={[MockFunction this.handleViewAllResponsesClick]}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TableAction component snapshots 1`] = `
|
||||
<Button
|
||||
className="view-all-responses-btn"
|
||||
onClick={[MockFunction]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View all responses"
|
||||
description="Button text to load all responses for review/grading"
|
||||
id="ora-grading.ListView.viewAllResponses"
|
||||
/>
|
||||
</Button>
|
||||
`;
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FileRenderer, isSupported } from 'components/FilePreview';
|
||||
import { FileRenderer } from 'components/FilePreview';
|
||||
import { isSupported } from 'components/FilePreview/hooks';
|
||||
|
||||
/**
|
||||
* <PreviewDisplay />
|
||||
|
||||
@@ -7,7 +7,6 @@ import { PreviewDisplay } from './PreviewDisplay';
|
||||
|
||||
jest.mock('components/FilePreview', () => ({
|
||||
FileRenderer: () => 'FileRenderer',
|
||||
isSupported: jest.requireActual('components/FilePreview').isSupported,
|
||||
}));
|
||||
|
||||
describe('PreviewDisplay', () => {
|
||||
|
||||
@@ -43,6 +43,11 @@
|
||||
.preview-display {
|
||||
padding: map-get($spacers, 3) 0;
|
||||
}
|
||||
|
||||
.response-display-text-content {
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import messages from './messages';
|
||||
jest.mock('./components/FileNameCell', () => jest.fn().mockName('FileNameCell'));
|
||||
jest.mock('./components/FileExtensionCell', () => jest.fn().mockName('FileExtensionCell'));
|
||||
jest.mock('./components/FilePopoverCell', () => jest.fn().mockName('FilePopoverCell'));
|
||||
jest.mock('./FileDownload', () => 'FileDownload');
|
||||
|
||||
describe('SubmissionFiles', () => {
|
||||
describe('component', () => {
|
||||
|
||||
@@ -95,7 +95,7 @@ exports[`SubmissionFiles component snapshot files existed for props 1`] = `
|
||||
<Card.Footer
|
||||
className="text-right"
|
||||
>
|
||||
<Connect(FileDownload)
|
||||
<FileDownload
|
||||
files={
|
||||
Array [
|
||||
Object {
|
||||
|
||||
@@ -5,9 +5,11 @@ exports[`ResponseDisplay component snapshot file upload disable with valid respo
|
||||
className="response-display"
|
||||
>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Section
|
||||
className="response-display-text-content"
|
||||
>
|
||||
parsed html (sanitized (some text response here))
|
||||
</Card.Body>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</div>
|
||||
`;
|
||||
@@ -62,9 +64,11 @@ exports[`ResponseDisplay component snapshot file upload enable with valid respon
|
||||
}
|
||||
/>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Section
|
||||
className="response-display-text-content"
|
||||
>
|
||||
parsed html (sanitized (some text response here))
|
||||
</Card.Body>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -48,7 +48,7 @@ export class ResponseDisplay extends React.Component {
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
this.textContents.map((textContent, index) => (
|
||||
<Card key={index}>
|
||||
<Card.Body>{textContent}</Card.Body>
|
||||
<Card.Section className="response-display-text-content">{textContent}</Card.Section>
|
||||
</Card>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Cancel, Highlight } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { gradingStatuses as statuses } from 'data/services/lms/constants';
|
||||
|
||||
import StopGradingConfirmModal from './StopGradingConfirmModal';
|
||||
import OverrideGradeConfirmModal from './OverrideGradeConfirmModal';
|
||||
import messages from './messages';
|
||||
|
||||
export const buttonArgs = {
|
||||
[statuses.ungraded]: {
|
||||
label: messages.startGrading,
|
||||
iconAfter: Highlight,
|
||||
},
|
||||
[statuses.graded]: {
|
||||
label: messages.overrideGrade,
|
||||
iconAfter: Highlight,
|
||||
},
|
||||
[statuses.inProgress]: {
|
||||
label: messages.stopGrading,
|
||||
iconAfter: Cancel,
|
||||
},
|
||||
};
|
||||
|
||||
export class StartGradingButton extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showConfirmStopGrading: false,
|
||||
showConfirmOverrideGrade: false,
|
||||
};
|
||||
|
||||
this.showConfirmStopGrading = this.showConfirmStopGrading.bind(this);
|
||||
this.hideConfirmStopGrading = this.hideConfirmStopGrading.bind(this);
|
||||
this.showConfirmOverrideGrade = this.showConfirmOverrideGrade.bind(this);
|
||||
this.hideConfirmOverrideGrade = this.hideConfirmOverrideGrade.bind(this);
|
||||
this.confirmStopGrading = this.confirmStopGrading.bind(this);
|
||||
this.confirmOverrideGrade = this.confirmOverrideGrade.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
showConfirmStopGrading() {
|
||||
this.setState({ showConfirmStopGrading: true });
|
||||
}
|
||||
|
||||
hideConfirmStopGrading() {
|
||||
this.setState({ showConfirmStopGrading: false });
|
||||
}
|
||||
|
||||
showConfirmOverrideGrade() {
|
||||
this.setState({ showConfirmOverrideGrade: true });
|
||||
}
|
||||
|
||||
hideConfirmOverrideGrade() {
|
||||
this.setState({ showConfirmOverrideGrade: false });
|
||||
}
|
||||
|
||||
confirmStopGrading() {
|
||||
this.hideConfirmStopGrading();
|
||||
this.props.stopGrading();
|
||||
}
|
||||
|
||||
confirmOverrideGrade() {
|
||||
this.hideConfirmOverrideGrade();
|
||||
this.props.startGrading();
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
if (this.props.gradingStatus === statuses.inProgress) {
|
||||
this.showConfirmStopGrading();
|
||||
} else if (this.props.gradingStatus === statuses.graded) {
|
||||
this.showConfirmOverrideGrade();
|
||||
} else {
|
||||
this.props.startGrading();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { gradingStatus } = this.props;
|
||||
if (gradingStatus === statuses.locked) {
|
||||
return null;
|
||||
}
|
||||
const args = buttonArgs[gradingStatus];
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
iconAfter={args.iconAfter}
|
||||
onClick={this.handleClick}
|
||||
disabled={this.props.gradeIsPending || this.props.lockIsPending}
|
||||
>
|
||||
<FormattedMessage {...args.label} />
|
||||
</Button>
|
||||
<OverrideGradeConfirmModal
|
||||
isOpen={this.state.showConfirmOverrideGrade}
|
||||
onCancel={this.hideConfirmOverrideGrade}
|
||||
onConfirm={this.confirmOverrideGrade}
|
||||
/>
|
||||
<StopGradingConfirmModal
|
||||
isOpen={this.state.showConfirmStopGrading}
|
||||
onCancel={this.hideConfirmStopGrading}
|
||||
onConfirm={this.confirmStopGrading}
|
||||
isOverride={this.props.gradeStatus === statuses.graded}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StartGradingButton.propTypes = {
|
||||
gradeStatus: PropTypes.string.isRequired,
|
||||
gradingStatus: PropTypes.string.isRequired,
|
||||
startGrading: PropTypes.func.isRequired,
|
||||
stopGrading: PropTypes.func.isRequired,
|
||||
gradeIsPending: PropTypes.bool.isRequired,
|
||||
lockIsPending: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
gradeIsPending: selectors.requests.isPending(state, { requestKey: RequestKeys.submitGrade }),
|
||||
lockIsPending: selectors.requests.isPending(state, { requestKey: RequestKeys.setLock }),
|
||||
gradeStatus: selectors.grading.selected.gradeStatus(state),
|
||||
gradingStatus: selectors.grading.selected.gradingStatus(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
startGrading: thunkActions.grading.startGrading,
|
||||
stopGrading: thunkActions.grading.cancelGrading,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StartGradingButton);
|
||||
@@ -1,132 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { gradingStatuses as statuses } from 'data/services/lms/constants';
|
||||
|
||||
import {
|
||||
StartGradingButton,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './StartGradingButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
grading: {
|
||||
selected: {
|
||||
gradeStatus: (state) => ({ gradeStatus: state }),
|
||||
gradingStatus: (state) => ({ gradingStatus: state }),
|
||||
},
|
||||
},
|
||||
requests: { isPending: (...args) => ({ isPending: args }) },
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
startGrading: jest.fn(),
|
||||
stopGrading: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('./OverrideGradeConfirmModal', () => 'OverrideGradeConfirmModal');
|
||||
jest.mock('./StopGradingConfirmModal', () => 'StopGradingConfirmModal');
|
||||
|
||||
let el;
|
||||
|
||||
describe('StartGradingButton component', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
gradeIsPending: false,
|
||||
lockIsPending: false,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.startGrading = jest.fn().mockName('this.props.startGrading');
|
||||
props.stopGrading = jest.fn().mockName('this.props.stopGrading');
|
||||
});
|
||||
describe('snapshotes', () => {
|
||||
const mockedEl = (gradingStatus, gradeStatus) => {
|
||||
const renderedEl = shallow(
|
||||
<StartGradingButton
|
||||
{...props}
|
||||
gradingStatus={gradingStatus}
|
||||
gradeStatus={gradeStatus || gradingStatus}
|
||||
/>,
|
||||
);
|
||||
const mockMethod = (methodName) => {
|
||||
renderedEl.instance()[methodName] = jest.fn().mockName(`this.${methodName}`);
|
||||
};
|
||||
mockMethod('handleClick');
|
||||
mockMethod('hideConfirmOverrideGrade');
|
||||
mockMethod('confirmOverrideGrade');
|
||||
mockMethod('hideConfirmStopGrading');
|
||||
mockMethod('confirmStopGrading');
|
||||
return renderedEl;
|
||||
};
|
||||
test('snapshot: locked (null)', () => {
|
||||
el = mockedEl(statuses.locked);
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot: ungraded (startGrading callback)', () => {
|
||||
expect(mockedEl(statuses.ungraded).instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: grade pending (disabled)', () => {
|
||||
el = mockedEl(statuses.ungraded);
|
||||
el.setProps({ gradeIsPending: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: lock pending (disabled)', () => {
|
||||
el = mockedEl(statuses.ungraded);
|
||||
el.setProps({ lockIsPending: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: graded, confirmOverride (startGrading callback)', () => {
|
||||
el = mockedEl(statuses.graded);
|
||||
el.setState({ showConfirmOverrideGrade: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: inProgress, isOverride, confirmStop (stopGrading callback)', () => {
|
||||
el = mockedEl(statuses.inProgress, statuses.graded);
|
||||
el.setState({ showConfirmStopGrading: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('gradeIsPending loads from requests.gradeIsPending(submitGrade)', () => {
|
||||
expect(mapped.gradeIsPending).toEqual(
|
||||
selectors.requests.isPending(
|
||||
testState,
|
||||
{ requestKey: RequestKeys.submitGrade },
|
||||
),
|
||||
);
|
||||
});
|
||||
test('lockIsPending loads from requests.lockIsPending(setLock)', () => {
|
||||
expect(mapped.lockIsPending).toEqual(
|
||||
selectors.requests.isPending(
|
||||
testState,
|
||||
{ requestKey: RequestKeys.setLock },
|
||||
),
|
||||
);
|
||||
});
|
||||
test('gradeStatus loads from grading.selected.gradeStatus', () => {
|
||||
expect(mapped.gradeStatus).toEqual(selectors.grading.selected.gradeStatus(testState));
|
||||
});
|
||||
test('gradingStatus loads from grading.selected.gradingStatus', () => {
|
||||
expect(mapped.gradingStatus).toEqual(selectors.grading.selected.gradingStatus(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads startGrading from thunkActions.grading.stargGrading', () => {
|
||||
expect(mapDispatchToProps.startGrading).toEqual(thunkActions.grading.startGrading);
|
||||
});
|
||||
it('loads stopGrading from thunkActions.grading.cancelGrading', () => {
|
||||
expect(mapDispatchToProps.stopGrading).toEqual(thunkActions.grading.cancelGrading);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StartGradingButton component component snapshots hide: renders empty component if hook.hide is true 1`] = `""`;
|
||||
|
||||
exports[`StartGradingButton component component snapshots smoke test: forwards props to components from hooks 1`] = `
|
||||
<Fragment>
|
||||
<Button
|
||||
props="hooks.buttonArgs"
|
||||
variant="primary"
|
||||
/>
|
||||
<OverrideGradeConfirmModal
|
||||
props="hooks.overrideGradeArgs"
|
||||
/>
|
||||
<StopGradingConfirmModal
|
||||
props="hooks.stopGradingArgs"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -0,0 +1,141 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Cancel, Highlight } from '@edx/paragon/icons';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { gradingStatuses as statuses } from 'data/services/lms/constants';
|
||||
import { StrictDict } from 'utils';
|
||||
import * as module from './hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const buttonConfig = {
|
||||
[statuses.ungraded]: {
|
||||
label: messages.startGrading,
|
||||
iconAfter: Highlight,
|
||||
},
|
||||
[statuses.graded]: {
|
||||
label: messages.overrideGrade,
|
||||
iconAfter: Highlight,
|
||||
},
|
||||
[statuses.inProgress]: {
|
||||
label: messages.stopGrading,
|
||||
iconAfter: Cancel,
|
||||
},
|
||||
};
|
||||
|
||||
export const state = StrictDict({
|
||||
showConfirmStopGrading: (val) => React.useState(val),
|
||||
showConfirmOverrideGrade: (val) => React.useState(val),
|
||||
});
|
||||
|
||||
export const reduxValues = () => ({
|
||||
gradeStatus: useSelector(selectors.grading.selected.gradeStatus),
|
||||
gradingStatus: useSelector(selectors.grading.selected.gradingStatus),
|
||||
gradeIsPending: useSelector((reduxState) => (
|
||||
selectors.requests.isPending(reduxState, { requestKey: RequestKeys.submitGrade })
|
||||
)),
|
||||
lockIsPending: useSelector((reduxState) => (
|
||||
selectors.requests.isPending(reduxState, { requestKey: RequestKeys.setLock })
|
||||
)),
|
||||
});
|
||||
|
||||
export const buttonArgs = ({
|
||||
intl,
|
||||
dispatch,
|
||||
overrideGradeState,
|
||||
stopGradingState,
|
||||
gradingStatus,
|
||||
isPending,
|
||||
}) => ({
|
||||
iconAfter: module.buttonConfig[gradingStatus].iconAfter,
|
||||
children: intl.formatMessage(module.buttonConfig[gradingStatus].label),
|
||||
disabled: isPending,
|
||||
onClick: () => {
|
||||
if (gradingStatus === statuses.inProgress) {
|
||||
stopGradingState.setShow(true);
|
||||
} else if (gradingStatus === statuses.graded) {
|
||||
overrideGradeState.setShow(true);
|
||||
} else {
|
||||
dispatch(thunkActions.grading.startGrading());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const overrideGradeArgs = ({
|
||||
dispatch,
|
||||
overrideGradeState: { show, setShow },
|
||||
}) => ({
|
||||
isOpen: show,
|
||||
onCancel: () => setShow(false),
|
||||
onConfirm: () => {
|
||||
setShow(false);
|
||||
dispatch(thunkActions.grading.startGrading());
|
||||
},
|
||||
});
|
||||
|
||||
export const stopGradingArgs = ({
|
||||
dispatch,
|
||||
isGraded,
|
||||
stopGradingState: { show, setShow },
|
||||
}) => ({
|
||||
isOpen: show,
|
||||
onCancel: () => setShow(false),
|
||||
onConfirm: () => {
|
||||
setShow(false);
|
||||
dispatch(thunkActions.grading.cancelGrading());
|
||||
},
|
||||
isOverride: isGraded,
|
||||
});
|
||||
|
||||
export const buttonHooks = ({
|
||||
dispatch,
|
||||
intl,
|
||||
}) => {
|
||||
const showState = {
|
||||
stopGrading: state.showConfirmStopGrading(false),
|
||||
overrideGrade: state.showConfirmOverrideGrade(false),
|
||||
};
|
||||
const overrideGradeState = {
|
||||
show: showState.overrideGrade[0],
|
||||
setShow: showState.overrideGrade[1],
|
||||
};
|
||||
const stopGradingState = {
|
||||
show: showState.stopGrading[0],
|
||||
setShow: showState.stopGrading[1],
|
||||
};
|
||||
|
||||
const {
|
||||
gradeStatus,
|
||||
gradingStatus,
|
||||
gradeIsPending,
|
||||
lockIsPending,
|
||||
} = module.reduxValues();
|
||||
|
||||
const hide = gradingStatus === statuses.locked;
|
||||
if (hide) {
|
||||
return { hide };
|
||||
}
|
||||
return {
|
||||
hide,
|
||||
overrideGradeArgs: module.overrideGradeArgs({
|
||||
dispatch,
|
||||
overrideGradeState,
|
||||
}),
|
||||
stopGradingArgs: module.stopGradingArgs({
|
||||
dispatch,
|
||||
stopGradingState,
|
||||
isGraded: gradeStatus === statuses.graded,
|
||||
}),
|
||||
buttonArgs: module.buttonArgs({
|
||||
intl,
|
||||
dispatch,
|
||||
stopGradingState,
|
||||
overrideGradeState,
|
||||
isPending: lockIsPending || gradeIsPending,
|
||||
gradingStatus,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,280 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { formatMessage, MockUseState } from 'testUtils';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { gradingStatuses } from 'data/services/lms/constants';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { keyStore } from 'utils';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useSelector: args => ({ useSelector: args }),
|
||||
}));
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
grading: {
|
||||
selected: {
|
||||
gradeStatus: jest.fn((...args) => ({ gradeStatus: args })),
|
||||
gradingStatus: jest.fn((...args) => ({ gradingStatus: args })),
|
||||
},
|
||||
},
|
||||
requests: {
|
||||
isPending: jest.fn((...args) => ({ isPending: args })),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
startGrading: jest.fn((...args) => ({ startGrading: args })),
|
||||
cancelGrading: jest.fn((...args) => ({ cancelGrading: args })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
const hookKeys = keyStore(hooks);
|
||||
|
||||
let hook;
|
||||
const testValue = 'my-test-value';
|
||||
|
||||
const dispatch = jest.fn();
|
||||
const intl = { formatMessage };
|
||||
describe('Start Grading Button hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.showConfirmStopGrading);
|
||||
state.testGetter(state.keys.showConfirmOverrideGrade);
|
||||
});
|
||||
describe('redux values', () => {
|
||||
const testState = { my: 'test-state' };
|
||||
beforeEach(() => {
|
||||
hook = hooks.reduxValues();
|
||||
});
|
||||
test('gradeStatus drawn from selected grade status', () => {
|
||||
expect(hook.gradeStatus).toEqual(
|
||||
useSelector(selectors.grading.selected.gradeStatus),
|
||||
);
|
||||
});
|
||||
test('gradingStatus drawn from selected grading status', () => {
|
||||
expect(hook.gradingStatus).toEqual(
|
||||
useSelector(selectors.grading.selected.gradingStatus),
|
||||
);
|
||||
});
|
||||
test('gradeIsPending drawn from requests.isPending on submitGrade request', () => {
|
||||
expect(hook.gradeIsPending.useSelector(testState)).toEqual(
|
||||
selectors.requests.isPending(testState, { requestKey: RequestKeys.submitGrade }),
|
||||
);
|
||||
});
|
||||
test('lockIsPending drawn from requests.isPending on setLock request', () => {
|
||||
expect(hook.lockIsPending.useSelector(testState)).toEqual(
|
||||
selectors.requests.isPending(testState, { requestKey: RequestKeys.setLock }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('non-state hooks', () => {
|
||||
beforeEach(state.mock);
|
||||
afterEach(state.restore);
|
||||
describe('buttonArgs', () => {
|
||||
const props = {
|
||||
intl,
|
||||
dispatch,
|
||||
overrideGradeState: { setShow: jest.fn() },
|
||||
stopGradingState: { setShow: jest.fn() },
|
||||
gradingStatus: gradingStatuses.ungraded,
|
||||
isPending: false,
|
||||
};
|
||||
describe('returned args', () => {
|
||||
const testStatusConfig = (status) => {
|
||||
describe(`status config for ${status} submissions`, () => {
|
||||
beforeEach(() => {
|
||||
selectors.grading.selected.gradingStatus.mockReturnValue(status);
|
||||
hook = hooks.buttonArgs({ ...props, gradingStatus: status });
|
||||
});
|
||||
it('loads configured iconAfter', () => {
|
||||
expect(hook.iconAfter).toEqual(hooks.buttonConfig[status].iconAfter);
|
||||
});
|
||||
it('loads and formats label from config', () => {
|
||||
expect(hook.children).toEqual(formatMessage(hooks.buttonConfig[status].label));
|
||||
});
|
||||
describe('onClick', () => {
|
||||
if (status === gradingStatuses.inProgress) {
|
||||
it('shows the confirm stop-grading modal', () => {
|
||||
hook.onClick();
|
||||
expect(props.stopGradingState.setShow).toHaveBeenCalledWith(true);
|
||||
});
|
||||
} else if (status === gradingStatuses.graded) {
|
||||
it('shows the confirm stop-grading modal', () => {
|
||||
hook.onClick();
|
||||
expect(props.overrideGradeState.setShow).toHaveBeenCalledWith(true);
|
||||
});
|
||||
} else {
|
||||
it('dispatches the startGrading thunkAction', () => {
|
||||
hook.onClick();
|
||||
expect(props.dispatch).toHaveBeenCalledWith(thunkActions.grading.startGrading());
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
testStatusConfig(gradingStatuses.ungraded);
|
||||
testStatusConfig(gradingStatuses.graded);
|
||||
testStatusConfig(gradingStatuses.inProgress);
|
||||
});
|
||||
});
|
||||
describe('overrideGradeArgs', () => {
|
||||
const props = { show: 'test-show', setShow: jest.fn() };
|
||||
beforeEach(() => {
|
||||
hook = hooks.overrideGradeArgs({ dispatch, overrideGradeState: props });
|
||||
});
|
||||
test('isOpen returns the override grade show state', () => {
|
||||
expect(hook.isOpen).toEqual(props.show);
|
||||
});
|
||||
test('onCancel: sets override grade show state to false', () => {
|
||||
hook.onCancel();
|
||||
expect(props.setShow).toHaveBeenCalledWith(false);
|
||||
});
|
||||
describe('onConfirm', () => {
|
||||
test('sets override grade show state to false and starts grading', () => {
|
||||
hook.onConfirm();
|
||||
expect(props.setShow).toHaveBeenCalledWith(false);
|
||||
expect(dispatch).toHaveBeenCalledWith(thunkActions.grading.startGrading());
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('stopGradingArgs', () => {
|
||||
const props = { show: 'test-show', setShow: jest.fn() };
|
||||
beforeEach(() => {
|
||||
hook = hooks.stopGradingArgs({
|
||||
dispatch,
|
||||
isGraded: testValue,
|
||||
stopGradingState: props,
|
||||
});
|
||||
});
|
||||
test('isOpen returns the stop grading show state', () => {
|
||||
expect(hook.isOpen).toEqual(props.show);
|
||||
});
|
||||
test('onCancel: sets stop grading show state to false', () => {
|
||||
hook.onCancel();
|
||||
expect(props.setShow).toHaveBeenCalledWith(false);
|
||||
});
|
||||
describe('onConfirm', () => {
|
||||
test('sets stop grading show state to false and cancels grading', () => {
|
||||
hook.onConfirm();
|
||||
expect(props.setShow).toHaveBeenCalledWith(false);
|
||||
expect(dispatch).toHaveBeenCalledWith(thunkActions.grading.cancelGrading());
|
||||
});
|
||||
});
|
||||
test('isOverride is set to isGraded arg', () => {
|
||||
expect(hook.isOverride).toEqual(testValue);
|
||||
});
|
||||
});
|
||||
describe('button component hooks', () => {
|
||||
const reduxValues = {
|
||||
gradeStatus: 'redux-values.gradeStatus',
|
||||
gradingStatus: 'redux-values.gradingStatus',
|
||||
gradeIsPending: false,
|
||||
lockIsPending: false,
|
||||
};
|
||||
const mocks = {
|
||||
buttonArgs: jest.fn(args => ({ buttonArgs: args })),
|
||||
overrideGradeArgs: jest.fn(args => ({ overrideGradeArgs: args })),
|
||||
reduxValues: () => reduxValues,
|
||||
stopGradingArgs: jest.fn(args => ({ stopGradingArgs: args })),
|
||||
};
|
||||
let overrideGradeState;
|
||||
let stopGradingState;
|
||||
const mockHooks = (values) => {
|
||||
jest.spyOn(hooks, hookKeys.buttonArgs).mockImplementationOnce(mocks.buttonArgs);
|
||||
jest.spyOn(hooks, hookKeys.overrideGradeArgs).mockImplementationOnce(mocks.overrideGradeArgs);
|
||||
jest.spyOn(hooks, hookKeys.reduxValues).mockImplementationOnce(() => ({
|
||||
...reduxValues,
|
||||
...values,
|
||||
}));
|
||||
jest.spyOn(hooks, hookKeys.stopGradingArgs).mockImplementationOnce(mocks.stopGradingArgs);
|
||||
};
|
||||
beforeEach(() => {
|
||||
mockHooks(reduxValues);
|
||||
hook = hooks.buttonHooks({ dispatch, intl });
|
||||
overrideGradeState = {
|
||||
show: state.stateVals.showConfirmOverrideGrade,
|
||||
setShow: state.setState.showConfirmOverrideGrade,
|
||||
};
|
||||
stopGradingState = {
|
||||
show: state.stateVals.showConfirmStopGrading,
|
||||
setShow: state.setState.showConfirmStopGrading,
|
||||
};
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes showConfirmStopGrading and showConfirmOverrideGrade to false', () => {
|
||||
expect(hooks.state.showConfirmStopGrading).toHaveBeenCalledWith(false);
|
||||
expect(hooks.state.showConfirmOverrideGrade).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
describe('returned object', () => {
|
||||
test('hide returns true iff gradingStatus is not locked', () => {
|
||||
expect(hook.hide).toEqual(false);
|
||||
mockHooks({ gradingStatus: gradingStatuses.locked });
|
||||
hook = hooks.buttonHooks({ dispatch, intl });
|
||||
expect(hook.hide).toEqual(true);
|
||||
});
|
||||
test('returns only hide hook if locked', () => {
|
||||
mockHooks({ gradingStatus: gradingStatuses.locked });
|
||||
hook = hooks.buttonHooks({ dispatch, intl });
|
||||
expect(hook).toEqual({ hide: true });
|
||||
});
|
||||
test('overrideGradeArgs: calls local hook with dispatch and override grade state', () => {
|
||||
expect(hook.overrideGradeArgs).toEqual(mocks.overrideGradeArgs({
|
||||
dispatch,
|
||||
overrideGradeState,
|
||||
}));
|
||||
});
|
||||
describe('stopGradingArgs forwards local hook called with', () => {
|
||||
test('dispatch, stop grading state, and if submission is graded', () => {
|
||||
expect(hook.stopGradingArgs).toEqual(mocks.stopGradingArgs({
|
||||
dispatch,
|
||||
stopGradingState,
|
||||
isGraded: false,
|
||||
}));
|
||||
mockHooks({ gradeStatus: gradingStatuses.graded });
|
||||
hook = hooks.buttonHooks({ dispatch, intl });
|
||||
expect(hook.stopGradingArgs).toEqual(mocks.stopGradingArgs({
|
||||
dispatch,
|
||||
stopGradingState,
|
||||
isGraded: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
describe('buttonArgs forwards local hook called with', () => {
|
||||
test('props, local state, grading status and whether lock or grade are pending', () => {
|
||||
expect(hook.buttonArgs).toEqual(mocks.buttonArgs({
|
||||
intl,
|
||||
dispatch,
|
||||
stopGradingState,
|
||||
overrideGradeState,
|
||||
gradingStatus: reduxValues.gradingStatus,
|
||||
isPending: false,
|
||||
}));
|
||||
[
|
||||
{ gradeIsPending: true },
|
||||
{ lockIsPending: true },
|
||||
{ gradeIsPending: true, lockIsPending: true },
|
||||
].forEach(values => {
|
||||
mockHooks(values);
|
||||
hook = hooks.buttonHooks({ dispatch, intl });
|
||||
expect(hook.buttonArgs).toEqual(mocks.buttonArgs({
|
||||
intl,
|
||||
dispatch,
|
||||
stopGradingState,
|
||||
overrideGradeState,
|
||||
gradingStatus: reduxValues.gradingStatus,
|
||||
isPending: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import StopGradingConfirmModal from '../StopGradingConfirmModal';
|
||||
import OverrideGradeConfirmModal from '../OverrideGradeConfirmModal';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
export const StartGradingButton = ({ intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
hide,
|
||||
buttonArgs,
|
||||
overrideGradeArgs,
|
||||
stopGradingArgs,
|
||||
} = hooks.buttonHooks({ dispatch, intl });
|
||||
|
||||
if (hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="primary" {...buttonArgs} />
|
||||
<OverrideGradeConfirmModal {...overrideGradeArgs} />
|
||||
<StopGradingConfirmModal {...stopGradingArgs} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StartGradingButton.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(StartGradingButton);
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import * as hooks from './hooks';
|
||||
import { StartGradingButton } from '.';
|
||||
|
||||
jest.mock('../OverrideGradeConfirmModal', () => 'OverrideGradeConfirmModal');
|
||||
jest.mock('../StopGradingConfirmModal', () => 'StopGradingConfirmModal');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
buttonHooks: jest.fn(),
|
||||
}));
|
||||
|
||||
const intl = { formatMessage };
|
||||
|
||||
let el;
|
||||
describe('StartGradingButton component', () => {
|
||||
describe('component', () => {
|
||||
const dispatch = useDispatch();
|
||||
const props = { intl };
|
||||
const buttonHooks = {
|
||||
hide: false,
|
||||
buttonArgs: { props: 'hooks.buttonArgs' },
|
||||
overrideGradeArgs: { props: 'hooks.overrideGradeArgs' },
|
||||
stopGradingArgs: { props: 'hooks.stopGradingArgs' },
|
||||
};
|
||||
describe('behavior', () => {
|
||||
it('initializes buttonHooks with dispatch and intl fields', () => {
|
||||
hooks.buttonHooks.mockReturnValueOnce(buttonHooks);
|
||||
el = shallow(<StartGradingButton {...props} />);
|
||||
expect(hooks.buttonHooks).toHaveBeenCalledWith({ dispatch, intl });
|
||||
});
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('hide: renders empty component if hook.hide is true', () => {
|
||||
hooks.buttonHooks.mockReturnValueOnce({ ...buttonHooks, hide: true });
|
||||
el = shallow(<StartGradingButton {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('smoke test: forwards props to components from hooks', () => {
|
||||
hooks.buttonHooks.mockReturnValueOnce(buttonHooks);
|
||||
el = shallow(<StartGradingButton {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
startGrading: {
|
||||
id: 'ora-grading.ReviewActions.StartGradingButton.startGrading',
|
||||
defaultMessage: 'Start grading',
|
||||
description: 'Review pane button text to start grading',
|
||||
},
|
||||
overrideGrade: {
|
||||
id: 'ora-grading.ReviewActions.StartGradingButton.overrideGrade',
|
||||
defaultMessage: 'Override grade',
|
||||
description: 'Review pane button text to start grading an already graded submission',
|
||||
},
|
||||
stopGrading: {
|
||||
id: 'ora-grading.ReviewActions.StartGradingButton.stopGrading',
|
||||
defaultMessage: 'Stop grading this response',
|
||||
description: 'Review pane button text to stop grading',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,143 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StartGradingButton component component snapshotes snapshot: grade pending (disabled) 1`] = `
|
||||
<React.Fragment>
|
||||
<Button
|
||||
disabled={true}
|
||||
iconAfter={[MockFunction icons.Highlight]}
|
||||
onClick={[MockFunction this.handleClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Start grading"
|
||||
description="Review pane button text to start grading"
|
||||
id="ora-grading.ReviewActions.StartGradingButton.startGrading"
|
||||
/>
|
||||
</Button>
|
||||
<OverrideGradeConfirmModal
|
||||
isOpen={false}
|
||||
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
|
||||
onConfirm={[MockFunction this.confirmOverrideGrade]}
|
||||
/>
|
||||
<StopGradingConfirmModal
|
||||
isOpen={false}
|
||||
isOverride={false}
|
||||
onCancel={[MockFunction this.hideConfirmStopGrading]}
|
||||
onConfirm={[MockFunction this.confirmStopGrading]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`StartGradingButton component component snapshotes snapshot: graded, confirmOverride (startGrading callback) 1`] = `
|
||||
<React.Fragment>
|
||||
<Button
|
||||
disabled={false}
|
||||
iconAfter={[MockFunction icons.Highlight]}
|
||||
onClick={[MockFunction this.handleClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Override grade"
|
||||
description="Review pane button text to start grading an already graded submission"
|
||||
id="ora-grading.ReviewActions.StartGradingButton.overrideGrade"
|
||||
/>
|
||||
</Button>
|
||||
<OverrideGradeConfirmModal
|
||||
isOpen={true}
|
||||
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
|
||||
onConfirm={[MockFunction this.confirmOverrideGrade]}
|
||||
/>
|
||||
<StopGradingConfirmModal
|
||||
isOpen={false}
|
||||
isOverride={true}
|
||||
onCancel={[MockFunction this.hideConfirmStopGrading]}
|
||||
onConfirm={[MockFunction this.confirmStopGrading]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`StartGradingButton component component snapshotes snapshot: inProgress, isOverride, confirmStop (stopGrading callback) 1`] = `
|
||||
<React.Fragment>
|
||||
<Button
|
||||
disabled={false}
|
||||
iconAfter={[MockFunction icons.Cancel]}
|
||||
onClick={[MockFunction this.handleClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Stop grading this response"
|
||||
description="Review pane button text to stop grading"
|
||||
id="ora-grading.ReviewActions.StartGradingButton.stopGrading"
|
||||
/>
|
||||
</Button>
|
||||
<OverrideGradeConfirmModal
|
||||
isOpen={false}
|
||||
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
|
||||
onConfirm={[MockFunction this.confirmOverrideGrade]}
|
||||
/>
|
||||
<StopGradingConfirmModal
|
||||
isOpen={true}
|
||||
isOverride={true}
|
||||
onCancel={[MockFunction this.hideConfirmStopGrading]}
|
||||
onConfirm={[MockFunction this.confirmStopGrading]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`StartGradingButton component component snapshotes snapshot: lock pending (disabled) 1`] = `
|
||||
<React.Fragment>
|
||||
<Button
|
||||
disabled={true}
|
||||
iconAfter={[MockFunction icons.Highlight]}
|
||||
onClick={[MockFunction this.handleClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Start grading"
|
||||
description="Review pane button text to start grading"
|
||||
id="ora-grading.ReviewActions.StartGradingButton.startGrading"
|
||||
/>
|
||||
</Button>
|
||||
<OverrideGradeConfirmModal
|
||||
isOpen={false}
|
||||
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
|
||||
onConfirm={[MockFunction this.confirmOverrideGrade]}
|
||||
/>
|
||||
<StopGradingConfirmModal
|
||||
isOpen={false}
|
||||
isOverride={false}
|
||||
onCancel={[MockFunction this.hideConfirmStopGrading]}
|
||||
onConfirm={[MockFunction this.confirmStopGrading]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`StartGradingButton component component snapshotes snapshot: locked (null) 1`] = `null`;
|
||||
|
||||
exports[`StartGradingButton component component snapshotes snapshot: ungraded (startGrading callback) 1`] = `
|
||||
<React.Fragment>
|
||||
<Button
|
||||
disabled={false}
|
||||
iconAfter={[MockFunction icons.Highlight]}
|
||||
onClick={[MockFunction this.handleClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Start grading"
|
||||
description="Review pane button text to start grading"
|
||||
id="ora-grading.ReviewActions.StartGradingButton.startGrading"
|
||||
/>
|
||||
</Button>
|
||||
<OverrideGradeConfirmModal
|
||||
isOpen={false}
|
||||
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
|
||||
onConfirm={[MockFunction this.confirmOverrideGrade]}
|
||||
/>
|
||||
<StopGradingConfirmModal
|
||||
isOpen={false}
|
||||
isOverride={false}
|
||||
onCancel={[MockFunction this.hideConfirmStopGrading]}
|
||||
onConfirm={[MockFunction this.confirmStopGrading]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -16,7 +16,7 @@ import ReviewError from './ReviewError';
|
||||
*/
|
||||
export class LockErrors extends React.Component {
|
||||
get errorProp() {
|
||||
if (this.errorStatus === ErrorStatuses.forbidden) {
|
||||
if (this.props.errorStatus === ErrorStatuses.forbidden) {
|
||||
return {
|
||||
heading: messages.errorLockContestedHeading,
|
||||
message: messages.errorLockContested,
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { actions, selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys, ErrorStatuses } from 'data/constants/requests';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
import ReviewError from './ReviewError';
|
||||
|
||||
/**
|
||||
* <SubmitErrors />
|
||||
*/
|
||||
export class SubmitErrors extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.dismissError = this.dismissError.bind(this);
|
||||
}
|
||||
|
||||
get gradeNotSubmitted() {
|
||||
return {
|
||||
confirm: { onClick: this.props.resubmit, message: messages.resubmitGrade },
|
||||
headingMessage: messages.gradeNotSubmittedHeading,
|
||||
contentMessage: messages.gradeNotSubmittedContent,
|
||||
};
|
||||
}
|
||||
|
||||
get errorSubmittingGrade() {
|
||||
return {
|
||||
headingMessage: messages.errorSubmittingGradeHeading,
|
||||
contentMessage: messages.errorSubmittingGradeContent,
|
||||
};
|
||||
}
|
||||
|
||||
get errorProps() {
|
||||
if (this.props.errorStatus === ErrorStatuses.badRequest) {
|
||||
return this.gradeNotSubmitted;
|
||||
}
|
||||
if (this.props.errorStatus === ErrorStatuses.conflict) {
|
||||
return this.errorSubmittingGrade;
|
||||
}
|
||||
// TODO: Network-Log an error here for unhandled error type
|
||||
return this.gradeNotSubmitted;
|
||||
}
|
||||
|
||||
dismissError() {
|
||||
this.props.clearRequest({ requestKey: RequestKeys.submitGrade });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.errorStatus) {
|
||||
return null;
|
||||
}
|
||||
const props = this.errorProps;
|
||||
|
||||
return (
|
||||
<ReviewError
|
||||
actions={{
|
||||
cancel: { onClick: this.dismissError, message: messages.dismiss },
|
||||
confirm: props.confirm,
|
||||
}}
|
||||
headingMessage={props.headingMessage}
|
||||
>
|
||||
<FormattedMessage {...props.contentMessage} />
|
||||
</ReviewError>
|
||||
);
|
||||
}
|
||||
}
|
||||
SubmitErrors.defaultProps = {
|
||||
errorStatus: undefined,
|
||||
};
|
||||
SubmitErrors.propTypes = {
|
||||
// redux
|
||||
clearRequest: PropTypes.func.isRequired,
|
||||
errorStatus: PropTypes.number,
|
||||
resubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const requestKey = RequestKeys.submitGrade;
|
||||
export const mapStateToProps = (state) => ({
|
||||
errorStatus: selectors.requests.errorStatus(state, { requestKey }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
clearRequest: actions.requests.clearRequest,
|
||||
resubmit: thunkActions.grading.submitGrade,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SubmitErrors);
|
||||
@@ -1,79 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { actions, selectors, thunkActions } from 'data/redux';
|
||||
import { ErrorStatuses, RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import {
|
||||
SubmitErrors,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './SubmitErrors';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
actions: {
|
||||
requests: {
|
||||
clearRequest: jest.fn().mockName('actions.requests.clearRequest'),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
requests: {
|
||||
errorStatus: (...args) => ({ errorStatus: args }),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
submitGrade: jest.fn().mockName('thunkActions.grading.submitGrade'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let el;
|
||||
jest.mock('./ReviewError', () => 'ReviewError');
|
||||
|
||||
const requestKey = RequestKeys.submitGrade;
|
||||
|
||||
describe('SubmitErrors component', () => {
|
||||
const props = {};
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
props.resubmit = jest.fn();
|
||||
props.clearRequest = jest.fn();
|
||||
el = shallow(<SubmitErrors {...props} />);
|
||||
el.instance().dismissError = jest.fn().mockName('this.dismissError');
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('snapshot: no failure', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: with network failure', () => {
|
||||
el.setProps({ errorStatus: ErrorStatuses.badRequest });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: with conflict failure', () => {
|
||||
el.setProps({ errorStatus: ErrorStatuses.conflict });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('errorStatus loads from requests.errorStatus(fetchSubmission)', () => {
|
||||
expect(mapped.errorStatus).toEqual(
|
||||
selectors.requests.errorStatus(testState, { requestKey }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads clearRequest from actions.requests.clearRequest', () => {
|
||||
expect(mapDispatchToProps.clearRequest).toEqual(actions.requests.clearRequest);
|
||||
});
|
||||
it('loads resubmit from thunkActions.grading.submitGrade', () => {
|
||||
expect(mapDispatchToProps.resubmit).toEqual(thunkActions.grading.submitGrade);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SubmitErrors component snapshots snapshot: no failure 1`] = `""`;
|
||||
|
||||
exports[`SubmitErrors component snapshots snapshot: with valid error, loads from hook 1`] = `
|
||||
<ReviewError
|
||||
actions={
|
||||
Object {
|
||||
"cancel": "hooks.reviewActions.cancel",
|
||||
"confirm": "hooks.reviewActions.confirm",
|
||||
}
|
||||
}
|
||||
headingMessage="hooks.headingMessage"
|
||||
>
|
||||
hooks.content
|
||||
</ReviewError>
|
||||
`;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { actions, selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys, ErrorStatuses } from 'data/constants/requests';
|
||||
|
||||
import messages from './messages';
|
||||
import * as module from './hooks';
|
||||
|
||||
const requestKey = RequestKeys.submitGrade;
|
||||
|
||||
export const badRequestError = ({ dispatch }) => ({
|
||||
confirm: {
|
||||
onClick: () => dispatch(thunkActions.grading.submitGrade()),
|
||||
message: messages.resubmitGrade,
|
||||
},
|
||||
headingMessage: messages.gradeNotSubmittedHeading,
|
||||
contentMessage: messages.gradeNotSubmittedContent,
|
||||
});
|
||||
|
||||
export const conflictError = () => ({
|
||||
headingMessage: messages.errorSubmittingGradeHeading,
|
||||
contentMessage: messages.errorSubmittingGradeContent,
|
||||
});
|
||||
|
||||
export const defaultError = module.badRequestError;
|
||||
|
||||
export const errorProps = ({
|
||||
dispatch,
|
||||
errorStatus,
|
||||
}) => {
|
||||
const errors = {
|
||||
[ErrorStatuses.badRequest]: module.badRequestError({ dispatch }),
|
||||
[ErrorStatuses.conflict]: module.conflictError({ dispatch }),
|
||||
default: module.defaultError({ dispatch }),
|
||||
};
|
||||
// TODO: Network-Log an error here for unhandled error type
|
||||
// if (errors[errorStatus] === undefined) { }
|
||||
return errors[errorStatus] || errors.default;
|
||||
};
|
||||
|
||||
export const errorStatusSelector = (state) => selectors.requests.errorStatus(state, { requestKey });
|
||||
|
||||
export const rendererHooks = ({
|
||||
dispatch,
|
||||
intl,
|
||||
}) => {
|
||||
const errorStatus = useSelector(module.errorStatusSelector);
|
||||
|
||||
if (!errorStatus) {
|
||||
return { show: false };
|
||||
}
|
||||
|
||||
const error = module.errorProps({ dispatch, errorStatus });
|
||||
|
||||
return {
|
||||
show: true,
|
||||
reviewActions: {
|
||||
cancel: {
|
||||
onClick: () => dispatch(actions.requests.clearRequest({ requestKey })),
|
||||
message: messages.dismiss,
|
||||
},
|
||||
confirm: error.confirm,
|
||||
},
|
||||
headingMessage: error.headingMessage,
|
||||
content: intl.formatMessage(error.contentMessage),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { actions, selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys, ErrorStatuses } from 'data/constants/requests';
|
||||
import messages from './messages';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
errorStatus: (...args) => ({ errorStatus: args }),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
requests: {
|
||||
clearRequest: (args) => ({ clearRequest: args }),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
submitGrade: jest.fn((args) => ({ submitGrade: args })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const hookKeys = keyStore(hooks);
|
||||
const dispatch = useDispatch();
|
||||
const intl = { formatMessage };
|
||||
const testState = { my: 'test-state' };
|
||||
const requestKey = RequestKeys.submitGrade;
|
||||
let errorStatus;
|
||||
let hook;
|
||||
|
||||
describe('Review Modal Submit Error hooks', () => {
|
||||
beforeEach(jest.clearAllMocks);
|
||||
describe('badRequestError', () => {
|
||||
beforeEach(() => { hook = hooks.badRequestError({ dispatch }); });
|
||||
it('returns messages from gradeNotSubmitted error messages', () => {
|
||||
expect(hook.headingMessage).toEqual(messages.gradeNotSubmittedHeading);
|
||||
expect(hook.contentMessage).toEqual(messages.gradeNotSubmittedContent);
|
||||
});
|
||||
test('onClick, dispatches thunkAction to submit grade', () => {
|
||||
hook.confirm.onClick();
|
||||
expect(dispatch).toHaveBeenCalledWith(thunkActions.grading.submitGrade());
|
||||
});
|
||||
it('provides a confirm resubmitGrade message', () => {
|
||||
expect(hook.confirm.message).toEqual(messages.resubmitGrade);
|
||||
});
|
||||
});
|
||||
describe('conflictError', () => {
|
||||
beforeEach(() => { hook = hooks.conflictError({ dispatch }); });
|
||||
it('returns messages from errorSubmittingGrade error messages', () => {
|
||||
expect(hook.headingMessage).toEqual(messages.errorSubmittingGradeHeading);
|
||||
expect(hook.contentMessage).toEqual(messages.errorSubmittingGradeContent);
|
||||
});
|
||||
it('does not provide an onClick event', () => {
|
||||
expect(hook.onClick).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
test('defaultError returns badRequestError', () => {
|
||||
expect(hooks.defaultError).toEqual(hooks.badRequestError);
|
||||
});
|
||||
describe('errorProps', () => {
|
||||
const mockedError = (args) => ({ mockedError: args });
|
||||
const mockError = (hookKey) => {
|
||||
jest.spyOn(hooks, hookKey).mockImplementationOnce(mockedError);
|
||||
};
|
||||
test('on bad request, returns badRequestError', () => {
|
||||
mockError(hookKeys.badRequestError);
|
||||
expect(
|
||||
hooks.errorProps({ dispatch, errorStatus: ErrorStatuses.badRequest }),
|
||||
).toEqual(mockedError({ dispatch }));
|
||||
});
|
||||
test('on conflict, returns conflictError', () => {
|
||||
mockError(hookKeys.conflictError);
|
||||
expect(
|
||||
hooks.errorProps({ dispatch, errorStatus: ErrorStatuses.conflict }),
|
||||
).toEqual(mockedError({ dispatch }));
|
||||
});
|
||||
test('on unhandled error type, returns defaultError', () => {
|
||||
mockError(hookKeys.defaultError);
|
||||
expect(
|
||||
hooks.errorProps({ dispatch, errorStatus: 'fake-status' }),
|
||||
).toEqual(mockedError({ dispatch }));
|
||||
});
|
||||
});
|
||||
describe('errorStatusSelector', () => {
|
||||
it('returns the errorStatus of the submitGrade request', () => {
|
||||
expect(hooks.errorStatusSelector(testState)).toEqual(
|
||||
selectors.requests.errorStatus(testState, { requestKey }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('rendererHooks', () => {
|
||||
it('calls useSelector once on errorStatusSelector', () => {
|
||||
hooks.rendererHooks({ dispatch, intl });
|
||||
expect(useSelector.mock.calls).toEqual([[hooks.errorStatusSelector]]);
|
||||
});
|
||||
it('returns only a false show value if errorStatus is empty', () => {
|
||||
useSelector.mockReturnValueOnce(false);
|
||||
expect(hooks.rendererHooks({ dispatch, intl })).toEqual({ show: false });
|
||||
});
|
||||
describe('with valid error status', () => {
|
||||
errorStatus = 'test-status';
|
||||
const mockErrorProps = (args) => ({
|
||||
confirm: { confirm: args },
|
||||
headingMessag: { headingMessage: args },
|
||||
contentMessage: { contentMessage: args },
|
||||
});
|
||||
const mockProps = mockErrorProps({ dispatch, errorStatus });
|
||||
beforeEach(() => {
|
||||
useSelector.mockReturnValueOnce(errorStatus);
|
||||
jest.spyOn(hooks, hookKeys.errorProps).mockImplementationOnce(mockErrorProps);
|
||||
hook = hooks.rendererHooks({ dispatch, intl });
|
||||
});
|
||||
describe('reviewActions', () => {
|
||||
describe('cancel', () => {
|
||||
test('onClick, dispatches action to clear submit grade action', () => {
|
||||
hook.reviewActions.cancel.onClick();
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.requests.clearRequest({ requestKey }));
|
||||
});
|
||||
test('provides dismiss message', () => {
|
||||
expect(hook.reviewActions.cancel.message).toEqual(messages.dismiss);
|
||||
});
|
||||
});
|
||||
test('confirm forwards confirm action from errorProps', () => {
|
||||
expect(hook.reviewActions.confirm).toEqual(mockProps.confirm);
|
||||
});
|
||||
});
|
||||
test('loads headingMessage from errorProps', () => {
|
||||
expect(hook.headingMessage).toEqual(mockProps.headingMessage);
|
||||
});
|
||||
test('formats contentMessage from errorProps', () => {
|
||||
expect(hook.content).toEqual(formatMessage(mockProps.contentMessage));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { rendererHooks } from './hooks';
|
||||
|
||||
import ReviewError from '../ReviewError';
|
||||
|
||||
/**
|
||||
* <SubmitErrors />
|
||||
*/
|
||||
export const SubmitErrors = ({ intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
show,
|
||||
reviewActions,
|
||||
headingMessage,
|
||||
content,
|
||||
} = rendererHooks({ dispatch, intl });
|
||||
if (!show) { return null; }
|
||||
return (
|
||||
<ReviewError
|
||||
actions={reviewActions}
|
||||
headingMessage={headingMessage}
|
||||
>
|
||||
{content}
|
||||
</ReviewError>
|
||||
);
|
||||
};
|
||||
|
||||
SubmitErrors.propTypes = {
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SubmitErrors);
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import { SubmitErrors } from '.';
|
||||
|
||||
jest.mock('../ReviewError', () => 'ReviewError');
|
||||
|
||||
const hookKeys = keyStore(hooks);
|
||||
describe('SubmitErrors component', () => {
|
||||
const props = { intl: { formatMessage } };
|
||||
describe('snapshots', () => {
|
||||
test('snapshot: no failure', () => {
|
||||
jest.spyOn(hooks, hookKeys.rendererHooks).mockReturnValueOnce({ show: false });
|
||||
const el = shallow(<SubmitErrors {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot: with valid error, loads from hook', () => {
|
||||
const mockHook = {
|
||||
show: true,
|
||||
reviewActions: {
|
||||
confirm: 'hooks.reviewActions.confirm',
|
||||
cancel: 'hooks.reviewActions.cancel',
|
||||
},
|
||||
headingMessage: 'hooks.headingMessage',
|
||||
content: 'hooks.content',
|
||||
};
|
||||
jest.spyOn(hooks, hookKeys.rendererHooks).mockReturnValueOnce(mockHook);
|
||||
expect(shallow(<SubmitErrors {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/* eslint-disable quotes */
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
const messages = defineMessages({
|
||||
gradeNotSubmittedHeading: {
|
||||
id: 'ora-grading.ReviewModal.gradeNotSubmitted.heading',
|
||||
defaultMessage: 'Grade not submitted',
|
||||
description: 'Grade submission network error heading',
|
||||
},
|
||||
gradeNotSubmittedContent: {
|
||||
id: 'ora-grading.ReviewModal.gradeNotSubmitted.Content',
|
||||
defaultMessage: "We're sorry, something went wrong when we tried to submit this grade. Please try again.",
|
||||
description: 'Grade submission network error message',
|
||||
},
|
||||
resubmitGrade: {
|
||||
id: 'ora-grading.ReviewModal.resubmitGrade',
|
||||
defaultMessage: 'Resubmit grate',
|
||||
description: 'Resubmit grade button after network failure',
|
||||
},
|
||||
dismiss: {
|
||||
id: 'ora-grading.ReviewModal.dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
description: 'Dismiss error action button text',
|
||||
},
|
||||
errorSubmittingGradeHeading: {
|
||||
id: 'ora-grading.ReviewModal.errorSubmittingGrade.Heading',
|
||||
defaultMessage: 'Error submitting grade',
|
||||
description: 'Error Submitting Grade heading text',
|
||||
},
|
||||
errorSubmittingGradeContent: {
|
||||
id: 'ora-grading.ReviewModal.errorSubmittingGrade.Content',
|
||||
defaultMessage: 'It looks like someone else got here first! Your grade submission has been rejected',
|
||||
description: 'Error Submitting Grade content',
|
||||
},
|
||||
});
|
||||
|
||||
export default StrictDict(messages);
|
||||
@@ -24,16 +24,16 @@ exports[`LockErrors component component snapshots snapshot: error with conflicte
|
||||
<ReviewError
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "Invalid request. Please check your input.",
|
||||
"description": "Error lock request for missing params",
|
||||
"id": "ora-grading.ReviewModal.errorLockBadRequestHeading",
|
||||
"defaultMessage": "The lock owned by another user",
|
||||
"description": "Error lock by someone else",
|
||||
"id": "ora-grading.ReviewModal.errorLockContestedHeading",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Invalid request. Please check your input."
|
||||
description="Error lock request for missing params"
|
||||
id="ora-grading.ReviewModal.errorLockBadRequest"
|
||||
defaultMessage="The lock owned by another user"
|
||||
description="Error lock by someone else"
|
||||
id="ora-grading.ReviewModal.errorLockContested"
|
||||
/>
|
||||
</ReviewError>
|
||||
`;
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SubmitErrors component component snapshots snapshot: no failure 1`] = `null`;
|
||||
|
||||
exports[`SubmitErrors component component snapshots snapshot: with conflict failure 1`] = `
|
||||
<ReviewError
|
||||
actions={
|
||||
Object {
|
||||
"cancel": Object {
|
||||
"message": Object {
|
||||
"defaultMessage": "Dismiss",
|
||||
"description": "Dismiss error action button text",
|
||||
"id": "ora-grading.ReviewModal.dismiss",
|
||||
},
|
||||
"onClick": [MockFunction this.dismissError],
|
||||
},
|
||||
"confirm": undefined,
|
||||
}
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "Error submitting grade",
|
||||
"description": "Error Submitting Grade heading text",
|
||||
"id": "ora-grading.ReviewModal.errorSubmittingGrade.Heading",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="It looks like someone else got here first! Your grade submission has been rejected"
|
||||
description="Error Submitting Grade content"
|
||||
id="ora-grading.ReviewModal.errorSubmittingGrade.Content"
|
||||
/>
|
||||
</ReviewError>
|
||||
`;
|
||||
|
||||
exports[`SubmitErrors component component snapshots snapshot: with network failure 1`] = `
|
||||
<ReviewError
|
||||
actions={
|
||||
Object {
|
||||
"cancel": Object {
|
||||
"message": Object {
|
||||
"defaultMessage": "Dismiss",
|
||||
"description": "Dismiss error action button text",
|
||||
"id": "ora-grading.ReviewModal.dismiss",
|
||||
},
|
||||
"onClick": [MockFunction this.dismissError],
|
||||
},
|
||||
"confirm": Object {
|
||||
"message": Object {
|
||||
"defaultMessage": "Resubmit grate",
|
||||
"description": "Resubmit grade button after network failure",
|
||||
"id": "ora-grading.ReviewModal.resubmitGrade",
|
||||
},
|
||||
"onClick": [MockFunction],
|
||||
},
|
||||
}
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "Grade not submitted",
|
||||
"description": "Grade submission network error heading",
|
||||
"id": "ora-grading.ReviewModal.gradeNotSubmitted.heading",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="We're sorry, something went wrong when we tried to submit this grade. Please try again."
|
||||
description="Grade submission network error message"
|
||||
id="ora-grading.ReviewModal.gradeNotSubmitted.Content"
|
||||
/>
|
||||
</ReviewError>
|
||||
`;
|
||||
@@ -11,45 +11,11 @@ exports[`ReviewModal component component snapshots closed 1`] = `
|
||||
className="review-modal"
|
||||
isOpen={false}
|
||||
modalBodyClassName="review-modal-body"
|
||||
onClose={[MockFunction this.onClose]}
|
||||
onClose={[MockFunction hooks.onClose]}
|
||||
title="test-ora-name"
|
||||
>
|
||||
<LoadingMessage
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Loading response",
|
||||
"description": "loading text for submission response review screen",
|
||||
"id": "ora-grading.ReviewModal.loadingResponse",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<CloseReviewConfirmModal
|
||||
isOpen={false}
|
||||
onCancel={[MockFunction this.hideConfirmCloseReviewGrade]}
|
||||
onConfirm={[MockFunction this.confirmCloseReviewGrade]}
|
||||
/>
|
||||
</FullscreenModal>
|
||||
`;
|
||||
|
||||
exports[`ReviewModal component component snapshots error 1`] = `
|
||||
<FullscreenModal
|
||||
beforeBodyNode={
|
||||
<React.Fragment>
|
||||
<ReviewActions />
|
||||
<DemoWarning />
|
||||
</React.Fragment>
|
||||
}
|
||||
className="review-modal"
|
||||
isOpen={true}
|
||||
modalBodyClassName="review-modal-body"
|
||||
onClose={[MockFunction this.onClose]}
|
||||
title="test-ora-name"
|
||||
>
|
||||
<ReviewContent />
|
||||
<CloseReviewConfirmModal
|
||||
isOpen={false}
|
||||
onCancel={[MockFunction this.hideConfirmCloseReviewGrade]}
|
||||
onConfirm={[MockFunction this.confirmCloseReviewGrade]}
|
||||
prop="hooks.closeConfirmModalProps"
|
||||
/>
|
||||
</FullscreenModal>
|
||||
`;
|
||||
@@ -65,7 +31,7 @@ exports[`ReviewModal component component snapshots loading 1`] = `
|
||||
className="review-modal"
|
||||
isOpen={true}
|
||||
modalBodyClassName="review-modal-body"
|
||||
onClose={[MockFunction this.onClose]}
|
||||
onClose={[MockFunction hooks.onClose]}
|
||||
title="test-ora-name"
|
||||
>
|
||||
<ReviewContent />
|
||||
@@ -79,9 +45,7 @@ exports[`ReviewModal component component snapshots loading 1`] = `
|
||||
}
|
||||
/>
|
||||
<CloseReviewConfirmModal
|
||||
isOpen={false}
|
||||
onCancel={[MockFunction this.hideConfirmCloseReviewGrade]}
|
||||
onConfirm={[MockFunction this.confirmCloseReviewGrade]}
|
||||
prop="hooks.closeConfirmModalProps"
|
||||
/>
|
||||
</FullscreenModal>
|
||||
`;
|
||||
@@ -97,37 +61,12 @@ exports[`ReviewModal component component snapshots success 1`] = `
|
||||
className="review-modal"
|
||||
isOpen={true}
|
||||
modalBodyClassName="review-modal-body"
|
||||
onClose={[MockFunction this.onClose]}
|
||||
onClose={[MockFunction hooks.onClose]}
|
||||
title="test-ora-name"
|
||||
>
|
||||
<ReviewContent />
|
||||
<CloseReviewConfirmModal
|
||||
isOpen={false}
|
||||
onCancel={[MockFunction this.hideConfirmCloseReviewGrade]}
|
||||
onConfirm={[MockFunction this.confirmCloseReviewGrade]}
|
||||
/>
|
||||
</FullscreenModal>
|
||||
`;
|
||||
|
||||
exports[`ReviewModal component component snapshots success, demo (title message) 1`] = `
|
||||
<FullscreenModal
|
||||
beforeBodyNode={
|
||||
<React.Fragment>
|
||||
<ReviewActions />
|
||||
<DemoWarning />
|
||||
</React.Fragment>
|
||||
}
|
||||
className="review-modal"
|
||||
isOpen={true}
|
||||
modalBodyClassName="review-modal-body"
|
||||
onClose={[MockFunction this.onClose]}
|
||||
title="test-ora-name - Grading Demo"
|
||||
>
|
||||
<ReviewContent />
|
||||
<CloseReviewConfirmModal
|
||||
isOpen={false}
|
||||
onCancel={[MockFunction this.hideConfirmCloseReviewGrade]}
|
||||
onConfirm={[MockFunction this.confirmCloseReviewGrade]}
|
||||
prop="hooks.closeConfirmModalProps"
|
||||
/>
|
||||
</FullscreenModal>
|
||||
`;
|
||||
|
||||
66
src/containers/ReviewModal/hooks.js
Normal file
66
src/containers/ReviewModal/hooks.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import messages from './messages';
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
showConfirmCloseReviewGrade: (val) => React.useState(val),
|
||||
});
|
||||
|
||||
export const reduxValues = () => ({
|
||||
errorStatus: useSelector((val) => (
|
||||
selectors.requests.errorStatus(val, { requestKey: RequestKeys.fetchSubmission })
|
||||
)),
|
||||
hasGradingProgress: useSelector(selectors.grading.hasGradingProgress),
|
||||
isEnabled: useSelector(selectors.app.isEnabled),
|
||||
isLoaded: useSelector((val) => (
|
||||
selectors.requests.isCompleted(val, { requestKey: RequestKeys.fetchSubmission })
|
||||
)),
|
||||
isOpen: useSelector(selectors.app.showReview),
|
||||
oraName: useSelector(selectors.app.ora.name),
|
||||
});
|
||||
|
||||
export const rendererHooks = ({
|
||||
dispatch,
|
||||
intl: { formatMessage },
|
||||
}) => {
|
||||
const [show, setShow] = state.showConfirmCloseReviewGrade(false);
|
||||
|
||||
const {
|
||||
errorStatus,
|
||||
hasGradingProgress,
|
||||
isEnabled,
|
||||
isLoaded,
|
||||
isOpen,
|
||||
oraName,
|
||||
} = module.reduxValues();
|
||||
|
||||
const onClose = () => {
|
||||
if (hasGradingProgress) {
|
||||
setShow(true);
|
||||
} else {
|
||||
dispatch(thunkActions.app.cancelReview());
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onClose,
|
||||
isLoading: !(errorStatus || isLoaded),
|
||||
title: isEnabled
|
||||
? `${oraName} - ${formatMessage(messages.demoTitleMessage)}`
|
||||
: oraName,
|
||||
isOpen,
|
||||
closeConfirmModalProps: {
|
||||
isOpen: show,
|
||||
onCancel: () => setShow(false),
|
||||
onConfirm: () => {
|
||||
setShow(false);
|
||||
dispatch(thunkActions.app.cancelReview());
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user