diff --git a/assets/js/__tests__/upload-test.png b/assets/js/__tests__/upload-test.png
new file mode 100644
index 000000000..770601f79
Binary files /dev/null and b/assets/js/__tests__/upload-test.png differ
diff --git a/assets/js/__tests__/upload-test.webm b/assets/js/__tests__/upload-test.webm
new file mode 100644
index 000000000..12442b6a3
Binary files /dev/null and b/assets/js/__tests__/upload-test.webm differ
diff --git a/assets/js/__tests__/upload.spec.ts b/assets/js/__tests__/upload.spec.ts
new file mode 100644
index 000000000..e9c5a5f07
--- /dev/null
+++ b/assets/js/__tests__/upload.spec.ts
@@ -0,0 +1,83 @@
+import { $, $$, clearEl } from '../utils/dom';
+import { setupImageUpload } from '../upload';
+import { assertNotNull, assertNotUndefined } from '../utils/assert';
+import { promises } from 'fs';
+import { join } from 'path';
+import { fireEvent } from '@testing-library/dom';
+
+describe('Image upload form', () => {
+ let form: HTMLFormElement;
+ let imgPreviews: HTMLDivElement;
+ let fileField: HTMLInputElement;
+ let remoteUrl: HTMLInputElement;
+ let scraperError: HTMLDivElement;
+ let fetchButton: HTMLButtonElement;
+ let tagsEl: HTMLTextAreaElement;
+ let sourceEl: HTMLInputElement;
+ let descrEl: HTMLTextAreaElement;
+
+ beforeAll(() => {
+ document.documentElement.insertAdjacentHTML('beforeend', `
+
+ `);
+
+ form = assertNotNull($('form'));
+ imgPreviews = assertNotNull($('#js-image-upload-previews'));
+ fileField = assertNotUndefined($$('.js-scraper')[0]);
+ remoteUrl = assertNotUndefined($$('.js-scraper')[1]);
+ scraperError = assertNotUndefined($$('.js-scraper')[2]);
+ tagsEl = assertNotNull($('.js-image-tags-input'));
+ sourceEl = assertNotNull($('.js-source-url'));
+ descrEl = assertNotNull($('.js-image-descr-input'));
+ fetchButton = assertNotNull($('#js-scraper-preview'));
+
+ setupImageUpload();
+ });
+
+ 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' });
+ });
+
+ beforeEach(() => {
+ // Reset the state of everything between runs
+ clearEl(imgPreviews);
+ form.reset();
+ });
+
+ 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] }});
+ expect(imgPreviews.querySelectorAll('img').length).toBe(1);
+ });
+
+ it('should create a preview element when a Matroska video file is uploaded', () => {
+ fireEvent.change(fileField, { target: { files: [mockWebm] }});
+ expect(imgPreviews.querySelectorAll('video').length).toBe(1);
+ });
+});