diff --git a/assets/js/__tests__/upload.spec.ts b/assets/js/__tests__/upload.spec.ts index 06d14d64..bd5d9c5e 100644 --- a/assets/js/__tests__/upload.spec.ts +++ b/assets/js/__tests__/upload.spec.ts @@ -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', () => { +
+ +
+ +
`, ); @@ -87,9 +105,11 @@ describe('Image upload form', () => { remoteUrl = assertNotUndefined($$('.js-scraper')[1]); scraperError = assertNotUndefined($$('.js-scraper')[2]); tagsEl = assertNotNull($('.js-image-tags-input')); + taginputEl = assertNotNull($('.js-taginput')); sourceEl = assertNotNull($('.js-source-url')); descrEl = assertNotNull($('.js-image-descr-input')); fetchButton = assertNotNull($('#js-scraper-preview')); + submitButton = assertNotNull($('.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 { + 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]); + }); + } + } + }); }); diff --git a/assets/js/upload.js b/assets/js/upload.js index 16d33959..0f931037 100644 --- a/assets/js/upload.js +++ b/assets/js/upload.js @@ -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 }; diff --git a/lib/philomena_web/templates/image/new.html.slime b/lib/philomena_web/templates/image/new.html.slime index dfb664d9..2a080eb1 100644 --- a/lib/philomena_web/templates/image/new.html.slime +++ b/lib/philomena_web/templates/image/new.html.slime @@ -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"