import { $, $$, removeEl } from '../utils/dom'; import { assertNotNull, assertNotUndefined } from '../utils/assert'; import { fetchMock } from '../../test/fetch-mock'; import { fixEventListeners } from '../../test/fix-event-listeners'; import { fireEvent, waitFor } from '@testing-library/dom'; import { promises } from 'fs'; import { join } from 'path'; import { setupImageUpload } from '../upload'; /* eslint-disable camelcase */ const scrapeResponse = { description: 'test', images: [ { url: 'http://localhost/images/1', camo_url: 'http://localhost/images/1' }, { url: 'http://localhost/images/2', camo_url: 'http://localhost/images/2' }, ], source_url: 'http://localhost/images', author_name: 'test', }; const nullResponse = null; const errorResponse = { errors: ['Error 1', 'Error 2'], }; /* 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; beforeAll(async () => { const mockPngPath = join(__dirname, 'upload-test.png'); const mockWebmPath = join(__dirname, 'upload-test.webm'); mockPng = new File([(await promises.readFile(mockPngPath, { encoding: null })).buffer], 'upload-test.png', { type: 'image/png', }); mockWebm = new File([(await promises.readFile(mockWebmPath, { encoding: null })).buffer], 'upload-test.webm', { type: 'video/webm', }); }); beforeAll(() => { fetchMock.enableMocks(); }); afterAll(() => { fetchMock.disableMocks(); }); fixEventListeners(window); let form: HTMLFormElement; let imgPreviews: HTMLDivElement; let fileField: HTMLInputElement; let remoteUrl: HTMLInputElement; 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', `<form action="/images"> <div id="js-image-upload-previews"></div> <input id="image_image" name="image[image]" type="file" class="js-scraper" /> <input id="image_scraper_url" name="image[scraper_url]" type="url" class="js-scraper" /> <button id="js-scraper-preview" type="button">Fetch</button> <div class="field-error-js hidden js-scraper"></div> <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>`, ); form = assertNotNull($<HTMLFormElement>('form')); imgPreviews = assertNotNull($<HTMLDivElement>('#js-image-upload-previews')); fileField = assertNotUndefined($$<HTMLInputElement>('.js-scraper')[0]); 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(); }); afterEach(() => { removeEl(form); }); it('should disable fetch button on empty source', () => { fireEvent.input(remoteUrl, { target: { value: '' } }); expect(fetchButton.disabled).toBe(true); }); it('should enable fetch button on non-empty source', () => { fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } }); expect(fetchButton.disabled).toBe(false); }); it('should create a preview element when an image file is uploaded', () => { fireEvent.change(fileField, { target: { files: [mockPng] } }); return waitFor(() => { assertFetchButtonIsDisabled(); expect(imgPreviews.querySelectorAll('img')).toHaveLength(1); }); }); it('should create a preview element when a Matroska video file is uploaded', () => { fireEvent.change(fileField, { target: { files: [mockWebm] } }); return waitFor(() => { assertFetchButtonIsDisabled(); expect(imgPreviews.querySelectorAll('video')).toHaveLength(1); }); }); it('should block navigation away after an image file is attached, but not after form submission', async () => { fireEvent.change(fileField, { target: { files: [mockPng] } }); await waitFor(() => { assertFetchButtonIsDisabled(); expect(imgPreviews.querySelectorAll('img')).toHaveLength(1); }); const failedUnloadEvent = new Event('beforeunload', { cancelable: true }); expect(fireEvent(window, failedUnloadEvent)).toBe(false); await new Promise<void>(resolve => { form.addEventListener('submit', event => { event.preventDefault(); resolve(); }); fireEvent.submit(form); }); const succeededUnloadEvent = new Event('beforeunload', { cancelable: true }); expect(fireEvent(window, succeededUnloadEvent)).toBe(true); }); it('should scrape images when the fetch button is clicked', async () => { fetchMock.mockResolvedValue(new Response(JSON.stringify(scrapeResponse), { status: 200 })); fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } }); await new Promise<void>(resolve => { tagsEl.addEventListener('addtag', (event: Event) => { expect((event as CustomEvent).detail).toEqual({ name: 'artist:test' }); resolve(); }); fireEvent.keyDown(remoteUrl, { keyCode: 13 }); }); await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1)); await waitFor(() => expect(imgPreviews.querySelectorAll('img')).toHaveLength(2)); expect(scraperError.innerHTML).toEqual(''); expect(sourceEl.value).toEqual('http://localhost/images'); expect(descrEl.value).toEqual('test'); }); it('should show null scrape result', () => { fetchMock.mockResolvedValue(new Response(JSON.stringify(nullResponse), { status: 200 })); fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } }); fireEvent.click(fetchButton); return waitFor(() => { expect(fetch).toHaveBeenCalledTimes(1); expect(imgPreviews.querySelectorAll('img')).toHaveLength(0); expect(scraperError.innerText).toEqual('No image found at that address.'); }); }); it('should show error scrape result', () => { fetchMock.mockResolvedValue(new Response(JSON.stringify(errorResponse), { status: 200 })); fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } }); fireEvent.click(fetchButton); return waitFor(() => { expect(fetch).toHaveBeenCalledTimes(1); expect(imgPreviews.querySelectorAll('img')).toHaveLength(0); expect(scraperError.innerText).toEqual('Error 1 Error 2'); }); }); async function submitForm(frm: HTMLFormElement): 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.innerText = 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 const frm = form; await waitFor(() => { assertSubmitButtonIsEnabled(); expect(frm.querySelectorAll('.help-block')).toHaveLength(tagErrorCounts[i]); }); } } }); });