diff --git a/package-lock.json b/package-lock.json index 63b4dab..0a32cc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index d934f62..d3c5908 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/FilePreview/Banners/ErrorBanner.jsx b/src/components/FilePreview/Banners/ErrorBanner.jsx new file mode 100644 index 0000000..4be244c --- /dev/null +++ b/src/components/FilePreview/Banners/ErrorBanner.jsx @@ -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 => ( + + )); + return ( + + + + + {children} + + ); +}; +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; diff --git a/src/components/FilePreview/Banners/ErrorBanner.test.jsx b/src/components/FilePreview/Banners/ErrorBanner.test.jsx new file mode 100644 index 0000000..6a9d76c --- /dev/null +++ b/src/components/FilePreview/Banners/ErrorBanner.test.jsx @@ -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 =

Abitary Child

; + + 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(); + }); + + 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); + }); + }); +}); diff --git a/src/components/FilePreview/Banners/LoadingBanner.jsx b/src/components/FilePreview/Banners/LoadingBanner.jsx new file mode 100644 index 0000000..4557533 --- /dev/null +++ b/src/components/FilePreview/Banners/LoadingBanner.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Alert, Spinner } from '@edx/paragon'; + +export const LoadingBanner = () => ( + + + +); + +LoadingBanner.defaultProps = {}; +LoadingBanner.propTypes = {}; + +export default LoadingBanner; diff --git a/src/components/FilePreview/Banners/LoadingBanner.test.jsx b/src/components/FilePreview/Banners/LoadingBanner.test.jsx new file mode 100644 index 0000000..affe55f --- /dev/null +++ b/src/components/FilePreview/Banners/LoadingBanner.test.jsx @@ -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(); + expect(el).toMatchSnapshot(); + }); +}); diff --git a/src/components/FilePreview/Banners/__snapshots__/ErrorBanner.test.jsx.snap b/src/components/FilePreview/Banners/__snapshots__/ErrorBanner.test.jsx.snap new file mode 100644 index 0000000..83313e6 --- /dev/null +++ b/src/components/FilePreview/Banners/__snapshots__/ErrorBanner.test.jsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Error Banner component snapshot 1`] = ` + + + , + , + ] + } + variant="danger" +> + + + +

+ Abitary Child +

+
+`; diff --git a/src/components/FilePreview/Banners/__snapshots__/LoadingBanner.test.jsx.snap b/src/components/FilePreview/Banners/__snapshots__/LoadingBanner.test.jsx.snap new file mode 100644 index 0000000..d6a2a51 --- /dev/null +++ b/src/components/FilePreview/Banners/__snapshots__/LoadingBanner.test.jsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Loading Banner component snapshot 1`] = ` + + + +`; diff --git a/src/components/FilePreview/Banners/index.jsx b/src/components/FilePreview/Banners/index.jsx new file mode 100644 index 0000000..fbba7f6 --- /dev/null +++ b/src/components/FilePreview/Banners/index.jsx @@ -0,0 +1,2 @@ +export { default as ErrorBanner } from './ErrorBanner'; +export { default as LoadingBanner } from './LoadingBanner'; diff --git a/src/components/FilePreview/BaseRenderers/ImageRenderer.jsx b/src/components/FilePreview/BaseRenderers/ImageRenderer.jsx new file mode 100644 index 0000000..c4c975c --- /dev/null +++ b/src/components/FilePreview/BaseRenderers/ImageRenderer.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const ImageRenderer = ({ + url, fileName, onError, onSuccess, +}) => ( + {fileName} +); + +ImageRenderer.defaultProps = { + fileName: '', +}; + +ImageRenderer.propTypes = { + url: PropTypes.string.isRequired, + fileName: PropTypes.string, + onError: PropTypes.func.isRequired, + onSuccess: PropTypes.func.isRequired, +}; + +export default ImageRenderer; diff --git a/src/components/FilePreview/ImageRenderer.test.jsx b/src/components/FilePreview/BaseRenderers/ImageRenderer.test.jsx similarity index 74% rename from src/components/FilePreview/ImageRenderer.test.jsx rename to src/components/FilePreview/BaseRenderers/ImageRenderer.test.jsx index ee8b492..148044f 100644 --- a/src/components/FilePreview/ImageRenderer.test.jsx +++ b/src/components/FilePreview/BaseRenderers/ImageRenderer.test.jsx @@ -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(); diff --git a/src/components/FilePreview/PDFRenderer.jsx b/src/components/FilePreview/BaseRenderers/PDFRenderer.jsx similarity index 92% rename from src/components/FilePreview/PDFRenderer.jsx rename to src/components/FilePreview/BaseRenderers/PDFRenderer.jsx index 1d7bf94..acc23a4 100644 --- a/src/components/FilePreview/PDFRenderer.jsx +++ b/src/components/FilePreview/BaseRenderers/PDFRenderer.jsx @@ -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; diff --git a/src/components/FilePreview/PDFRenderer.test.jsx b/src/components/FilePreview/BaseRenderers/PDFRenderer.test.jsx similarity index 98% rename from src/components/FilePreview/PDFRenderer.test.jsx rename to src/components/FilePreview/BaseRenderers/PDFRenderer.test.jsx index fffb924..ee40f86 100644 --- a/src/components/FilePreview/PDFRenderer.test.jsx +++ b/src/components/FilePreview/BaseRenderers/PDFRenderer.test.jsx @@ -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(() => { diff --git a/src/components/FilePreview/TXTRenderer.jsx b/src/components/FilePreview/BaseRenderers/TXTRenderer.jsx similarity index 54% rename from src/components/FilePreview/TXTRenderer.jsx rename to src/components/FilePreview/BaseRenderers/TXTRenderer.jsx index 168d5de..1df9597 100644 --- a/src/components/FilePreview/TXTRenderer.jsx +++ b/src/components/FilePreview/BaseRenderers/TXTRenderer.jsx @@ -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; diff --git a/src/components/FilePreview/TXTRenderer.test.jsx b/src/components/FilePreview/BaseRenderers/TXTRenderer.test.jsx similarity index 66% rename from src/components/FilePreview/TXTRenderer.test.jsx rename to src/components/FilePreview/BaseRenderers/TXTRenderer.test.jsx index 4f205e8..5e2632c 100644 --- a/src/components/FilePreview/TXTRenderer.test.jsx +++ b/src/components/FilePreview/BaseRenderers/TXTRenderer.test.jsx @@ -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(); diff --git a/src/components/FilePreview/__snapshots__/ImageRenderer.test.jsx.snap b/src/components/FilePreview/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap similarity index 63% rename from src/components/FilePreview/__snapshots__/ImageRenderer.test.jsx.snap rename to src/components/FilePreview/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap index 9b293a7..3e9712c 100644 --- a/src/components/FilePreview/__snapshots__/ImageRenderer.test.jsx.snap +++ b/src/components/FilePreview/BaseRenderers/__snapshots__/ImageRenderer.test.jsx.snap @@ -4,6 +4,8 @@ exports[`Image Renderer Component snapshot 1`] = ` `; diff --git a/src/components/FilePreview/__snapshots__/PDFRenderer.test.jsx.snap b/src/components/FilePreview/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap similarity index 100% rename from src/components/FilePreview/__snapshots__/PDFRenderer.test.jsx.snap rename to src/components/FilePreview/BaseRenderers/__snapshots__/PDFRenderer.test.jsx.snap diff --git a/src/components/FilePreview/__snapshots__/TXTRenderer.test.jsx.snap b/src/components/FilePreview/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap similarity index 69% rename from src/components/FilePreview/__snapshots__/TXTRenderer.test.jsx.snap rename to src/components/FilePreview/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap index 91d905a..330a63c 100644 --- a/src/components/FilePreview/__snapshots__/TXTRenderer.test.jsx.snap +++ b/src/components/FilePreview/BaseRenderers/__snapshots__/TXTRenderer.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Image Renderer Component snapshot 1`] = ` +exports[`TXT Renderer Component snapshot 1`] = `
diff --git a/src/components/FilePreview/BaseRenderers/index.jsx b/src/components/FilePreview/BaseRenderers/index.jsx
new file mode 100644
index 0000000..e390639
--- /dev/null
+++ b/src/components/FilePreview/BaseRenderers/index.jsx
@@ -0,0 +1,3 @@
+export { default as ImageRenderer } from './ImageRenderer';
+export { default as PDFRenderer } from './PDFRenderer';
+export { default as TXTRenderer } from './TXTRenderer';
diff --git a/src/components/FilePreview/FileRenderer.jsx b/src/components/FilePreview/FileRenderer.jsx
new file mode 100644
index 0000000..fde6592
--- /dev/null
+++ b/src/components/FilePreview/FileRenderer.jsx
@@ -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: ,
+  },
+  500: {
+    headingMessage: messages.unknownError,
+    children: ,
+  },
+};
+
+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));
+
+/**
+ * 
+ */
+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 (
+      
+        {this.state.isLoading && }
+        {this.state.errorStatus ? (
+          
+        ) : (
+          
+        )}
+      
+    );
+  }
+}
+
+FileRenderer.defaultProps = {};
+FileRenderer.propTypes = {
+  file: PropTypes.shape({
+    name: PropTypes.string,
+    downloadUrl: PropTypes.string,
+  }).isRequired,
+};
+
+export default FileRenderer;
diff --git a/src/components/FilePreview/FileRenderer.test.jsx b/src/components/FilePreview/FileRenderer.test.jsx
new file mode 100644
index 0000000..d240d0c
--- /dev/null
+++ b/src/components/FilePreview/FileRenderer.test.jsx
@@ -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();
+      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();
+          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();
+          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();
+        });
+      });
+    });
+  });
+});
diff --git a/src/components/FilePreview/ImageRenderer.jsx b/src/components/FilePreview/ImageRenderer.jsx
deleted file mode 100644
index 75c5639..0000000
--- a/src/components/FilePreview/ImageRenderer.jsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-const ImageRenderer = ({ url, fileName }) => ({fileName});
-
-ImageRenderer.defaultProps = {
-  fileName: '',
-};
-
-ImageRenderer.propTypes = {
-  url: PropTypes.string.isRequired,
-  fileName: PropTypes.string,
-};
-
-export default ImageRenderer;
diff --git a/src/components/FilePreview/__snapshots__/FileRenderer.test.jsx.snap b/src/components/FilePreview/__snapshots__/FileRenderer.test.jsx.snap
new file mode 100644
index 0000000..2d2659a
--- /dev/null
+++ b/src/components/FilePreview/__snapshots__/FileRenderer.test.jsx.snap
@@ -0,0 +1,197 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FileRenderer component snapshot has error 404 1`] = `
+
+  
+    
+  
+
+`;
+
+exports[`FileRenderer component snapshot has error 500 1`] = `
+
+  
+    
+  
+
+`;
+
+exports[`FileRenderer component snapshot successful rendering bmp 1`] = `
+
+  
+
+`;
+
+exports[`FileRenderer component snapshot successful rendering jpeg 1`] = `
+
+  
+
+`;
+
+exports[`FileRenderer component snapshot successful rendering jpg 1`] = `
+
+  
+
+`;
+
+exports[`FileRenderer component snapshot successful rendering pdf 1`] = `
+
+  
+
+`;
+
+exports[`FileRenderer component snapshot successful rendering png 1`] = `
+
+  
+
+`;
+
+exports[`FileRenderer component snapshot successful rendering txt 1`] = `
+
+  
+
+`;
diff --git a/src/components/FilePreview/index.jsx b/src/components/FilePreview/index.jsx
index 44dbaba..2e03acc 100644
--- a/src/components/FilePreview/index.jsx
+++ b/src/components/FilePreview/index.jsx
@@ -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';
diff --git a/src/components/FilePreview/messages.js b/src/components/FilePreview/messages.js
index 86e5faf..573a546 100644
--- a/src/components/FilePreview/messages.js
+++ b/src/components/FilePreview/messages.js
@@ -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;
diff --git a/src/containers/ResponseDisplay/PreviewDisplay.jsx b/src/containers/ResponseDisplay/PreviewDisplay.jsx
index bda1dbe..a0bb3e1 100644
--- a/src/containers/ResponseDisplay/PreviewDisplay.jsx
+++ b/src/containers/ResponseDisplay/PreviewDisplay.jsx
@@ -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';
 
 /**
  * 
  */
-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 (
-      
- {this.supportedFiles.map((file) => { - const Renderer = PreviewDisplay.RENDERERS[this.fileType(file.name)]; - return ( - - - - ); - })} -
- ); - } -} +export const PreviewDisplay = ({ files }) => ( +
+ {files.filter(isSupported).map((file) => ( + + ))} +
+); 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; diff --git a/src/containers/ResponseDisplay/PreviewDisplay.test.jsx b/src/containers/ResponseDisplay/PreviewDisplay.test.jsx index 2a4831e..b567ffd 100644 --- a/src/containers/ResponseDisplay/PreviewDisplay.test.jsx +++ b/src/containers/ResponseDisplay/PreviewDisplay.test.jsx @@ -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)); - }); }); }); }); diff --git a/src/containers/ResponseDisplay/__snapshots__/PreviewDisplay.test.jsx.snap b/src/containers/ResponseDisplay/__snapshots__/PreviewDisplay.test.jsx.snap index 72138fd..dbad76f 100644 --- a/src/containers/ResponseDisplay/__snapshots__/PreviewDisplay.test.jsx.snap +++ b/src/containers/ResponseDisplay/__snapshots__/PreviewDisplay.test.jsx.snap @@ -1,10 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PreviewDisplay component snapshot files does not exist 1`] = ` +
+`; + +exports[`PreviewDisplay component snapshot files render with props 1`] = `
- - - - + - - - + - - - + - - - + - - + key="fake_file_4.bmp" + /> +
`; - -exports[`PreviewDisplay component snapshot files exited for props 1`] = ` -
-`; diff --git a/src/setupTest.js b/src/setupTest.js index 89cf949..cb4bc05 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -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', () => ({