Skip to content

Commit

Permalink
UIU-3080 - Handle invalid image URLs when uploading profile photo (#2670
Browse files Browse the repository at this point in the history
)

* UIU-3080 - error message on invalid Image URL

* restrict save on invalid url n update test cases

* increase test coverage

* test cases for isAValidImageUrl function

* add optional chaining in isAValidImageUrl

* isAValidImageUrl refinement

* change img url validation approach

* remove handleBlur and update test cases

* refine state variable usage

* remove unnecessary code
  • Loading branch information
manvendra-s-rathore authored Apr 29, 2024
1 parent 5077eb7 commit 45f3094
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Update username field validation to trim leading and trailing spaces. Refs UIU-3099.
* Fix "Total paid amount" value that set as "$NaN" on "Refund fee/fine" modal. Refs UIU-3094.
* Allow override for reminder fees with renewal blocked. Refs UICIRC-1077.
* Validate image url provided as external url for user profile picture. Refs UIU-3080.

## [10.1.0](https://github.com/folio-org/ui-users/tree/v10.1.0) (2024-03-20)
[Full Changelog](https://github.com/folio-org/ui-users/compare/v10.0.4...v10.1.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ jest.mock('react-image', () => ({
Img: jest.fn(() => null),
}));

jest.mock('../../../../util', () => ({
isAValidImageUrl: jest.fn(() => true),
isAValidURL: jest.fn(() => true),
}));

const defaultProps = {
profilePictureMaxFileSize: 3,
profilePictureId: 'https://folio.org/wp-content/uploads/2023/08/folio-site-general-Illustration-social-image-1200.jpg',
Expand Down Expand Up @@ -157,7 +162,7 @@ describe('Edit User Profile Picture', () => {
await userEvent.click(saveButton);
expect(Img).toHaveBeenCalled();
const renderedProfileImg = Img.mock.lastCall[0];
expect(expect(renderedProfileImg.src).toContain('https://upload.wikimedia.org/wikipedia/commons/e/e2/FOLIO_400x400.jpg'));
expect(renderedProfileImg.src).toContain('https://upload.wikimedia.org/wikipedia/commons/e/e2/FOLIO_400x400.jpg');
});

it('should invoke local file upload handlers with compression', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
TextField,
} from '@folio/stripes/components';
import { FormattedMessage } from 'react-intl';
import { isAValidURL } from '../../../../util';
import { isAValidURL, isAValidImageUrl } from '../../../../util';

const ExternalLinkModal = ({
open,
Expand All @@ -23,26 +23,34 @@ const ExternalLinkModal = ({
const [inputValue, setInputValue] = useState('');
const previousInputValue = useRef(profilePictureLink);
const [disabled, setDisabled] = useState(false);
const [error, setError] = useState(false);
const externalURLValidityError = error ?
<FormattedMessage id="ui-users.information.profilePicture.externalLink.modal.externalURL.errorMessage" />
: null;
const [externalURLValidityError, setExternalURLValidityError] = useState(null);

useEffect(() => {
setInputValue(profilePictureLink);
setError(false);
}, [profilePictureLink]);

useEffect(() => {
setExternalURLValidityError(null);
if (inputValue) {
setDisabled(previousInputValue.current === inputValue);
setError(false);
} else {
setDisabled(true);
}
}, [inputValue]);

const handleSave = () => {
const handleSave = async () => {
if (!isAValidURL(inputValue)) {
setExternalURLValidityError(<FormattedMessage id="ui-users.information.profilePicture.externalLink.modal.externalURL.invalidURLErrorMessage" />);
setDisabled(true);
return;
}

const isValidImgURL = await isAValidImageUrl(inputValue);
if (!isValidImgURL) {
setExternalURLValidityError(<FormattedMessage id="ui-users.information.profilePicture.externalLink.modal.externalURL.invalidImageURLErrorMessage" />);
setDisabled(true);
return;
}
onSave(inputValue);
};

Expand All @@ -51,13 +59,6 @@ const ExternalLinkModal = ({
setInputValue(e.target.value);
};

const handleBlur = () => {
if (inputValue && !isAValidURL(inputValue)) {
setError(true);
setDisabled(true);
}
};

const renderModalFooter = () => {
return (
<ModalFooter>
Expand Down Expand Up @@ -92,7 +93,6 @@ const ExternalLinkModal = ({
label={<FormattedMessage id="ui-users.information.profilePicture.externalLink.modal.externalURL" />}
error={externalURLValidityError}
onChange={handleInputChange}
onBlur={handleBlur}
value={inputValue}
hasClearIcon={false}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@folio/jest-config-stripes/testing-library/react';
import userEvent from '@folio/jest-config-stripes/testing-library/user-event';

import { isAValidURL } from '../../../../util';
import { isAValidURL, isAValidImageUrl } from '../../../../util';

import '../../../../../../test/jest/__mock__';

Expand Down Expand Up @@ -43,6 +43,7 @@ describe('ExternalLinkModal', () => {

beforeEach(() => {
isAValidURL.mockReset();
isAValidImageUrl.mockReset();
renderExternalLinkModal(props);
});

Expand All @@ -53,6 +54,9 @@ describe('ExternalLinkModal', () => {
expect(screen.getByText('ui-users.information.profilePicture.externalLink.modal.externalURL')).toBeInTheDocument();
});
it('should call onSave', async () => {
isAValidURL.mockImplementationOnce(() => true);
isAValidImageUrl.mockImplementationOnce(() => true);

const saveButton = screen.getByRole('button', { name: 'ui-users.save' });
const inputElement = screen.getByLabelText('ui-users.information.profilePicture.externalLink.modal.externalURL');

Expand All @@ -61,14 +65,28 @@ describe('ExternalLinkModal', () => {

expect(props.onSave).toHaveBeenCalled();
});
it('should show error text when url is invalid', async () => {
it('should show error text when url is invalid url', async () => {
isAValidURL.mockImplementationOnce(() => false);

const saveButton = screen.getByRole('button', { name: 'ui-users.save' });
const inputElement = screen.getByLabelText('ui-users.information.profilePicture.externalLink.modal.externalURL');

fireEvent.change(inputElement, { target: { value: 'profile picture' } });
fireEvent.blur(inputElement);
await userEvent.click(saveButton);

await waitFor(() => expect(screen.getByText('ui-users.information.profilePicture.externalLink.modal.externalURL.invalidURLErrorMessage')).toBeInTheDocument());
});
it('should show error text when url is invalid image url', async () => {
isAValidURL.mockImplementationOnce(() => true);
isAValidImageUrl.mockImplementationOnce(() => false);

const saveButton = screen.getByRole('button', { name: 'ui-users.save' });
const inputElement = screen.getByLabelText('ui-users.information.profilePicture.externalLink.modal.externalURL');

fireEvent.change(inputElement, { target: { value: 'https://folio-org.atlassian.net/browse/UIU-3080' } });
await userEvent.click(saveButton);

await waitFor(() => expect(screen.getByText('ui-users.information.profilePicture.externalLink.modal.externalURL.errorMessage')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('ui-users.information.profilePicture.externalLink.modal.externalURL.invalidImageURLErrorMessage')).toBeInTheDocument());
});
it('should call onClose', async () => {
const cancelButton = screen.getByRole('button', { name: 'stripes-core.button.cancel' });
Expand Down
12 changes: 12 additions & 0 deletions src/components/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,15 @@ export const isAValidURL = (str) => {
return URL.canParse(str);
};

export const isAValidImageUrl = async (url) => {
try {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
img.src = url;
});
} catch (e) {
return false;
}
};
50 changes: 50 additions & 0 deletions src/components/util/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
getRequestUrl,
isAffiliationsEnabled,
isDCBItem,
isAValidImageUrl,
} from './util';

const STRIPES = {
Expand Down Expand Up @@ -474,3 +475,52 @@ describe('isDCBItem ', () => {
expect(isDCBItem(item)).toBeFalsy();
});
});

describe('isAValidImageUrl', () => {
it('should return true for a valid image URL', async () => {
class MockImage {
constructor() {
this.onload = null;
this.onerror = null;
}

set src(value) {
if (this.onload) {
setTimeout(() => {
this.onload();
}, 0);
}
}
}

global.Image = MockImage;
const validImageUrl = 'https://upload.wikimedia.org/wikipedia/commons/e/e2/FOLIO_400x400.jpg';

const isValid = await isAValidImageUrl(validImageUrl);
expect(isValid).toBe(true);
});

it('should return false for an invalid image URL', async () => {
class MockImage {
constructor() {
this.onload = null;
this.onerror = null;
}

set src(value) {
if (this.onerror) {
setTimeout(() => {
this.onerror();
}, 0);
}
}
}

global.Image = MockImage;
const invalidImageUrl = 'https://example.com';

const isValid = await isAValidImageUrl(invalidImageUrl);
expect(isValid).toBe(false);
});
});

3 changes: 2 additions & 1 deletion translations/ui-users/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@
"information.profilePicture.delete": "Delete",
"information.profilePicture.externalLink.modal.updateProfilePicture": "Update profile picture",
"information.profilePicture.externalLink.modal.externalURL": "External URL",
"information.profilePicture.externalLink.modal.externalURL.errorMessage": "Invalid image URL",
"information.profilePicture.externalLink.modal.externalURL.invalidURLErrorMessage": "Invalid image URL",
"information.profilePicture.externalLink.modal.externalURL.invalidImageURLErrorMessage": "The provided URL is valid but does not point to an image file",
"information.profilePicture.delete.modal.message": "You are deleting the profile picture for {name}",
"information.profilePicture.delete.modal.heading": "Delete profile picture",
"information.profilePicture.localFile.modal.previewAndEdit": "Preview and edit",
Expand Down

0 comments on commit 45f3094

Please sign in to comment.