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:
leangseu-edx
2022-01-19 12:34:44 -05:00
committed by GitHub
parent b2aac6036e
commit c5bf0a7d11
29 changed files with 789 additions and 172 deletions

16
package-lock.json generated
View File

@@ -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": {

View File

@@ -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",

View 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;

View 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);
});
});
});

View 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;

View 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();
});
});

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -0,0 +1,2 @@
export { default as ErrorBanner } from './ErrorBanner';
export { default as LoadingBanner } from './LoadingBanner';

View 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;

View File

@@ -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} />);

View File

@@ -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;

View File

@@ -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(() => {

View File

@@ -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;

View File

@@ -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} />);

View File

@@ -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"
/>
`;

View File

@@ -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"
>

View File

@@ -0,0 +1,3 @@
export { default as ImageRenderer } from './ImageRenderer';
export { default as PDFRenderer } from './PDFRenderer';
export { default as TXTRenderer } from './TXTRenderer';

View 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;

View 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();
});
});
});
});
});

View File

@@ -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;

View File

@@ -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>
`;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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));
});
});
});
});

View File

@@ -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"
/>
`;

View File

@@ -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', () => ({