diff --git a/app/javascript/article-form/components/ImageUploader.jsx b/app/javascript/article-form/components/ImageUploader.jsx index e6b5bd43a..063f02808 100644 --- a/app/javascript/article-form/components/ImageUploader.jsx +++ b/app/javascript/article-form/components/ImageUploader.jsx @@ -2,7 +2,7 @@ import { h, Component } from 'preact'; import { generateMainImage } from '../actions'; import { validateFileInputs } from '../../packs/validateFileInputs'; import { ClipboardButton } from './ClipboardButton'; -import { Button } from '@crayons'; +import { Button, Spinner } from '@crayons'; function isNativeAndroid() { return ( @@ -41,6 +41,7 @@ export class ImageUploader extends Component { uploadError: false, uploadErrorMessage: null, showImageCopiedMessage: false, + uploadingImage: false, }; onUploadError = (error) => { @@ -48,6 +49,7 @@ export class ImageUploader extends Component { insertionImageUrls: [], uploadError: true, uploadErrorMessage: error.message, + uploadingImage: false, }); }; @@ -76,13 +78,16 @@ export class ImageUploader extends Component { handleInsertionImageUpload = (e) => { const { files } = e.target; - this.clearUploadError(); - const validFileInputs = validateFileInputs(); - - if (validFileInputs && files.length > 0) { + if (files.length > 0 && validateFileInputs()) { const payload = { image: files }; - this.setState({ showImageCopiedMessage: false }); + this.setState({ + uploadError: false, + uploadErrorMessage: null, + uploadingImage: true, + insertionImageUrls: [], + showImageCopiedMessage: false, + }); generateMainImage( payload, @@ -94,6 +99,7 @@ export class ImageUploader extends Component { handleInsertImageUploadSuccess = (response) => { this.setState({ + uploadingImage: false, insertionImageUrls: response.links, }); }; @@ -107,43 +113,43 @@ export class ImageUploader extends Component { this.setState({ showImageCopiedMessage: true }); } - clearUploadError() { - this.setState({ - uploadError: false, - uploadErrorMessage: null, - }); - } - render() { const { insertionImageUrls, uploadError, uploadErrorMessage, showImageCopiedMessage, + uploadingImage, } = this.state; return (
- + > + Upload image + + + )} {insertionImageUrls.length > 0 && ( ', () => { - const fakeLinksResponse = JSON.stringify({ - links: ['/i/fake-link.jpg'], - }); - - const fakeErrorMessage = { - message: 'Some Fake Error', - }; - it('should have no a11y violations', async () => { const { container } = render(); const results = await axe(container); @@ -29,30 +26,74 @@ describe('', () => { expect(uploadInput.getAttribute('type')).toEqual('file'); }); - it('displays text to copy after upload', async () => { - const { getByTitle, getByDisplayValue, getByLabelText } = render( - , + it('displays the upload spinner during upload', async () => { + fetch.mockResponse( + JSON.stringify({ + links: ['/i/fake-link.jpg'], + }), ); + + const { getByLabelText, queryByText } = render(); + + const inputEl = getByLabelText(/Upload an image/i); + const file = new File(['(⌐□_□)'], 'chucknorris.png', { + type: 'image/png', + }); + + fireEvent.change(inputEl, { target: { files: [file] } }); + + const uploadingImage = await waitForElement(() => + queryByText(/uploading.../i), + ); + + expect(uploadingImage).toBeDefined(); + }); + + it('displays text to copy after upload', async () => { + fetch.mockResponse( + JSON.stringify({ + links: ['/i/fake-link.jpg'], + }), + ); + + const { + getByTitle, + getByDisplayValue, + getByLabelText, + queryByText, + } = render(); const inputEl = getByLabelText(/Upload an image/i); const file = new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png', }); - fetch.mockResponse(fakeLinksResponse); fireEvent.change(inputEl, { target: { files: [file] } }); + let uploadingImage = await waitForElement(() => + queryByText(/uploading.../i), + ); + + expect(uploadingImage).toBeDefined(); expect(inputEl.files[0]).toEqual(file); expect(inputEl.files).toHaveLength(1); - await waitForElement(() => getByTitle(/copy markdown for image/i)); + await waitForElementToBeRemoved(() => queryByText(/uploading.../i)); + + getByTitle(/copy markdown for image/i); getByDisplayValue(/fake-link.jpg/i); }); // TODO: 'Copied!' is always in the DOM, and so we cannot test that the visual implications of the copy when clicking on the copy icon it('displays an error when one occurs', async () => { - const { getByText, getByLabelText } = render(); + fetch.mockReject({ + message: 'Some Fake Error', + }); + + const { getByText, getByLabelText, queryByText } = render( + , + ); const inputEl = getByLabelText(/Upload an image/i); // Check the input validation settings @@ -63,13 +104,19 @@ describe('', () => { type: 'image/png', }); - fetch.mockReject(fakeErrorMessage); fireEvent.change(inputEl, { target: { files: [file], }, }); + let uploadingImage = await waitForElement(() => + queryByText(/uploading.../i), + ); + + expect(uploadingImage).toBeDefined(); + + await waitForElementToBeRemoved(() => queryByText(/uploading.../i)); await waitForElement(() => getByText(/some fake error/i)); }); }); diff --git a/app/javascript/crayons/Spinner/index.js b/app/javascript/crayons/Spinner/index.js new file mode 100644 index 000000000..b259397be --- /dev/null +++ b/app/javascript/crayons/Spinner/index.js @@ -0,0 +1 @@ +export * from './Spinner'; diff --git a/app/javascript/crayons/index.js b/app/javascript/crayons/index.js index f1025d486..1f8885db5 100644 --- a/app/javascript/crayons/index.js +++ b/app/javascript/crayons/index.js @@ -3,3 +3,4 @@ export * from '@crayons/ButtonGroup'; export * from '@crayons/Dropdown'; export * from '@crayons/formElements'; export * from '@crayons/Modal'; +export * from '@crayons/Spinner';