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