2024-04-23 00:43:36 +02:00
|
|
|
import { $, $$, removeEl } from '../utils/dom';
|
|
|
|
import { assertNotNull, assertNotUndefined } from '../utils/assert';
|
|
|
|
|
2024-04-30 20:44:26 +02:00
|
|
|
import { fetchMock } from '../../test/fetch-mock';
|
2024-04-23 00:43:36 +02:00
|
|
|
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: [
|
2024-04-30 20:44:26 +02:00
|
|
|
{ url: 'http://localhost/images/1', camo_url: 'http://localhost/images/1' },
|
|
|
|
{ url: 'http://localhost/images/2', camo_url: 'http://localhost/images/2' },
|
2024-04-23 00:43:36 +02:00
|
|
|
],
|
|
|
|
source_url: 'http://localhost/images',
|
|
|
|
author_name: 'test',
|
|
|
|
};
|
|
|
|
const nullResponse = null;
|
|
|
|
const errorResponse = {
|
|
|
|
errors: ['Error 1', 'Error 2'],
|
|
|
|
};
|
|
|
|
/* eslint-enable camelcase */
|
|
|
|
|
2024-08-28 02:02:23 +02:00
|
|
|
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];
|
2024-08-28 01:29:41 +02:00
|
|
|
|
2024-04-23 00:43:36 +02:00
|
|
|
describe('Image upload form', () => {
|
|
|
|
let mockPng: File;
|
|
|
|
let mockWebm: File;
|
|
|
|
|
2024-07-04 02:27:59 +02:00
|
|
|
beforeAll(async () => {
|
2024-04-23 00:43:36 +02:00
|
|
|
const mockPngPath = join(__dirname, 'upload-test.png');
|
|
|
|
const mockWebmPath = join(__dirname, 'upload-test.webm');
|
|
|
|
|
2024-07-04 02:27:59 +02:00
|
|
|
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',
|
|
|
|
});
|
2024-04-23 00:43:36 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
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;
|
2024-08-28 00:24:03 +02:00
|
|
|
let taginputEl: HTMLDivElement;
|
2024-04-23 00:43:36 +02:00
|
|
|
let sourceEl: HTMLInputElement;
|
|
|
|
let descrEl: HTMLTextAreaElement;
|
2024-08-28 00:24:03 +02:00
|
|
|
let submitButton: HTMLButtonElement;
|
2024-04-23 00:43:36 +02:00
|
|
|
|
2024-04-30 20:44:26 +02:00
|
|
|
const assertFetchButtonIsDisabled = () => {
|
|
|
|
if (!fetchButton.hasAttribute('disabled')) throw new Error('fetchButton is not disabled');
|
|
|
|
};
|
|
|
|
|
2024-08-28 00:38:14 +02:00
|
|
|
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');
|
|
|
|
};
|
|
|
|
|
2024-04-23 00:43:36 +02:00
|
|
|
beforeEach(() => {
|
2024-07-04 02:27:59 +02:00
|
|
|
document.documentElement.insertAdjacentHTML(
|
|
|
|
'beforeend',
|
|
|
|
`<form action="/images">
|
2024-04-23 00:43:36 +02:00
|
|
|
<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>
|
2024-08-28 01:29:41 +02:00
|
|
|
<div class="js-taginput"></div>
|
2024-08-28 00:52:11 +02:00
|
|
|
<button id="tagsinput-save" type="button" class="button">Save</button>
|
2024-04-23 00:43:36 +02:00
|
|
|
<textarea id="image_description" name="image[description]" class="js-image-descr-input"></textarea>
|
2024-08-28 00:24:03 +02:00
|
|
|
<div class="actions">
|
2024-08-28 01:16:13 +02:00
|
|
|
<button class="button input--separate-top" type="submit">Upload</button>
|
2024-08-28 00:24:03 +02:00
|
|
|
</div>
|
2024-07-04 02:27:59 +02:00
|
|
|
</form>`,
|
|
|
|
);
|
2024-04-23 00:43:36 +02:00
|
|
|
|
|
|
|
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'));
|
2024-08-28 00:24:03 +02:00
|
|
|
taginputEl = assertNotNull($<HTMLDivElement>('.js-taginput'));
|
2024-04-23 00:43:36 +02:00
|
|
|
sourceEl = assertNotNull($<HTMLInputElement>('.js-source-url'));
|
|
|
|
descrEl = assertNotNull($<HTMLTextAreaElement>('.js-image-descr-input'));
|
|
|
|
fetchButton = assertNotNull($<HTMLButtonElement>('#js-scraper-preview'));
|
2024-08-28 00:38:14 +02:00
|
|
|
submitButton = assertNotNull($<HTMLButtonElement>('.actions > .button'));
|
2024-04-23 00:43:36 +02:00
|
|
|
|
|
|
|
setupImageUpload();
|
|
|
|
fetchMock.resetMocks();
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
removeEl(form);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should disable fetch button on empty source', () => {
|
2024-04-30 20:44:26 +02:00
|
|
|
fireEvent.input(remoteUrl, { target: { value: '' } });
|
2024-04-23 00:43:36 +02:00
|
|
|
expect(fetchButton.disabled).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should enable fetch button on non-empty source', () => {
|
2024-04-30 20:44:26 +02:00
|
|
|
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
|
2024-04-23 00:43:36 +02:00
|
|
|
expect(fetchButton.disabled).toBe(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should create a preview element when an image file is uploaded', () => {
|
2024-04-30 20:44:26 +02:00
|
|
|
fireEvent.change(fileField, { target: { files: [mockPng] } });
|
|
|
|
return waitFor(() => {
|
|
|
|
assertFetchButtonIsDisabled();
|
|
|
|
expect(imgPreviews.querySelectorAll('img')).toHaveLength(1);
|
|
|
|
});
|
2024-04-23 00:43:36 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should create a preview element when a Matroska video file is uploaded', () => {
|
2024-04-30 20:44:26 +02:00
|
|
|
fireEvent.change(fileField, { target: { files: [mockWebm] } });
|
|
|
|
return waitFor(() => {
|
|
|
|
assertFetchButtonIsDisabled();
|
|
|
|
expect(imgPreviews.querySelectorAll('video')).toHaveLength(1);
|
|
|
|
});
|
2024-04-23 00:43:36 +02:00
|
|
|
});
|
|
|
|
|
2024-07-04 02:27:59 +02:00
|
|
|
it('should block navigation away after an image file is attached, but not after form submission', async () => {
|
2024-04-30 20:44:26 +02:00
|
|
|
fireEvent.change(fileField, { target: { files: [mockPng] } });
|
|
|
|
await waitFor(() => {
|
|
|
|
assertFetchButtonIsDisabled();
|
|
|
|
expect(imgPreviews.querySelectorAll('img')).toHaveLength(1);
|
|
|
|
});
|
2024-04-23 00:43:36 +02:00
|
|
|
|
|
|
|
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();
|
|
|
|
});
|
2024-04-30 20:44:26 +02:00
|
|
|
fireEvent.submit(form);
|
2024-04-23 00:43:36 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
const succeededUnloadEvent = new Event('beforeunload', { cancelable: true });
|
|
|
|
expect(fireEvent(window, succeededUnloadEvent)).toBe(true);
|
|
|
|
});
|
|
|
|
|
2024-07-04 02:27:59 +02:00
|
|
|
it('should scrape images when the fetch button is clicked', async () => {
|
2024-04-23 00:43:36 +02:00
|
|
|
fetchMock.mockResolvedValue(new Response(JSON.stringify(scrapeResponse), { status: 200 }));
|
2024-04-30 20:44:26 +02:00
|
|
|
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
|
2024-04-23 00:43:36 +02:00
|
|
|
|
|
|
|
await new Promise<void>(resolve => {
|
|
|
|
tagsEl.addEventListener('addtag', (event: Event) => {
|
2024-04-30 20:44:26 +02:00
|
|
|
expect((event as CustomEvent).detail).toEqual({ name: 'artist:test' });
|
2024-04-23 00:43:36 +02:00
|
|
|
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 }));
|
|
|
|
|
2024-04-30 20:44:26 +02:00
|
|
|
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
|
|
|
|
fireEvent.click(fetchButton);
|
2024-04-23 00:43:36 +02:00
|
|
|
|
|
|
|
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 }));
|
|
|
|
|
2024-04-30 20:44:26 +02:00
|
|
|
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
|
|
|
|
fireEvent.click(fetchButton);
|
2024-04-23 00:43:36 +02:00
|
|
|
|
|
|
|
return waitFor(() => {
|
|
|
|
expect(fetch).toHaveBeenCalledTimes(1);
|
|
|
|
expect(imgPreviews.querySelectorAll('img')).toHaveLength(0);
|
|
|
|
expect(scraperError.innerText).toEqual('Error 1 Error 2');
|
|
|
|
});
|
|
|
|
});
|
2024-08-28 01:16:13 +02:00
|
|
|
|
2024-09-10 16:54:17 +02:00
|
|
|
async function submitForm(frm: HTMLFormElement): Promise<boolean> {
|
2024-08-28 02:02:23 +02:00
|
|
|
return new Promise(resolve => {
|
|
|
|
function onSubmit() {
|
|
|
|
frm.removeEventListener('submit', onSubmit);
|
|
|
|
resolve(true);
|
|
|
|
}
|
2024-08-28 01:29:41 +02:00
|
|
|
|
2024-08-28 02:02:23 +02:00
|
|
|
frm.addEventListener('submit', onSubmit);
|
2024-08-28 01:16:13 +02:00
|
|
|
|
2024-08-28 02:02:23 +02:00
|
|
|
if (!fireEvent.submit(frm)) {
|
|
|
|
frm.removeEventListener('submit', onSubmit);
|
|
|
|
resolve(false);
|
|
|
|
}
|
2024-08-28 01:16:13 +02:00
|
|
|
});
|
2024-08-28 02:02:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
it('should prevent form submission if tag checks fail', async () => {
|
|
|
|
for (let i = 0; i < tagSets.length; i += 1) {
|
2024-09-10 16:54:17 +02:00
|
|
|
taginputEl.innerText = tagSets[i];
|
2024-08-28 02:02:23 +02:00
|
|
|
|
|
|
|
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
|
2024-09-10 16:54:17 +02:00
|
|
|
const frm = form;
|
2024-08-28 02:02:23 +02:00
|
|
|
await waitFor(() => {
|
|
|
|
assertSubmitButtonIsEnabled();
|
|
|
|
expect(frm.querySelectorAll('.help-block')).toHaveLength(tagErrorCounts[i]);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2024-08-28 01:16:13 +02:00
|
|
|
});
|
2024-04-23 00:43:36 +02:00
|
|
|
});
|