Compare commits
24 Commits
jenkins/ve
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fa0c1ab66 | ||
|
|
559c335aa3 | ||
|
|
18a1e7da48 | ||
|
|
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=''
|
||||
|
||||
@@ -6,6 +6,7 @@ const config = createConfig('eslint', {
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
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
|
||||
|
||||
2
Makefile
2
Makefile
@@ -57,7 +57,7 @@ push_translations:
|
||||
|
||||
# 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',
|
||||
|
||||
13754
package-lock.json
generated
13754
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -27,10 +27,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 +52,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,7 +75,7 @@
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "^9.1.4",
|
||||
"@edx/frontend-build": "^11.0.2",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
@@ -85,10 +87,10 @@
|
||||
"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';
|
||||
|
||||
function 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>
|
||||
`;
|
||||
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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +50,7 @@ export class SubmissionsTable extends React.Component {
|
||||
}
|
||||
|
||||
formatDate = ({ value }) => {
|
||||
const date = new Date(value);
|
||||
const date = new Date(moment(value));
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
@@ -65,22 +62,9 @@ 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() {
|
||||
@@ -88,59 +72,56 @@ export class SubmissionsTable extends React.Component {
|
||||
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());
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
168
src/containers/ReviewModal/hooks.test.js
Normal file
168
src/containers/ReviewModal/hooks.test.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
import { MockUseState, formatMessage } from 'testUtils';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import messages from './messages';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
jest.useFakeTimers('modern');
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
isEnabled: (args) => ({ isEnabled: args }),
|
||||
ora: { name: (...args) => ({ oraName: args }) },
|
||||
showReview: (...args) => ({ showReview: args }),
|
||||
},
|
||||
grading: {
|
||||
hasGradingProgress: (args) => ({ hasGradingProgress: args }),
|
||||
selected: {
|
||||
response: (...args) => ({ selectedResponse: args }),
|
||||
},
|
||||
},
|
||||
requests: {
|
||||
isCompleted: (...args) => ({ isCompleted: args }),
|
||||
errorStatus: (...args) => ({ errorStatus: args }),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
app: {
|
||||
setShowReview: jest.fn(),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
app: {
|
||||
initialize: jest.fn(),
|
||||
cancelReview: jest.fn(),
|
||||
},
|
||||
grading: {
|
||||
cancelGrading: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const requestKey = RequestKeys.fetchSubmission;
|
||||
const hookKeys = keyStore(hooks);
|
||||
|
||||
const testState = { my: 'test-state' };
|
||||
const intl = { formatMessage };
|
||||
const dispatch = jest.fn();
|
||||
describe('ReviewModal hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.showConfirmCloseReviewGrade);
|
||||
});
|
||||
describe('redux values', () => {
|
||||
const values = hooks.reduxValues();
|
||||
const reduxKeys = keyStore(values);
|
||||
const testSelector = (key, selector) => {
|
||||
expect(values[key]).toEqual(useSelector(selector));
|
||||
};
|
||||
const testRequestSelector = (key, selector) => {
|
||||
expect(values[key].useSelector(testState)).toEqual(
|
||||
selector(testState, { requestKey }),
|
||||
);
|
||||
};
|
||||
test('errorStatus loads the error status of the fetchSubmission request', () => {
|
||||
testRequestSelector(reduxKeys.errorStatus, selectors.requests.errorStatus);
|
||||
});
|
||||
test('hasGradingProgress loads grading.hasGradingProgress', () => {
|
||||
testSelector(reduxKeys.hasGradingProgress, selectors.grading.hasGradingProgress);
|
||||
});
|
||||
test('isEnabled loads app.isEnabled', () => {
|
||||
testSelector(reduxKeys.isEnabled, selectors.app.isEnabled);
|
||||
});
|
||||
test('isLoaded loads if fetchSubmission is complete', () => {
|
||||
testRequestSelector(reduxKeys.isLoaded, selectors.requests.isCompleted);
|
||||
});
|
||||
test('isOpen loads app.showReview', () => {
|
||||
testSelector(reduxKeys.isOpen, selectors.app.showReview);
|
||||
});
|
||||
test('oraName loads app.ora.name', () => {
|
||||
testSelector(reduxKeys.oraName, selectors.app.ora.name);
|
||||
});
|
||||
test('hasGradingProgress loads grading.hasGradingProgress', () => {
|
||||
testSelector(reduxKeys.hasGradingProgress, selectors.grading.hasGradingProgress);
|
||||
});
|
||||
});
|
||||
describe('non-state hooks', () => {
|
||||
beforeEach(state.mock);
|
||||
afterEach(state.restore);
|
||||
const reduxValues = {
|
||||
errorStatus: null,
|
||||
hasGradingProgress: false,
|
||||
isEnabled: false,
|
||||
isLoaded: false,
|
||||
isOpen: false,
|
||||
oraName: 'ora-NAAAAMME',
|
||||
};
|
||||
const mockRedux = (newVals) => {
|
||||
jest.spyOn(hooks, hookKeys.reduxValues).mockReturnValueOnce({
|
||||
...reduxValues,
|
||||
...newVals,
|
||||
});
|
||||
};
|
||||
const loadHook = (newVals) => {
|
||||
mockRedux(newVals);
|
||||
return hooks.rendererHooks({ dispatch, intl });
|
||||
};
|
||||
describe('rendererHooks - returned object:', () => {
|
||||
let hook;
|
||||
describe('onClose', () => {
|
||||
it('sets showConfirmCloseReviewGrade to true if hasGradingProgress', () => {
|
||||
hook = loadHook({ hasGradingProgress: true });
|
||||
hook.onClose();
|
||||
expect(state.setState.showConfirmCloseReviewGrade).toHaveBeenCalledWith(true);
|
||||
});
|
||||
it('cancels review if there is no grading progress', () => {
|
||||
hook = loadHook({});
|
||||
hook.onClose();
|
||||
expect(dispatch).toHaveBeenCalledWith(thunkActions.app.cancelReview());
|
||||
});
|
||||
});
|
||||
test('isLoading returns true iff is not loaded, or if there is an error status', () => {
|
||||
hook = loadHook({});
|
||||
expect(hook.isLoading).toEqual(true);
|
||||
hook = loadHook({ errorStatus: 'some-status' });
|
||||
expect(hook.isLoading).toEqual(false);
|
||||
hook = loadHook({ isLoaded: true });
|
||||
expect(hook.isLoading).toEqual(false);
|
||||
});
|
||||
test('title is ora name, with appended demo title message if isEnabled', () => {
|
||||
hook = loadHook({});
|
||||
expect(hook.title).toEqual(reduxValues.oraName);
|
||||
hook = loadHook({ isEnabled: true });
|
||||
expect(hook.title).toEqual(
|
||||
[reduxValues.oraName, formatMessage(messages.demoTitleMessage)].join(' - '),
|
||||
);
|
||||
});
|
||||
test('isOpen is loaded from redux value, defaulted to false', () => {
|
||||
expect(loadHook({}).isOpen).toEqual(false);
|
||||
expect(loadHook({ isOpen: true }).isOpen).toEqual(true);
|
||||
});
|
||||
describe('closeConfirmModalProps', () => {
|
||||
test('loads isOpen from showConfirmCloseReviewGrade state', () => {
|
||||
expect(loadHook({}).closeConfirmModalProps.isOpen).toEqual(false);
|
||||
expect(state.stateVals.showConfirmCloseReviewGrade).toEqual(false);
|
||||
state.mockVal(state.keys.showConfirmCloseReviewGrade, true);
|
||||
expect(loadHook({}).closeConfirmModalProps.isOpen).toEqual(true);
|
||||
});
|
||||
test('onCancel - sets showConfirmCloseReviewGrade to false', () => {
|
||||
loadHook({}).closeConfirmModalProps.onCancel();
|
||||
expect(state.setState.showConfirmCloseReviewGrade).toHaveBeenCalledWith(false);
|
||||
});
|
||||
test('onConfirm - sets showConfirmCloseReviewGrade to false and cancels review', () => {
|
||||
loadHook({}).closeConfirmModalProps.onConfirm();
|
||||
expect(state.setState.showConfirmCloseReviewGrade).toHaveBeenCalledWith(false);
|
||||
expect(dispatch).toHaveBeenCalledWith(thunkActions.app.cancelReview());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { FullscreenModal } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import LoadingMessage from 'components/LoadingMessage';
|
||||
import DemoWarning from 'containers/DemoWarning';
|
||||
import ReviewActions from 'containers/ReviewActions';
|
||||
@@ -15,121 +11,46 @@ import ReviewContent from './ReviewContent';
|
||||
import CloseReviewConfirmModal from './components/CloseReviewConfirmModal';
|
||||
import messages from './messages';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
import './ReviewModal.scss';
|
||||
|
||||
/**
|
||||
* <ReviewModal />
|
||||
*/
|
||||
export class ReviewModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { showConfirmCloseReviewGrade: false };
|
||||
|
||||
this.closeModal = this.closeModal.bind(this);
|
||||
this.confirmCloseReviewGrade = this.confirmCloseReviewGrade.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.hideConfirmCloseReviewGrade = this.hideConfirmCloseReviewGrade.bind(this);
|
||||
this.showConfirmCloseReviewGrade = this.showConfirmCloseReviewGrade.bind(this);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.props.hasGradingProgress) {
|
||||
this.showConfirmCloseReviewGrade();
|
||||
} else {
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
get isLoading() {
|
||||
return !(this.props.errorStatus || this.props.isLoaded);
|
||||
}
|
||||
|
||||
get title() {
|
||||
let title = this.props.oraName;
|
||||
if (!this.props.isEnabled) {
|
||||
title = `${title} - ${this.props.intl.formatMessage(messages.demoTitleMessage)}`;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.props.cancelReview();
|
||||
}
|
||||
|
||||
showConfirmCloseReviewGrade() {
|
||||
this.setState({ showConfirmCloseReviewGrade: true });
|
||||
}
|
||||
|
||||
hideConfirmCloseReviewGrade() {
|
||||
this.setState({ showConfirmCloseReviewGrade: false });
|
||||
}
|
||||
|
||||
confirmCloseReviewGrade() {
|
||||
this.hideConfirmCloseReviewGrade();
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isOpen, isLoaded, errorStatus } = this.props;
|
||||
return (
|
||||
<FullscreenModal
|
||||
title={this.title}
|
||||
isOpen={isOpen}
|
||||
beforeBodyNode={(
|
||||
<>
|
||||
<ReviewActions />
|
||||
<DemoWarning />
|
||||
</>
|
||||
)}
|
||||
onClose={this.onClose}
|
||||
className="review-modal"
|
||||
modalBodyClassName="review-modal-body"
|
||||
>
|
||||
{isOpen && <ReviewContent />}
|
||||
{/* even if the modal is closed, in case we want to add transitions later */}
|
||||
{!(isLoaded || errorStatus) && <LoadingMessage message={messages.loadingResponse} />}
|
||||
<CloseReviewConfirmModal
|
||||
isOpen={this.state.showConfirmCloseReviewGrade}
|
||||
onCancel={this.hideConfirmCloseReviewGrade}
|
||||
onConfirm={this.confirmCloseReviewGrade}
|
||||
/>
|
||||
</FullscreenModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
ReviewModal.defaultProps = {
|
||||
errorStatus: null,
|
||||
response: null,
|
||||
export const ReviewModal = ({ intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
isLoading,
|
||||
title,
|
||||
onClose,
|
||||
isOpen,
|
||||
closeConfirmModalProps,
|
||||
} = hooks.rendererHooks({ dispatch, intl });
|
||||
return (
|
||||
<FullscreenModal
|
||||
title={title}
|
||||
isOpen={isOpen}
|
||||
beforeBodyNode={(
|
||||
<>
|
||||
<ReviewActions />
|
||||
<DemoWarning />
|
||||
</>
|
||||
)}
|
||||
onClose={onClose}
|
||||
className="review-modal"
|
||||
modalBodyClassName="review-modal-body"
|
||||
>
|
||||
{isOpen && <ReviewContent />}
|
||||
{/* even if the modal is closed, in case we want to add transitions later */}
|
||||
{isLoading && <LoadingMessage message={messages.loadingResponse} />}
|
||||
<CloseReviewConfirmModal {...closeConfirmModalProps} />
|
||||
</FullscreenModal>
|
||||
);
|
||||
};
|
||||
ReviewModal.propTypes = {
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
cancelReview: PropTypes.func.isRequired,
|
||||
errorStatus: PropTypes.number,
|
||||
hasGradingProgress: PropTypes.bool.isRequired,
|
||||
isEnabled: PropTypes.bool.isRequired,
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
oraName: PropTypes.string.isRequired,
|
||||
response: PropTypes.shape({
|
||||
text: PropTypes.node,
|
||||
}),
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
errorStatus: selectors.requests.errorStatus(state, { requestKey: RequestKeys.fetchSubmission }),
|
||||
hasGradingProgress: selectors.grading.hasGradingProgress(state),
|
||||
isEnabled: selectors.app.isEnabled(state),
|
||||
isLoaded: selectors.requests.isCompleted(state, { requestKey: RequestKeys.fetchSubmission }),
|
||||
isOpen: selectors.app.showReview(state),
|
||||
oraName: selectors.app.ora.name(state),
|
||||
response: selectors.grading.selected.response(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
cancelReview: thunkActions.app.cancelReview,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ReviewModal));
|
||||
export default injectIntl(ReviewModal);
|
||||
|
||||
@@ -1,184 +1,58 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import {
|
||||
ReviewModal,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
import * as hooks from './hooks';
|
||||
import { ReviewModal } from '.';
|
||||
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
isEnabled: (args) => ({ isEnabled: args }),
|
||||
ora: { name: (...args) => ({ oraName: args }) },
|
||||
showReview: (...args) => ({ showReview: args }),
|
||||
},
|
||||
grading: {
|
||||
hasGradingProgress: (args) => ({ hasGradingProgress: args }),
|
||||
selected: {
|
||||
response: (...args) => ({ selectedResponse: args }),
|
||||
},
|
||||
},
|
||||
requests: {
|
||||
isCompleted: (...args) => ({ isCompleted: args }),
|
||||
errorStatus: (...args) => ({ errorStatus: args }),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
app: {
|
||||
setShowReview: jest.fn(),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
app: {
|
||||
initialize: jest.fn(),
|
||||
},
|
||||
grading: {
|
||||
cancelGrading: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('components/LoadingMessage', () => 'LoadingMessage');
|
||||
jest.mock('containers/DemoWarning', () => 'DemoWarning');
|
||||
jest.mock('containers/ReviewActions', () => 'ReviewActions');
|
||||
jest.mock('./ReviewContent', () => 'ReviewContent');
|
||||
jest.mock('./components/CloseReviewConfirmModal', () => 'CloseReviewConfirmModal');
|
||||
|
||||
const requestKey = RequestKeys.fetchSubmission;
|
||||
jest.mock('./hooks', () => ({
|
||||
rendererHooks: jest.fn(),
|
||||
}));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
describe('ReviewModal component', () => {
|
||||
let el;
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
hasGradingProgress: false,
|
||||
errorStatus: null,
|
||||
isEnabled: true,
|
||||
isLoaded: false,
|
||||
const hookProps = {
|
||||
isLoading: false,
|
||||
title: 'test-ora-name',
|
||||
onClose: jest.fn().mockName('hooks.onClose'),
|
||||
isOpen: false,
|
||||
oraName: 'test-ora-name',
|
||||
response: { text: (<div>some text</div>) },
|
||||
showRubric: false,
|
||||
closeConfirmModalProps: {
|
||||
prop: 'hooks.closeConfirmModalProps',
|
||||
},
|
||||
};
|
||||
|
||||
const render = (newVals) => {
|
||||
hooks.rendererHooks.mockReturnValueOnce({ ...hookProps, ...newVals });
|
||||
return shallow(<ReviewModal intl={{ formatMessage }} />);
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.setShowReview = jest.fn();
|
||||
props.stopGrading = jest.fn();
|
||||
props.reloadSubmissions = jest.fn();
|
||||
props.cancelReview = jest.fn();
|
||||
});
|
||||
describe('component', () => {
|
||||
describe('snapshots', () => {
|
||||
let render;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ReviewModal {...props} />);
|
||||
el.instance().onClose = jest.fn().mockName('this.onClose');
|
||||
el.instance().closeModal = jest.fn().mockName('this.closeModal');
|
||||
el.instance().showConfirmCloseReviewGrade = jest.fn().mockName('this.showConfirmCloseReviewGrade');
|
||||
el.instance().hideConfirmCloseReviewGrade = jest.fn().mockName('this.hideConfirmCloseReviewGrade');
|
||||
el.instance().confirmCloseReviewGrade = jest.fn().mockName('this.confirmCloseReviewGrade');
|
||||
render = () => el.instance().render();
|
||||
});
|
||||
test('closed', () => {
|
||||
expect(render()).toMatchSnapshot();
|
||||
});
|
||||
test('loading', () => {
|
||||
el.setProps({ isOpen: true });
|
||||
expect(render()).toMatchSnapshot();
|
||||
});
|
||||
test('error', () => {
|
||||
el.setProps({ isOpen: true, errorStatus: 200 });
|
||||
expect(render()).toMatchSnapshot();
|
||||
expect(render({ isOpen: true, isLoading: true })).toMatchSnapshot();
|
||||
});
|
||||
test('success', () => {
|
||||
el.setProps({ isOpen: true, isLoaded: true });
|
||||
expect(render()).toMatchSnapshot();
|
||||
});
|
||||
test('success, demo (title message)', () => {
|
||||
const oldEnv = process.env;
|
||||
el.setProps({ isOpen: true, isLoaded: true, isEnabled: false });
|
||||
expect(render()).toMatchSnapshot();
|
||||
process.env = oldEnv;
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('onClose', () => {
|
||||
test('no grading progress - close modal', () => {
|
||||
el = shallow(<ReviewModal {...props} />);
|
||||
el.instance().closeModal = jest.fn();
|
||||
el.instance().onClose();
|
||||
expect(el.state().showConfirmCloseReviewGrade).toBe(false);
|
||||
expect(el.instance().closeModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('is grading', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<ReviewModal {...props} hasGradingProgress />);
|
||||
el.instance().closeModal = jest.fn();
|
||||
});
|
||||
|
||||
test('show modal', () => {
|
||||
el.instance().onClose();
|
||||
expect(el.state().showConfirmCloseReviewGrade).toBe(true);
|
||||
});
|
||||
|
||||
test('cancel closing then just close confirm do nothing else', () => {
|
||||
el.instance().onClose();
|
||||
el.instance().hideConfirmCloseReviewGrade();
|
||||
expect(el.state().showConfirmCloseReviewGrade).toBe(false);
|
||||
expect(el.instance().closeModal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('confirm closing then stop grading and close the modal', () => {
|
||||
el.instance().onClose();
|
||||
el.instance().confirmCloseReviewGrade();
|
||||
expect(el.state().showConfirmCloseReviewGrade).toBe(false);
|
||||
expect(el.instance().closeModal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
expect(render({ isOpen: true })).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 }));
|
||||
});
|
||||
test('hasGradingProgress loads from grading.hasGradingProgress', () => {
|
||||
expect(mapped.hasGradingProgress).toEqual(
|
||||
selectors.grading.hasGradingProgress(testState),
|
||||
);
|
||||
});
|
||||
test('oraName loads from app.ora.name', () => {
|
||||
expect(mapped.oraName).toEqual(selectors.app.ora.name(testState));
|
||||
});
|
||||
test('isOpen loads from app.showReview', () => {
|
||||
expect(mapped.isOpen).toEqual(selectors.app.showReview(testState));
|
||||
});
|
||||
test('response loads from grading.selected.response', () => {
|
||||
expect(mapped.response).toEqual(selectors.grading.selected.response(testState));
|
||||
});
|
||||
test('isEnabled loads from app.isEnabled', () => {
|
||||
expect(mapped.isEnabled).toEqual(selectors.app.isEnabled(testState));
|
||||
});
|
||||
test('isLoaded loads from requests.isCompleted(fetchSubmission)', () => {
|
||||
expect(mapped.isLoaded).toEqual(selectors.requests.isCompleted(testState, { requestKey }));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads cancelReview from thunkActions.app.cancelReview', () => {
|
||||
expect(mapDispatchToProps.cancelReview).toEqual(thunkActions.app.cancelReview);
|
||||
describe('behavior', () => {
|
||||
it('initializes renderer hook with dispatch and intl props', () => {
|
||||
render();
|
||||
expect(hooks.rendererHooks).toHaveBeenCalledWith({ dispatch, intl: { formatMessage } });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
|
||||
.grading-rubric-card {
|
||||
width: 320px;
|
||||
width: 320px !important;
|
||||
height: fit-content;
|
||||
max-height: 100%;
|
||||
margin-left: map-get($spacers, 3);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Rubric Container snapshot is grading 1`] = `
|
||||
<React.Fragment>
|
||||
exports[`Rubric Container shapshot: hide footer 1`] = `
|
||||
<Fragment>
|
||||
<Card
|
||||
className="grading-rubric-card"
|
||||
>
|
||||
<Card.Body
|
||||
<Card.Section
|
||||
className="grading-rubric-body"
|
||||
>
|
||||
<h3>
|
||||
@@ -14,20 +14,61 @@ exports[`Rubric Container snapshot is grading 1`] = `
|
||||
<hr
|
||||
className="m-2.5"
|
||||
/>
|
||||
// get this.criteria()
|
||||
<CriterionContainer
|
||||
key="1"
|
||||
prop="hook-criteria-props-1"
|
||||
/>
|
||||
<CriterionContainer
|
||||
key="2"
|
||||
prop="hook-criteria-props-2"
|
||||
/>
|
||||
<CriterionContainer
|
||||
key="3"
|
||||
prop="hook-criteria-props-3"
|
||||
/>
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Body>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
<DemoAlert
|
||||
prop="demo-alert-props"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`Rubric Container snapshot: show footer 1`] = `
|
||||
<Fragment>
|
||||
<Card
|
||||
className="grading-rubric-card"
|
||||
>
|
||||
<Card.Section
|
||||
className="grading-rubric-body"
|
||||
>
|
||||
<h3>
|
||||
Rubric
|
||||
</h3>
|
||||
<hr
|
||||
className="m-2.5"
|
||||
/>
|
||||
<CriterionContainer
|
||||
key="1"
|
||||
prop="hook-criteria-props-1"
|
||||
/>
|
||||
<CriterionContainer
|
||||
key="2"
|
||||
prop="hook-criteria-props-2"
|
||||
/>
|
||||
<CriterionContainer
|
||||
key="3"
|
||||
prop="hook-criteria-props-3"
|
||||
/>
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Section>
|
||||
<div
|
||||
className="grading-rubric-footer"
|
||||
>
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": "Grade Submitted",
|
||||
@@ -35,181 +76,12 @@ exports[`Rubric Container snapshot is grading 1`] = `
|
||||
"pending": "Submitting grade",
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.submitGradeHandler]}
|
||||
state="default"
|
||||
prop="hook-button-props"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<DemoAlert
|
||||
isOpen={false}
|
||||
onClose={[MockFunction this.hideDemoAlert]}
|
||||
prop="demo-alert-props"
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`Rubric Container snapshot is grading, lock is pending 1`] = `
|
||||
<React.Fragment>
|
||||
<Card
|
||||
className="grading-rubric-card"
|
||||
>
|
||||
<Card.Body
|
||||
className="grading-rubric-body"
|
||||
>
|
||||
<h3>
|
||||
Rubric
|
||||
</h3>
|
||||
<hr
|
||||
className="m-2.5"
|
||||
/>
|
||||
// get this.criteria()
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Body>
|
||||
<div
|
||||
className="grading-rubric-footer"
|
||||
>
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": "Grade Submitted",
|
||||
"default": "Submit grade",
|
||||
"pending": "Submitting grade",
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.submitGradeHandler]}
|
||||
state="pending"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<DemoAlert
|
||||
isOpen={false}
|
||||
onClose={[MockFunction this.hideDemoAlert]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`Rubric Container snapshot is grading, submit pending 1`] = `
|
||||
<React.Fragment>
|
||||
<Card
|
||||
className="grading-rubric-card"
|
||||
>
|
||||
<Card.Body
|
||||
className="grading-rubric-body"
|
||||
>
|
||||
<h3>
|
||||
Rubric
|
||||
</h3>
|
||||
<hr
|
||||
className="m-2.5"
|
||||
/>
|
||||
// get this.criteria()
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Body>
|
||||
<div
|
||||
className="grading-rubric-footer"
|
||||
>
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": "Grade Submitted",
|
||||
"default": "Submit grade",
|
||||
"pending": "Submitting grade",
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.submitGradeHandler]}
|
||||
state="pending"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<DemoAlert
|
||||
isOpen={false}
|
||||
onClose={[MockFunction this.hideDemoAlert]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`Rubric Container snapshot is not grading 1`] = `
|
||||
<React.Fragment>
|
||||
<Card
|
||||
className="grading-rubric-card"
|
||||
>
|
||||
<Card.Body
|
||||
className="grading-rubric-body"
|
||||
>
|
||||
<h3>
|
||||
Rubric
|
||||
</h3>
|
||||
<hr
|
||||
className="m-2.5"
|
||||
/>
|
||||
// get this.criteria()
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Body>
|
||||
</Card>
|
||||
<DemoAlert
|
||||
isOpen={false}
|
||||
onClose={[MockFunction this.hideDemoAlert]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`Rubric Container snapshot submit completed 1`] = `
|
||||
<React.Fragment>
|
||||
<Card
|
||||
className="grading-rubric-card"
|
||||
>
|
||||
<Card.Body
|
||||
className="grading-rubric-body"
|
||||
>
|
||||
<h3>
|
||||
Rubric
|
||||
</h3>
|
||||
<hr
|
||||
className="m-2.5"
|
||||
/>
|
||||
// get this.criteria()
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Body>
|
||||
<div
|
||||
className="grading-rubric-footer"
|
||||
>
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": "Grade Submitted",
|
||||
"default": "Submit grade",
|
||||
"pending": "Submitting grade",
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.submitGradeHandler]}
|
||||
state="complete"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<DemoAlert
|
||||
isOpen={false}
|
||||
onClose={[MockFunction this.hideDemoAlert]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
82
src/containers/Rubric/hooks.js
Normal file
82
src/containers/Rubric/hooks.js
Normal file
@@ -0,0 +1,82 @@
|
||||
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 * as module from './hooks';
|
||||
|
||||
export const ButtonStates = StrictDict({
|
||||
default: 'default',
|
||||
pending: 'pending',
|
||||
complete: 'complete',
|
||||
error: 'error',
|
||||
});
|
||||
|
||||
export const state = {
|
||||
showDemoAlert: (val) => React.useState(val),
|
||||
};
|
||||
|
||||
export const reduxValues = () => ({
|
||||
criteriaIndices: useSelector(selectors.app.rubric.criteriaIndices),
|
||||
gradeIsPending: useSelector(
|
||||
val => selectors.requests.isPending(val, { requestKey: RequestKeys.submitGrade }),
|
||||
),
|
||||
isCompleted: useSelector(
|
||||
val => selectors.requests.isCompleted(val, { requestKey: RequestKeys.submitGrade }),
|
||||
),
|
||||
isEnabled: useSelector(selectors.app.isEnabled),
|
||||
isGrading: useSelector(selectors.grading.selected.isGrading),
|
||||
lockIsPending: useSelector(
|
||||
val => selectors.requests.isPending(val, { requestKey: RequestKeys.setLock }),
|
||||
),
|
||||
});
|
||||
|
||||
export const rendererHooks = ({
|
||||
dispatch,
|
||||
}) => {
|
||||
const [showDemoAlert, setShowDemoAlert] = module.state.showDemoAlert(false);
|
||||
const {
|
||||
criteriaIndices,
|
||||
gradeIsPending,
|
||||
isCompleted,
|
||||
isEnabled,
|
||||
isGrading,
|
||||
lockIsPending,
|
||||
} = module.reduxValues();
|
||||
|
||||
const isPending = (gradeIsPending || lockIsPending);
|
||||
let submitButtonState;
|
||||
if (isCompleted) {
|
||||
submitButtonState = ButtonStates.complete;
|
||||
} else if (isPending) {
|
||||
submitButtonState = ButtonStates.pending;
|
||||
} else {
|
||||
submitButtonState = ButtonStates.default;
|
||||
}
|
||||
|
||||
const criteria = criteriaIndices.map((index) => ({
|
||||
isGrading,
|
||||
key: index,
|
||||
orderNum: index,
|
||||
}));
|
||||
|
||||
return {
|
||||
criteria,
|
||||
showFooter: isGrading || isCompleted,
|
||||
buttonProps: {
|
||||
onClick: () => (
|
||||
!isEnabled
|
||||
? setShowDemoAlert(true)
|
||||
: dispatch(thunkActions.grading.submitGrade())
|
||||
),
|
||||
state: submitButtonState,
|
||||
disabledStates: [ButtonStates.pending, ButtonStates.complete],
|
||||
},
|
||||
demoAlertProps: {
|
||||
isOpen: showDemoAlert,
|
||||
onClose: () => setShowDemoAlert(false),
|
||||
},
|
||||
};
|
||||
};
|
||||
138
src/containers/Rubric/hooks.test.js
Normal file
138
src/containers/Rubric/hooks.test.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
rubric: { criteriaIndices: (args) => ({ criteriaIndices: args }) },
|
||||
},
|
||||
grading: {
|
||||
selected: { isGrading: (args) => ({ isGrading: args }) },
|
||||
},
|
||||
requests: {
|
||||
isCompleted: (...args) => ({ isCompleted: args }),
|
||||
isPending: (...args) => ({ isPending: args }),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
submitGrade: (args) => ({ submitGrade: args }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
const hookKeys = keyStore(hooks);
|
||||
const testState = { my: 'test-state' };
|
||||
const dispatch = jest.fn();
|
||||
let hook;
|
||||
|
||||
describe('Rubric hooks', () => {
|
||||
beforeEach(jest.clearAllMocks);
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.showDemoAlert);
|
||||
});
|
||||
describe('non-state hooks', () => {
|
||||
beforeEach(state.mock);
|
||||
afterEach(state.restore);
|
||||
describe('redux values', () => {
|
||||
beforeEach(() => { hook = hooks.reduxValues(); });
|
||||
test('loads gradeIsPending from isPending requests selectror on submitGrade request', () => {
|
||||
expect(hook.gradeIsPending.useSelector(testState)).toEqual(
|
||||
selectors.requests.isPending(testState, { requestKey: RequestKeys.submitGrade }),
|
||||
);
|
||||
});
|
||||
test('loads criteriaIndices from rubric selector', () => {
|
||||
expect(hook.criteriaIndices).toEqual(useSelector(selectors.app.rubric.criteriaIndices));
|
||||
});
|
||||
test('loads isCompleted from requests selectror on submitGrade request', () => {
|
||||
expect(hook.isCompleted.useSelector(testState)).toEqual(
|
||||
selectors.requests.isCompleted(testState, { requestKey: RequestKeys.submitGrade }),
|
||||
);
|
||||
});
|
||||
test('loads isEnabled from app selector', () => {
|
||||
expect(hook.isEnabled).toEqual(useSelector(selectors.app.isEnabled));
|
||||
});
|
||||
test('loads isGrading from grading selector for selected submission', () => {
|
||||
expect(hook.isGrading).toEqual(useSelector(selectors.grading.selected.isGrading));
|
||||
});
|
||||
test('loads lockIsPending from isPending requests selectror on setLock request', () => {
|
||||
expect(hook.lockIsPending.useSelector(testState)).toEqual(
|
||||
selectors.requests.isPending(testState, { requestKey: RequestKeys.setLock }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('rendererHooks', () => {
|
||||
const reduxValues = {
|
||||
criteriaIndices: [0, 1, 2, 3, 4],
|
||||
isGrading: false,
|
||||
isCompleted: false,
|
||||
gradeIsPending: false,
|
||||
lockIsPending: false,
|
||||
isEnabled: true,
|
||||
};
|
||||
|
||||
const mockHook = (values) => {
|
||||
jest.spyOn(hooks, hookKeys.reduxValues).mockReturnValueOnce({
|
||||
...reduxValues,
|
||||
...values,
|
||||
});
|
||||
hook = hooks.rendererHooks({ dispatch });
|
||||
};
|
||||
describe('criteria', () => {
|
||||
it('maps criteria indices from redux to an object with isGrading value', () => {
|
||||
const testIsGrading = 'test-is-grading';
|
||||
mockHook({ isGrading: testIsGrading });
|
||||
expect(hook.criteria).toEqual(reduxValues.criteriaIndices.map(index => ({
|
||||
isGrading: testIsGrading,
|
||||
orderNum: index,
|
||||
key: index,
|
||||
})));
|
||||
});
|
||||
});
|
||||
it('shows footer is grading or completed', () => {
|
||||
mockHook({});
|
||||
expect(hook.showFooter).toEqual(false);
|
||||
mockHook({ isGrading: true });
|
||||
expect(hook.showFooter).toEqual(true);
|
||||
mockHook({ isCompleted: true });
|
||||
expect(hook.showFooter).toEqual(true);
|
||||
});
|
||||
describe('buttonProps', () => {
|
||||
describe('onClick', () => {
|
||||
it('shows demo alert if app is not enabled', () => {
|
||||
mockHook({ isEnabled: false });
|
||||
hook.buttonProps.onClick();
|
||||
expect(state.setState.showDemoAlert).toHaveBeenCalledWith(true);
|
||||
});
|
||||
it('submits grade if app is enabled', () => {
|
||||
mockHook({});
|
||||
hook.buttonProps.onClick();
|
||||
expect(dispatch).toHaveBeenCalledWith(thunkActions.grading.submitGrade());
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('demoAlertProps', () => {
|
||||
test('open linked to showDemoAlert state', () => {
|
||||
mockHook({});
|
||||
expect(hook.demoAlertProps.isOpen).toEqual(state.stateVals.showDemoAlert);
|
||||
expect(hook.demoAlertProps.isOpen).toEqual(false);
|
||||
state.mockVal(state.keys.showDemoAlert, true);
|
||||
mockHook({});
|
||||
expect(hook.demoAlertProps.isOpen).toEqual(true);
|
||||
});
|
||||
test('on close, hides demo alert', () => {
|
||||
mockHook({});
|
||||
hook.demoAlertProps.onClose();
|
||||
expect(state.setState.showDemoAlert).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,128 +1,61 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Card, StatefulButton } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import DemoAlert from 'components/DemoAlert';
|
||||
import CriterionContainer from 'containers/CriterionContainer';
|
||||
import RubricFeedback from './RubricFeedback';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
import './Rubric.scss';
|
||||
|
||||
const ButtonStates = StrictDict({
|
||||
default: 'default',
|
||||
pending: 'pending',
|
||||
complete: 'complete',
|
||||
error: 'error',
|
||||
});
|
||||
const { ButtonStates } = hooks;
|
||||
|
||||
/**
|
||||
* <Rubric />
|
||||
*/
|
||||
export class Rubric extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { showDemoAlert: false };
|
||||
this.submitGradeHandler = this.submitGradeHandler.bind(this);
|
||||
this.hideDemoAlert = this.hideDemoAlert.bind(this);
|
||||
}
|
||||
|
||||
get submitButtonState() {
|
||||
if (this.props.gradeIsPending || this.props.lockIsPending) {
|
||||
return ButtonStates.pending;
|
||||
}
|
||||
if (this.props.isCompleted) {
|
||||
return ButtonStates.complete;
|
||||
}
|
||||
return ButtonStates.default;
|
||||
}
|
||||
|
||||
get criteria() {
|
||||
return this.props.criteriaIndices.map((index) => (
|
||||
<CriterionContainer
|
||||
isGrading={this.props.isGrading}
|
||||
key={index}
|
||||
orderNum={index}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
submitGradeHandler() {
|
||||
if (process.env.REACT_APP_NOT_ENABLED) {
|
||||
this.setState({ showDemoAlert: true });
|
||||
} else {
|
||||
this.props.submitGrade();
|
||||
}
|
||||
}
|
||||
|
||||
hideDemoAlert() {
|
||||
this.setState({ showDemoAlert: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isGrading, intl: { formatMessage } } = this.props;
|
||||
return (
|
||||
<>
|
||||
<Card className="grading-rubric-card">
|
||||
<Card.Body className="grading-rubric-body">
|
||||
<h3>{formatMessage(messages.rubric)}</h3>
|
||||
<hr className="m-2.5" />
|
||||
{this.criteria}
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Body>
|
||||
{(isGrading || this.props.isCompleted) && (
|
||||
<div className="grading-rubric-footer">
|
||||
<StatefulButton
|
||||
onClick={this.submitGradeHandler}
|
||||
state={this.submitButtonState}
|
||||
disabledStates={[ButtonStates.pending, ButtonStates.complete]}
|
||||
labels={{
|
||||
[ButtonStates.default]: formatMessage(messages.submitGrade),
|
||||
[ButtonStates.pending]: formatMessage(messages.submittingGrade),
|
||||
[ButtonStates.complete]: formatMessage(messages.gradeSubmitted),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
<DemoAlert isOpen={this.state.showDemoAlert} onClose={this.hideDemoAlert} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
Rubric.defaultProps = {
|
||||
criteriaIndices: [],
|
||||
export const Rubric = ({ intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
criteria,
|
||||
showFooter,
|
||||
buttonProps,
|
||||
demoAlertProps,
|
||||
} = hooks.rendererHooks({ dispatch });
|
||||
return (
|
||||
<>
|
||||
<Card className="grading-rubric-card">
|
||||
<Card.Section className="grading-rubric-body">
|
||||
<h3>{intl.formatMessage(messages.rubric)}</h3>
|
||||
<hr className="m-2.5" />
|
||||
{criteria.map(props => <CriterionContainer {...props} />)}
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Section>
|
||||
{showFooter && (
|
||||
<div className="grading-rubric-footer">
|
||||
<StatefulButton
|
||||
{...buttonProps}
|
||||
labels={{
|
||||
[ButtonStates.default]: intl.formatMessage(messages.submitGrade),
|
||||
[ButtonStates.pending]: intl.formatMessage(messages.submittingGrade),
|
||||
[ButtonStates.complete]: intl.formatMessage(messages.gradeSubmitted),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
<DemoAlert {...demoAlertProps} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
Rubric.propTypes = {
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
isCompleted: PropTypes.bool.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
gradeIsPending: PropTypes.bool.isRequired,
|
||||
lockIsPending: PropTypes.bool.isRequired,
|
||||
criteriaIndices: PropTypes.arrayOf(PropTypes.number),
|
||||
submitGrade: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
isCompleted: selectors.requests.isCompleted(state, { requestKey: RequestKeys.submitGrade }),
|
||||
isGrading: selectors.grading.selected.isGrading(state),
|
||||
gradeIsPending: selectors.requests.isPending(state, { requestKey: RequestKeys.submitGrade }),
|
||||
lockIsPending: selectors.requests.isPending(state, { requestKey: RequestKeys.setLock }),
|
||||
criteriaIndices: selectors.app.rubric.criteriaIndices(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
submitGrade: thunkActions.grading.submitGrade,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Rubric));
|
||||
export default injectIntl(Rubric);
|
||||
|
||||
@@ -2,155 +2,38 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import CriterionContainer from 'containers/CriterionContainer';
|
||||
import { Rubric, mapStateToProps, mapDispatchToProps } from '.';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import { Rubric } from '.';
|
||||
|
||||
jest.mock('containers/CriterionContainer', () => 'CriterionContainer');
|
||||
jest.mock('./RubricFeedback', () => 'RubricFeedback');
|
||||
jest.mock('components/DemoAlert', () => 'DemoAlert');
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
rubric: {
|
||||
criteriaIndices: jest.fn((...args) => ({
|
||||
rubricCriteriaIndices: args,
|
||||
})),
|
||||
},
|
||||
},
|
||||
grading: {
|
||||
selected: {
|
||||
isGrading: jest.fn((...args) => ({ isGrading: args })),
|
||||
},
|
||||
},
|
||||
requests: {
|
||||
isCompleted: jest.fn((...args) => ({ isCompleted: args })),
|
||||
isPending: jest.fn((...args) => ({ isPending: args })),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
submitGrade: jest.fn(),
|
||||
},
|
||||
},
|
||||
jest.mock('./hooks', () => ({
|
||||
rendererHooks: jest.fn(),
|
||||
ButtonStates: jest.requireActual('./hooks').ButtonStates,
|
||||
}));
|
||||
|
||||
describe('Rubric Container', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
isCompleted: false,
|
||||
gradeIsPending: false,
|
||||
lockIsPending: false,
|
||||
isGrading: true,
|
||||
criteriaIndices: [1, 2, 3, 4, 5],
|
||||
submitGrade: jest.fn().mockName('this.props.submitGrade'),
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<Rubric {...props} />);
|
||||
el.instance().submitGradeHandler = jest
|
||||
.fn()
|
||||
.mockName('this.submitGradeHandler');
|
||||
const hookProps = {
|
||||
criteria: [
|
||||
{ prop: 'hook-criteria-props-1', key: 1 },
|
||||
{ prop: 'hook-criteria-props-2', key: 2 },
|
||||
{ prop: 'hook-criteria-props-3', key: 3 },
|
||||
],
|
||||
showFooter: false,
|
||||
buttonProps: { prop: 'hook-button-props' },
|
||||
demoAlertProps: { prop: 'demo-alert-props' },
|
||||
};
|
||||
test('snapshot: show footer', () => {
|
||||
hooks.rendererHooks.mockReturnValueOnce({ ...hookProps, showFooter: true });
|
||||
expect(shallow(<Rubric {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
beforeEach(() => {
|
||||
el.instance().submitGradeHandler = jest.fn().mockName('this.submitGradeHandler');
|
||||
el.instance().hideDemoAlert = jest.fn().mockName('this.hideDemoAlert');
|
||||
jest.spyOn(el.instance(), 'criteria', 'get').mockReturnValue('// get this.criteria()');
|
||||
});
|
||||
test('is grading', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('is not grading', () => {
|
||||
el.setProps({ isGrading: false });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('is grading, submit pending', () => {
|
||||
el.setProps({ gradeIsPending: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('is grading, lock is pending', () => {
|
||||
el.setProps({ lockIsPending: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('submit completed', () => {
|
||||
el.setProps({ isCompleted: true, isGrading: false });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('render', () => {
|
||||
test('criteria getter returns a CriterionContainer for each criteriaIndices value', () => {
|
||||
const container = (value) => (
|
||||
shallow(<CriterionContainer isGrading key={value} orderNum={value} />)
|
||||
);
|
||||
expect(el.instance().criteria).toMatchObject(props.criteriaIndices.map(container));
|
||||
});
|
||||
test('is grading (grading footer present)', () => {
|
||||
expect(el.find('.grading-rubric-footer').length).toEqual(1);
|
||||
const containers = el.find('CriterionContainer');
|
||||
expect(containers.length).toEqual(props.criteriaIndices.length);
|
||||
containers.forEach((container, i) => {
|
||||
expect(container.key()).toEqual(String(props.criteriaIndices[i]));
|
||||
});
|
||||
});
|
||||
|
||||
test('is not grading (no grading footer)', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
});
|
||||
expect(el.find('.grading-rubric-footer').length).toEqual(0);
|
||||
const containers = el.find('CriterionContainer');
|
||||
expect(containers.length).toEqual(props.criteriaIndices.length);
|
||||
containers.forEach((container, i) => {
|
||||
expect(container.key()).toEqual(String(props.criteriaIndices[i]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('submitGrade', () => {
|
||||
el = shallow(<Rubric {...props} />);
|
||||
el.instance().submitGradeHandler();
|
||||
expect(props.submitGrade).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { abitaryState: 'some data' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('isGrading from selectors.grading.selected.isGrading', () => {
|
||||
expect(mapped.isGrading).toEqual(selectors.grading.selected.isGrading(testState));
|
||||
});
|
||||
test('gradeIsPending from selectors.requests.isPending(submitGrade)', () => {
|
||||
expect(mapped.gradeIsPending).toEqual(
|
||||
selectors.requests.isPending(testState, { requestKey: RequestKeys.submitGrade }),
|
||||
);
|
||||
});
|
||||
test('lockIsPending from selectors.requests.isPending(setLock)', () => {
|
||||
expect(mapped.lockIsPending).toEqual(selectors.requests.isPending(
|
||||
testState, { requestKey: RequestKeys.setLock },
|
||||
));
|
||||
});
|
||||
test('criteriaIndices from selectors.app.rubric.criteriaIndices', () => {
|
||||
expect(mapped.criteriaIndices).toEqual(
|
||||
selectors.app.rubric.criteriaIndices(testState),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
beforeEach(() => {});
|
||||
test('maps thunkActions.grading.submitGrade to submitGrade prop', () => {
|
||||
expect(mapDispatchToProps.submitGrade).toEqual(
|
||||
thunkActions.grading.submitGrade,
|
||||
);
|
||||
});
|
||||
test('shapshot: hide footer', () => {
|
||||
hooks.rendererHooks.mockReturnValueOnce(hookProps);
|
||||
expect(shallow(<Rubric {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const routePath = `${getConfig().PUBLIC_PATH}:courseId`;
|
||||
export const locationId = window.location.pathname.slice(1);
|
||||
export const locationId = window.location.pathname.replace(getConfig().PUBLIC_PATH, '');
|
||||
|
||||
24
src/data/constants/app.test.js
Normal file
24
src/data/constants/app.test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as platform from '@edx/frontend-platform';
|
||||
import * as constants from './app';
|
||||
|
||||
jest.unmock('./app');
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => {
|
||||
const PUBLIC_PATH = '/test-public-path/';
|
||||
return {
|
||||
getConfig: () => ({ PUBLIC_PATH }),
|
||||
PUBLIC_PATH,
|
||||
};
|
||||
});
|
||||
|
||||
describe('app constants', () => {
|
||||
test('route path draws from public path and adds courseId', () => {
|
||||
expect(constants.routePath).toEqual(`${platform.PUBLIC_PATH}:courseId`);
|
||||
});
|
||||
test('locationId returns trimmed pathname', () => {
|
||||
const old = window.location;
|
||||
window.location = { pathName: `${platform.PUBLIC_PATH}somePath.jpg` };
|
||||
expect(constants.locationId).toEqual(window.location.pathname.replace(platform.PUBLIC_PATH, ''));
|
||||
window.location = old;
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user