feat: add file renderer that support common error handling and loading banner (#42)
* feat: add file renderer that support common error handling and loading banner * chore: use axios instead of get from utils * chore: fixed typo and update snapshots
This commit is contained in:
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1636,6 +1636,14 @@
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
@@ -5053,11 +5061,11 @@
|
||||
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"axios-cache-adapter": {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.21.4",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.16.2",
|
||||
"dompurify": "^2.3.1",
|
||||
@@ -75,7 +76,6 @@
|
||||
"@edx/frontend-build": "8.0.4",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"axios": "0.21.1",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"codecov": "^3.8.3",
|
||||
"enzyme-adapter-react-16": "^1.15.6",
|
||||
|
||||
44
src/components/FilePreview/Banners/ErrorBanner.jsx
Normal file
44
src/components/FilePreview/Banners/ErrorBanner.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert, Button } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messageShape = PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
defaultMessage: PropTypes.string,
|
||||
});
|
||||
|
||||
export const ErrorBanner = ({ actions, headingMessage, children }) => {
|
||||
const actionButtons = actions.map(action => (
|
||||
<Button key={action.id} onClick={action.onClick} variant="outline-primary">
|
||||
<FormattedMessage {...action.message} />
|
||||
</Button>
|
||||
));
|
||||
return (
|
||||
<Alert variant="danger" icon={Info} actions={actionButtons}>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage {...headingMessage} />
|
||||
</Alert.Heading>
|
||||
{children}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
ErrorBanner.defaultProps = {
|
||||
actions: [],
|
||||
children: null,
|
||||
};
|
||||
ErrorBanner.propTypes = {
|
||||
actions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
message: messageShape,
|
||||
}),
|
||||
),
|
||||
headingMessage: messageShape.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ErrorBanner;
|
||||
59
src/components/FilePreview/Banners/ErrorBanner.test.jsx
Normal file
59
src/components/FilePreview/Banners/ErrorBanner.test.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import ErrorBanner from './ErrorBanner';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
describe('Error Banner component', () => {
|
||||
const children = <p>Abitary Child</p>;
|
||||
|
||||
const props = {
|
||||
actions: [
|
||||
{
|
||||
id: 'action1',
|
||||
onClick: jest.fn().mockName('action1.onClick'),
|
||||
message: messages.retryButton,
|
||||
},
|
||||
{
|
||||
id: 'action2',
|
||||
onClick: jest.fn().mockName('action2.onClick'),
|
||||
message: messages.retryButton,
|
||||
},
|
||||
],
|
||||
headingMessage: messages.unknownError,
|
||||
children,
|
||||
};
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ErrorBanner {...props} />);
|
||||
});
|
||||
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('children node', () => {
|
||||
expect(el.containsMatchingElement(children)).toEqual(true);
|
||||
});
|
||||
|
||||
test('verify actions', () => {
|
||||
const actions = el.find('Alert').prop('actions');
|
||||
expect(actions).toHaveLength(props.actions.length);
|
||||
|
||||
actions.forEach((action, index) => {
|
||||
expect(action.type).toEqual('Button');
|
||||
expect(action.props.onClick).toEqual(props.actions[index].onClick);
|
||||
// action message
|
||||
expect(action.props.children.props).toEqual(props.actions[index].message);
|
||||
});
|
||||
});
|
||||
|
||||
test('verify heading', () => {
|
||||
const heading = el.find('FormattedMessage');
|
||||
expect(heading.props()).toEqual(props.headingMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
14
src/components/FilePreview/Banners/LoadingBanner.jsx
Normal file
14
src/components/FilePreview/Banners/LoadingBanner.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Alert, Spinner } from '@edx/paragon';
|
||||
|
||||
export const LoadingBanner = () => (
|
||||
<Alert variant="info">
|
||||
<Spinner animation="border" className="d-flex m-auto" />
|
||||
</Alert>
|
||||
);
|
||||
|
||||
LoadingBanner.defaultProps = {};
|
||||
LoadingBanner.propTypes = {};
|
||||
|
||||
export default LoadingBanner;
|
||||
11
src/components/FilePreview/Banners/LoadingBanner.test.jsx
Normal file
11
src/components/FilePreview/Banners/LoadingBanner.test.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import LoadingBanner from './LoadingBanner';
|
||||
|
||||
describe('Loading Banner component', () => {
|
||||
test('snapshot', () => {
|
||||
const el = shallow(<LoadingBanner />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Error Banner component snapshot 1`] = `
|
||||
<Alert
|
||||
actions={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction action1.onClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Retry"
|
||||
description="Retry button for error in file renderer"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
|
||||
/>
|
||||
</Button>,
|
||||
<Button
|
||||
onClick={[MockFunction action2.onClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Retry"
|
||||
description="Retry button for error in file renderer"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Unknown errors"
|
||||
description="Unknown errors message"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
Abitary Child
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -0,0 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Loading Banner component snapshot 1`] = `
|
||||
<Alert
|
||||
variant="info"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="d-flex m-auto"
|
||||
/>
|
||||
</Alert>
|
||||
`;
|
||||
2
src/components/FilePreview/Banners/index.jsx
Normal file
2
src/components/FilePreview/Banners/index.jsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as ErrorBanner } from './ErrorBanner';
|
||||
export { default as LoadingBanner } from './LoadingBanner';
|
||||
27
src/components/FilePreview/BaseRenderers/ImageRenderer.jsx
Normal file
27
src/components/FilePreview/BaseRenderers/ImageRenderer.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ImageRenderer = ({
|
||||
url, fileName, onError, onSuccess,
|
||||
}) => (
|
||||
<img
|
||||
alt={fileName}
|
||||
className="image-renderer"
|
||||
src={url}
|
||||
onError={onError}
|
||||
onLoad={onSuccess}
|
||||
/>
|
||||
);
|
||||
|
||||
ImageRenderer.defaultProps = {
|
||||
fileName: '',
|
||||
};
|
||||
|
||||
ImageRenderer.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
fileName: PropTypes.string,
|
||||
onError: PropTypes.func.isRequired,
|
||||
onSuccess: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImageRenderer;
|
||||
@@ -8,6 +8,9 @@ describe('Image Renderer Component', () => {
|
||||
url: 'some_url.jpg',
|
||||
};
|
||||
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ImageRenderer {...props} />);
|
||||
@@ -37,6 +37,7 @@ export class PDFRenderer extends React.Component {
|
||||
}
|
||||
|
||||
onDocumentLoadSuccess = ({ numPages }) => {
|
||||
this.props.onSuccess();
|
||||
this.setState({ numPages });
|
||||
};
|
||||
|
||||
@@ -51,8 +52,16 @@ export class PDFRenderer extends React.Component {
|
||||
};
|
||||
|
||||
onDocumentLoadError = (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
let status;
|
||||
switch (error.name) {
|
||||
case 'MissingPDFException':
|
||||
status = 404;
|
||||
break;
|
||||
default:
|
||||
status = 500;
|
||||
break;
|
||||
}
|
||||
this.props.onError(status);
|
||||
};
|
||||
|
||||
onInputPageChange = ({ target: { value } }) => {
|
||||
@@ -140,6 +149,8 @@ PDFRenderer.defaultProps = {};
|
||||
|
||||
PDFRenderer.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
onError: PropTypes.func.isRequired,
|
||||
onSuccess: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PDFRenderer;
|
||||
@@ -17,6 +17,9 @@ describe('PDF Renderer Component', () => {
|
||||
url: 'some_url.pdf',
|
||||
};
|
||||
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
@@ -1,11 +1,16 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { get } from 'data/services/lms/utils';
|
||||
import { get } from 'axios';
|
||||
|
||||
const TXTRenderer = ({ url }) => {
|
||||
const TXTRenderer = ({ url, onError, onSuccess }) => {
|
||||
const [content, setContent] = useState('');
|
||||
useMemo(() => {
|
||||
get(url).then(({ data }) => setContent(data));
|
||||
get(url)
|
||||
.then(({ data }) => {
|
||||
onSuccess();
|
||||
setContent(data);
|
||||
})
|
||||
.catch(({ response }) => onError(response.status));
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
@@ -19,6 +24,8 @@ TXTRenderer.defaultProps = {};
|
||||
|
||||
TXTRenderer.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
onError: PropTypes.func.isRequired,
|
||||
onSuccess: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default TXTRenderer;
|
||||
@@ -3,15 +3,18 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import TXTRenderer from './TXTRenderer';
|
||||
|
||||
jest.mock('data/services/lms/utils', () => ({
|
||||
jest.mock('axios', () => ({
|
||||
get: jest.fn((...args) => Promise.resolve({ data: `Content of ${args}` })),
|
||||
}));
|
||||
|
||||
describe('Image Renderer Component', () => {
|
||||
describe('TXT Renderer Component', () => {
|
||||
const props = {
|
||||
url: 'some_url.txt',
|
||||
};
|
||||
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<TXTRenderer {...props} />);
|
||||
@@ -4,6 +4,8 @@ exports[`Image Renderer Component snapshot 1`] = `
|
||||
<img
|
||||
alt=""
|
||||
className="image-renderer"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onLoad={[MockFunction this.props.onSuccess]}
|
||||
src="some_url.jpg"
|
||||
/>
|
||||
`;
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Image Renderer Component snapshot 1`] = `
|
||||
exports[`TXT Renderer Component snapshot 1`] = `
|
||||
<pre
|
||||
className="txt-renderer"
|
||||
>
|
||||
3
src/components/FilePreview/BaseRenderers/index.jsx
Normal file
3
src/components/FilePreview/BaseRenderers/index.jsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as ImageRenderer } from './ImageRenderer';
|
||||
export { default as PDFRenderer } from './PDFRenderer';
|
||||
export { default as TXTRenderer } from './TXTRenderer';
|
||||
125
src/components/FilePreview/FileRenderer.jsx
Normal file
125
src/components/FilePreview/FileRenderer.jsx
Normal file
@@ -0,0 +1,125 @@
|
||||
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 {
|
||||
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,
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
/**
|
||||
* <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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileRenderer.defaultProps = {};
|
||||
FileRenderer.propTypes = {
|
||||
file: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default FileRenderer;
|
||||
128
src/components/FilePreview/FileRenderer.test.jsx
Normal file
128
src/components/FilePreview/FileRenderer.test.jsx
Normal file
@@ -0,0 +1,128 @@
|
||||
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';
|
||||
|
||||
jest.mock('./FileCard', () => 'FileCard');
|
||||
|
||||
jest.mock('components/FilePreview/BaseRenderers', () => ({
|
||||
PDFRenderer: () => 'PDFRenderer',
|
||||
ImageRenderer: () => 'ImageRenderer',
|
||||
TXTRenderer: () => 'TXTRenderer',
|
||||
}));
|
||||
|
||||
jest.mock('./Banners', () => ({
|
||||
ErrorBanner: () => 'ErrorBanner',
|
||||
LoadingBanner: () => 'LoadingBanner',
|
||||
}));
|
||||
|
||||
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);
|
||||
};
|
||||
/**
|
||||
* 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('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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ImageRenderer = ({ url, fileName }) => (<img alt={fileName} className="image-renderer" src={url} />);
|
||||
|
||||
ImageRenderer.defaultProps = {
|
||||
fileName: '',
|
||||
};
|
||||
|
||||
ImageRenderer.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
fileName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ImageRenderer;
|
||||
@@ -0,0 +1,197 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileRenderer component snapshot has error 404 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
"downloadUrl": "/url-path/fake_file_0.pdf",
|
||||
"name": "fake_file_0.pdf",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ErrorBanner
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"id": "retry",
|
||||
"message": Object {
|
||||
"defaultMessage": "Retry",
|
||||
"description": "Retry button for error in file renderer",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.retryButton",
|
||||
},
|
||||
"onClick": [MockFunction this.resetState],
|
||||
},
|
||||
]
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "File not found",
|
||||
"description": "File not found error message",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.fileNotFound",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="File not found"
|
||||
description="File not found error message"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.fileNotFound"
|
||||
/>
|
||||
</ErrorBanner>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot has error 500 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
"downloadUrl": "/url-path/fake_file_0.pdf",
|
||||
"name": "fake_file_0.pdf",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ErrorBanner
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"id": "retry",
|
||||
"message": Object {
|
||||
"defaultMessage": "Retry",
|
||||
"description": "Retry button for error in file renderer",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.retryButton",
|
||||
},
|
||||
"onClick": [MockFunction this.resetState],
|
||||
},
|
||||
]
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "Unknown errors",
|
||||
"description": "Unknown errors message",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.unknownError",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Unknown errors"
|
||||
description="Unknown errors message"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
|
||||
/>
|
||||
</ErrorBanner>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering bmp 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 3",
|
||||
"downloadUrl": "/url-path/fake_file_3.bmp",
|
||||
"name": "fake_file_3.bmp",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_3.bmp"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_3.bmp"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering 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 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 txt 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 5",
|
||||
"downloadUrl": "/url-path/fake_file_5.txt",
|
||||
"name": "fake_file_5.txt",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TXTRenderer
|
||||
fileName="fake_file_5.txt"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_5.txt"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
@@ -1,4 +1 @@
|
||||
export { default as FileCard } from './FileCard';
|
||||
export { default as ImageRenderer } from './ImageRenderer';
|
||||
export { default as PDFRenderer } from './PDFRenderer';
|
||||
export { default as TXTRenderer } from './TXTRenderer';
|
||||
export { default as FileRenderer, isSupported } from './FileRenderer';
|
||||
|
||||
@@ -6,6 +6,21 @@ const messages = defineMessages({
|
||||
defaultMessage: 'File info',
|
||||
description: 'Popover trigger button text for file preview card',
|
||||
},
|
||||
retryButton: {
|
||||
id: 'ora-grading.ResponseDisplay.FileRenderer.retryButton',
|
||||
defaultMessage: 'Retry',
|
||||
description: 'Retry button for error in file renderer',
|
||||
},
|
||||
fileNotFoundError: {
|
||||
id: 'ora-grading.ResponseDisplay.FileRenderer.fileNotFound',
|
||||
defaultMessage: 'File not found',
|
||||
description: 'File not found error message',
|
||||
},
|
||||
unknownError: {
|
||||
id: 'ora-grading.ResponseDisplay.FileRenderer.unknownError',
|
||||
defaultMessage: 'Unknown errors',
|
||||
description: 'Unknown errors message',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,70 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
|
||||
import {
|
||||
FileCard, PDFRenderer, ImageRenderer, TXTRenderer,
|
||||
} from 'components/FilePreview';
|
||||
import { FileRenderer, isSupported } from 'components/FilePreview';
|
||||
|
||||
/**
|
||||
* <PreviewDisplay />
|
||||
*/
|
||||
export class PreviewDisplay extends React.Component {
|
||||
static RENDERERS = StrictDict({
|
||||
[FileTypes.pdf]: PDFRenderer,
|
||||
[FileTypes.jpg]: ImageRenderer,
|
||||
[FileTypes.jpeg]: ImageRenderer,
|
||||
[FileTypes.bmp]: ImageRenderer,
|
||||
[FileTypes.png]: ImageRenderer,
|
||||
[FileTypes.txt]: TXTRenderer,
|
||||
});
|
||||
|
||||
static SUPPORTED_TYPES = Object.keys(PreviewDisplay.RENDERERS);
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.isSupported = this.isSupported.bind(this);
|
||||
this.fileType = this.fileType.bind(this);
|
||||
}
|
||||
|
||||
get supportedFiles() {
|
||||
return this.props.files.filter(this.isSupported);
|
||||
}
|
||||
|
||||
isSupported(file) {
|
||||
return PreviewDisplay.SUPPORTED_TYPES.includes(this.fileType(file.name));
|
||||
}
|
||||
|
||||
fileType(fileName) {
|
||||
return fileName.split('.').pop();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="preview-display">
|
||||
{this.supportedFiles.map((file) => {
|
||||
const Renderer = PreviewDisplay.RENDERERS[this.fileType(file.name)];
|
||||
return (
|
||||
<FileCard key={file.downloadUrl} file={file}>
|
||||
<Renderer fileName={file.name} url={file.downloadUrl} />
|
||||
</FileCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const PreviewDisplay = ({ files }) => (
|
||||
<div className="preview-display">
|
||||
{files.filter(isSupported).map((file) => (
|
||||
<FileRenderer key={file.name} file={file} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
PreviewDisplay.defaultProps = {
|
||||
files: [],
|
||||
};
|
||||
PreviewDisplay.propTypes = {
|
||||
files: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
})),
|
||||
files: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
export default PreviewDisplay;
|
||||
|
||||
@@ -2,24 +2,17 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
import { FileCard, ImageRenderer, PDFRenderer } from 'components/FilePreview';
|
||||
import { FileRenderer } from 'components/FilePreview';
|
||||
import { PreviewDisplay } from './PreviewDisplay';
|
||||
|
||||
jest.mock('components/FilePreview', () => ({
|
||||
FileCard: () => 'FileCard',
|
||||
PDFRenderer: () => 'PDFRenderer',
|
||||
ImageRenderer: () => 'ImageRenderer',
|
||||
FileRenderer: () => 'FileRenderer',
|
||||
isSupported: jest.requireActual('components/FilePreview').isSupported,
|
||||
}));
|
||||
|
||||
describe('PreviewDisplay', () => {
|
||||
describe('component', () => {
|
||||
const supportedTypes = [
|
||||
FileTypes.pdf,
|
||||
FileTypes.jpg,
|
||||
FileTypes.jpeg,
|
||||
FileTypes.bmp,
|
||||
FileTypes.png,
|
||||
];
|
||||
const supportedTypes = Object.values(FileTypes);
|
||||
const props = {
|
||||
files: [
|
||||
...supportedTypes.map((fileType, index) => ({
|
||||
@@ -40,44 +33,25 @@ describe('PreviewDisplay', () => {
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('files does not exist', () => {
|
||||
test('files render with props', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('files exited for props', () => {
|
||||
test('files does not exist', () => {
|
||||
el.setProps({ files: [] });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('only renders compatible files', () => {
|
||||
const cards = el.find(FileCard);
|
||||
const cards = el.find(FileRenderer);
|
||||
expect(cards.length).toEqual(supportedTypes.length);
|
||||
[0, 1, 2, 3, 4].forEach(index => {
|
||||
cards.forEach((_, index) => {
|
||||
expect(
|
||||
cards.at(index).prop('file'),
|
||||
).toEqual(props.files[index]);
|
||||
});
|
||||
});
|
||||
describe('uses the correct renderers', () => {
|
||||
const loadRenderer = (index) => (
|
||||
el.find(FileCard).at(index).children().at(0)
|
||||
);
|
||||
const checkFile = (index, expectedRenderer) => {
|
||||
const file = props.files[index];
|
||||
const renderer = loadRenderer(index);
|
||||
expect(renderer.type()).toEqual(expectedRenderer);
|
||||
expect(renderer.props()).toEqual({
|
||||
url: file.downloadUrl,
|
||||
fileName: file.name,
|
||||
});
|
||||
};
|
||||
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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PreviewDisplay component snapshot files does not exist 1`] = `
|
||||
<div
|
||||
className="preview-display"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`PreviewDisplay component snapshot files render with props 1`] = `
|
||||
<div
|
||||
className="preview-display"
|
||||
>
|
||||
<FileCard
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
@@ -12,14 +18,9 @@ exports[`PreviewDisplay component snapshot files does not exist 1`] = `
|
||||
"name": "fake_file_0.pdf",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_0.pdf"
|
||||
>
|
||||
<PDFRenderer
|
||||
fileName="fake_file_0.pdf"
|
||||
url="/url-path/fake_file_0.pdf"
|
||||
/>
|
||||
</FileCard>
|
||||
<FileCard
|
||||
key="fake_file_0.pdf"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 1",
|
||||
@@ -27,14 +28,9 @@ exports[`PreviewDisplay component snapshot files does not exist 1`] = `
|
||||
"name": "fake_file_1.jpg",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_1.jpg"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_1.jpg"
|
||||
url="/url-path/fake_file_1.jpg"
|
||||
/>
|
||||
</FileCard>
|
||||
<FileCard
|
||||
key="fake_file_1.jpg"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 2",
|
||||
@@ -42,48 +38,37 @@ exports[`PreviewDisplay component snapshot files does not exist 1`] = `
|
||||
"name": "fake_file_2.jpeg",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_2.jpeg"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_2.jpeg"
|
||||
url="/url-path/fake_file_2.jpeg"
|
||||
/>
|
||||
</FileCard>
|
||||
<FileCard
|
||||
key="fake_file_2.jpeg"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 3",
|
||||
"downloadUrl": "/url-path/fake_file_3.bmp",
|
||||
"name": "fake_file_3.bmp",
|
||||
"downloadUrl": "/url-path/fake_file_3.png",
|
||||
"name": "fake_file_3.png",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_3.bmp"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_3.bmp"
|
||||
url="/url-path/fake_file_3.bmp"
|
||||
/>
|
||||
</FileCard>
|
||||
<FileCard
|
||||
key="fake_file_3.png"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 4",
|
||||
"downloadUrl": "/url-path/fake_file_4.png",
|
||||
"name": "fake_file_4.png",
|
||||
"downloadUrl": "/url-path/fake_file_4.bmp",
|
||||
"name": "fake_file_4.bmp",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_4.png"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_4.png"
|
||||
url="/url-path/fake_file_4.png"
|
||||
/>
|
||||
</FileCard>
|
||||
key="fake_file_4.bmp"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 5",
|
||||
"downloadUrl": "/url-path/fake_file_5.txt",
|
||||
"name": "fake_file_5.txt",
|
||||
}
|
||||
}
|
||||
key="fake_file_5.txt"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PreviewDisplay component snapshot files exited for props 1`] = `
|
||||
<div
|
||||
className="preview-display"
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -74,6 +74,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
Row: 'Row',
|
||||
StatefulButton: 'StatefulButton',
|
||||
TextFilter: 'TextFilter',
|
||||
Spinner: 'Spinner',
|
||||
}));
|
||||
|
||||
jest.mock('@fortawesome/react-fontawesome', () => ({
|
||||
|
||||
Reference in New Issue
Block a user