mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-30 14:57:59 +01:00
Merge pull request #347 from wrenny-ko/client-tag-validation
Client-side tag input validation on image upload submit
This commit is contained in:
commit
42138a219b
3 changed files with 150 additions and 2 deletions
|
@ -25,6 +25,9 @@ const errorResponse = {
|
|||
};
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
const tagSets = ['', 'a tag', 'safe', 'one, two, three', 'safe, explicit', 'safe, explicit, three', 'safe, two, three'];
|
||||
const tagErrorCounts = [1, 2, 1, 1, 2, 1, 0];
|
||||
|
||||
describe('Image upload form', () => {
|
||||
let mockPng: File;
|
||||
let mockWebm: File;
|
||||
|
@ -58,13 +61,23 @@ describe('Image upload form', () => {
|
|||
let scraperError: HTMLDivElement;
|
||||
let fetchButton: HTMLButtonElement;
|
||||
let tagsEl: HTMLTextAreaElement;
|
||||
let taginputEl: HTMLDivElement;
|
||||
let sourceEl: HTMLInputElement;
|
||||
let descrEl: HTMLTextAreaElement;
|
||||
let submitButton: HTMLButtonElement;
|
||||
|
||||
const assertFetchButtonIsDisabled = () => {
|
||||
if (!fetchButton.hasAttribute('disabled')) throw new Error('fetchButton is not disabled');
|
||||
};
|
||||
|
||||
const assertSubmitButtonIsDisabled = () => {
|
||||
if (!submitButton.hasAttribute('disabled')) throw new Error('submitButton is not disabled');
|
||||
};
|
||||
|
||||
const assertSubmitButtonIsEnabled = () => {
|
||||
if (submitButton.hasAttribute('disabled')) throw new Error('submitButton is disabled');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
document.documentElement.insertAdjacentHTML(
|
||||
'beforeend',
|
||||
|
@ -77,7 +90,12 @@ describe('Image upload form', () => {
|
|||
|
||||
<input id="image_sources_0_source" name="image[sources][0][source]" type="text" class="js-source-url" />
|
||||
<textarea id="image_tag_input" name="image[tag_input]" class="js-image-tags-input"></textarea>
|
||||
<div class="js-taginput"></div>
|
||||
<button id="tagsinput-save" type="button" class="button">Save</button>
|
||||
<textarea id="image_description" name="image[description]" class="js-image-descr-input"></textarea>
|
||||
<div class="actions">
|
||||
<button class="button input--separate-top" type="submit">Upload</button>
|
||||
</div>
|
||||
</form>`,
|
||||
);
|
||||
|
||||
|
@ -87,9 +105,11 @@ describe('Image upload form', () => {
|
|||
remoteUrl = assertNotUndefined($$<HTMLInputElement>('.js-scraper')[1]);
|
||||
scraperError = assertNotUndefined($$<HTMLInputElement>('.js-scraper')[2]);
|
||||
tagsEl = assertNotNull($<HTMLTextAreaElement>('.js-image-tags-input'));
|
||||
taginputEl = assertNotNull($<HTMLDivElement>('.js-taginput'));
|
||||
sourceEl = assertNotNull($<HTMLInputElement>('.js-source-url'));
|
||||
descrEl = assertNotNull($<HTMLTextAreaElement>('.js-image-descr-input'));
|
||||
fetchButton = assertNotNull($<HTMLButtonElement>('#js-scraper-preview'));
|
||||
submitButton = assertNotNull($<HTMLButtonElement>('.actions > .button'));
|
||||
|
||||
setupImageUpload();
|
||||
fetchMock.resetMocks();
|
||||
|
@ -193,4 +213,42 @@ describe('Image upload form', () => {
|
|||
expect(scraperError.innerText).toEqual('Error 1 Error 2');
|
||||
});
|
||||
});
|
||||
|
||||
async function submitForm(frm): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
function onSubmit() {
|
||||
frm.removeEventListener('submit', onSubmit);
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
frm.addEventListener('submit', onSubmit);
|
||||
|
||||
if (!fireEvent.submit(frm)) {
|
||||
frm.removeEventListener('submit', onSubmit);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it('should prevent form submission if tag checks fail', async () => {
|
||||
for (let i = 0; i < tagSets.length; i += 1) {
|
||||
taginputEl.value = tagSets[i];
|
||||
|
||||
if (await submitForm(form)) {
|
||||
// form submit succeeded
|
||||
await waitFor(() => {
|
||||
assertSubmitButtonIsDisabled();
|
||||
const succeededUnloadEvent = new Event('beforeunload', { cancelable: true });
|
||||
expect(fireEvent(window, succeededUnloadEvent)).toBe(true);
|
||||
});
|
||||
} else {
|
||||
// form submit prevented
|
||||
frm = form;
|
||||
await waitFor(() => {
|
||||
assertSubmitButtonIsEnabled();
|
||||
expect(frm.querySelectorAll('.help-block')).toHaveLength(tagErrorCounts[i]);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* Fetch and display preview images for various image upload forms.
|
||||
*/
|
||||
|
||||
import { assertNotNull } from './utils/assert';
|
||||
import { fetchJson, handleError } from './utils/requests';
|
||||
import { $, $$, clearEl, hideEl, makeEl, showEl } from './utils/dom';
|
||||
import { addTag } from './tagsinput';
|
||||
|
@ -171,9 +172,98 @@ function setupImageUpload() {
|
|||
window.removeEventListener('beforeunload', beforeUnload);
|
||||
}
|
||||
|
||||
function createTagError(message) {
|
||||
const buttonAfter = $('#tagsinput-save');
|
||||
const errorElement = makeEl('span', { className: 'help-block tag-error', innerText: message });
|
||||
|
||||
buttonAfter.insertAdjacentElement('beforebegin', errorElement);
|
||||
}
|
||||
|
||||
function clearTagErrors() {
|
||||
$$('.tag-error').forEach(el => el.remove());
|
||||
}
|
||||
|
||||
const ratingsTags = ['safe', 'suggestive', 'questionable', 'explicit', 'semi-grimdark', 'grimdark', 'grotesque'];
|
||||
|
||||
// populate tag error helper bars as necessary
|
||||
// return true if all checks pass
|
||||
// return false if any check fails
|
||||
function validateTags() {
|
||||
const tagInput = $('textarea.js-taginput');
|
||||
|
||||
if (!tagInput) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tagsArr = tagInput.value.split(',').map(t => t.trim());
|
||||
|
||||
const errors = [];
|
||||
|
||||
let hasRating = false;
|
||||
let hasSafe = false;
|
||||
let hasOtherRating = false;
|
||||
|
||||
tagsArr.forEach(tag => {
|
||||
if (ratingsTags.includes(tag)) {
|
||||
hasRating = true;
|
||||
if (tag === 'safe') {
|
||||
hasSafe = true;
|
||||
} else {
|
||||
hasOtherRating = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasRating) {
|
||||
errors.push('Tag input must contain at least one rating tag');
|
||||
} else if (hasSafe && hasOtherRating) {
|
||||
errors.push('Tag input may not contain any other rating if safe');
|
||||
}
|
||||
|
||||
if (tagsArr.length < 3) {
|
||||
errors.push('Tag input must contain at least 3 tags');
|
||||
}
|
||||
|
||||
errors.forEach(msg => createTagError(msg));
|
||||
|
||||
return errors.length === 0; // true: valid if no errors
|
||||
}
|
||||
|
||||
function disableUploadButton() {
|
||||
const submitButton = $('.button.input--separate-top');
|
||||
if (submitButton !== null) {
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerText = 'Please wait...';
|
||||
}
|
||||
|
||||
// delay is needed because Safari stops the submit if the button is immediately disabled
|
||||
requestAnimationFrame(() => submitButton.setAttribute('disabled', 'disabled'));
|
||||
}
|
||||
|
||||
function submitHandler(event) {
|
||||
// Remove any existing tag error elements
|
||||
clearTagErrors();
|
||||
|
||||
if (validateTags()) {
|
||||
// Disable navigation check
|
||||
unregisterBeforeUnload();
|
||||
|
||||
// Prevent duplicate attempts to submit the form
|
||||
disableUploadButton();
|
||||
|
||||
// Let the form submission complete
|
||||
} else {
|
||||
// Scroll to view validation errors
|
||||
assertNotNull($('.fancy-tag-upload')).scrollIntoView();
|
||||
|
||||
// Prevent the form from being submitted
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
fileField.addEventListener('change', registerBeforeUnload);
|
||||
fetchButton.addEventListener('click', registerBeforeUnload);
|
||||
form.addEventListener('submit', unregisterBeforeUnload);
|
||||
form.addEventListener('submit', submitHandler);
|
||||
}
|
||||
|
||||
export { setupImageUpload };
|
||||
|
|
|
@ -88,4 +88,4 @@
|
|||
= render PhilomenaWeb.CaptchaView, "_captcha.html", name: "image", conn: @conn
|
||||
|
||||
.actions
|
||||
= submit "Upload", class: "button input--separate-top", autocomplete: "off", data: [disable_with: "Please wait..."]
|
||||
= submit "Upload", class: "button input--separate-top", autocomplete: "off"
|
||||
|
|
Loading…
Reference in a new issue