mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
upload: add pinning test (#231)
This commit is contained in:
parent
88a1131f35
commit
df2e336a24
8 changed files with 225 additions and 20 deletions
13
assets/fix-jsdom.ts
Normal file
13
assets/fix-jsdom.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import JSDOMEnvironment from 'jest-environment-jsdom';
|
||||
|
||||
export default class FixJSDOMEnvironment extends JSDOMEnvironment {
|
||||
constructor(...args: ConstructorParameters<typeof JSDOMEnvironment>) {
|
||||
super(...args);
|
||||
|
||||
// https://github.com/jsdom/jsdom/issues/1721#issuecomment-1484202038
|
||||
// jsdom URL and Blob are missing most of the implementation
|
||||
// Use the node version of these types instead
|
||||
this.global.URL = URL;
|
||||
this.global.Blob = Blob;
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ export default {
|
|||
},
|
||||
preset: 'ts-jest/presets/js-with-ts-esm',
|
||||
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
|
||||
testEnvironment: 'jsdom',
|
||||
testEnvironment: './fix-jsdom.ts',
|
||||
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
|
||||
moduleNameMapper: {
|
||||
'./js/(.*)': '<rootDir>/js/$1',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import fetchMock from 'jest-fetch-mock';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import { fireEvent, waitFor } from '@testing-library/dom';
|
||||
import { assertType } from '../utils/assert';
|
||||
import '../ujs';
|
||||
|
||||
|
@ -199,18 +199,10 @@ describe('Remote utilities', () => {
|
|||
}));
|
||||
|
||||
it('should reload the page on 300 multiple choices response', () => {
|
||||
const promiseLike = {
|
||||
then(cb: (r: Response) => void) {
|
||||
if (cb) {
|
||||
cb(new Response('', { status: 300 }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
jest.spyOn(global, 'fetch').mockReturnValue(promiseLike as any);
|
||||
jest.spyOn(global, 'fetch').mockResolvedValue(new Response('', { status: 300}));
|
||||
|
||||
submitForm();
|
||||
expect(window.location.reload).toHaveBeenCalledTimes(1);
|
||||
return waitFor(() => expect(window.location.reload).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
BIN
assets/js/__tests__/upload-test.png
Normal file
BIN
assets/js/__tests__/upload-test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 527 B |
BIN
assets/js/__tests__/upload-test.webm
Normal file
BIN
assets/js/__tests__/upload-test.webm
Normal file
Binary file not shown.
178
assets/js/__tests__/upload.spec.ts
Normal file
178
assets/js/__tests__/upload.spec.ts
Normal file
|
@ -0,0 +1,178 @@
|
|||
import { $, $$, removeEl } from '../utils/dom';
|
||||
import { assertNotNull, assertNotUndefined } from '../utils/assert';
|
||||
|
||||
import fetchMock from 'jest-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 */
|
||||
|
||||
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 sourceEl: HTMLInputElement;
|
||||
let descrEl: HTMLTextAreaElement;
|
||||
|
||||
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>
|
||||
<textarea id="image_description" name="image[description]" class="js-image-descr-input"></textarea>
|
||||
</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'));
|
||||
sourceEl = assertNotNull($<HTMLInputElement>('.js-source-url'));
|
||||
descrEl = assertNotNull($<HTMLTextAreaElement>('.js-image-descr-input'));
|
||||
fetchButton = assertNotNull($<HTMLButtonElement>('#js-scraper-preview'));
|
||||
|
||||
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(() => 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(() => 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(() => { 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();
|
||||
});
|
||||
form.submit();
|
||||
});
|
||||
|
||||
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' }});
|
||||
fetchButton.click();
|
||||
|
||||
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' }});
|
||||
fetchButton.click();
|
||||
|
||||
return waitFor(() => {
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(imgPreviews.querySelectorAll('img')).toHaveLength(0);
|
||||
expect(scraperError.innerText).toEqual('Error 1 Error 2');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -132,21 +132,17 @@ function setupImageUpload() {
|
|||
});
|
||||
|
||||
// Enable/disable the fetch button based on content in the image scraper. Fetching with no URL makes no sense.
|
||||
remoteUrl.addEventListener('input', () => {
|
||||
function setFetchEnabled() {
|
||||
if (remoteUrl.value.length > 0) {
|
||||
enableFetch();
|
||||
}
|
||||
else {
|
||||
disableFetch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (remoteUrl.value.length > 0) {
|
||||
enableFetch();
|
||||
}
|
||||
else {
|
||||
disableFetch();
|
||||
}
|
||||
remoteUrl.addEventListener('input', () => setFetchEnabled());
|
||||
setFetchEnabled();
|
||||
|
||||
// Catch unintentional navigation away from the page
|
||||
|
||||
|
|
26
assets/test/fix-event-listeners.ts
Normal file
26
assets/test/fix-event-listeners.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Add helper to fix event listeners on a given target
|
||||
|
||||
export function fixEventListeners(t: EventTarget) {
|
||||
let eventListeners: Record<string, unknown[]>;
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
beforeAll(() => {
|
||||
eventListeners = {};
|
||||
const oldAddEventListener = t.addEventListener;
|
||||
|
||||
t.addEventListener = function(type: string, listener: any, options: any): void {
|
||||
eventListeners[type] = eventListeners[type] || [];
|
||||
eventListeners[type].push(listener);
|
||||
return oldAddEventListener(type, listener, options);
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const key in eventListeners) {
|
||||
for (const listener of eventListeners[key]) {
|
||||
(t.removeEventListener as any)(key, listener);
|
||||
}
|
||||
}
|
||||
eventListeners = {};
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue