Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
liamwhite committed Apr 23, 2024
2 parents 63ee35d + df2e336 commit 69d36dc
Show file tree
Hide file tree
Showing 11 changed files with 392 additions and 103 deletions.
13 changes: 13 additions & 0 deletions assets/fix-jsdom.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion assets/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
91 changes: 91 additions & 0 deletions assets/js/__tests__/input-duplicator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { inputDuplicatorCreator } from '../input-duplicator';
import { assertNotNull } from '../utils/assert';
import { $, $$, removeEl } from '../utils/dom';

describe('Input duplicator functionality', () => {
beforeEach(() => {
document.documentElement.insertAdjacentHTML('beforeend', `<form action="/">
<div class="js-max-input-count">3</div>
<div class="js-input-source">
<input id="0" name="0" class="js-input" type="text"/>
<label>
<a href="#" class="js-remove-input">Delete</a>
</label>
</div>
<div class="js-button-container">
<button type="button" class="js-add-input">Add input</button>
</div>
</form>`);
});

afterEach(() => {
removeEl($$<HTMLFormElement>('form'));
});

function runCreator() {
inputDuplicatorCreator({
addButtonSelector: '.js-add-input',
fieldSelector: '.js-input-source',
maxInputCountSelector: '.js-max-input-count',
removeButtonSelector: '.js-remove-input',
});
}

it('should ignore forms without a duplicator button', () => {
removeEl($$<HTMLButtonElement>('button'));
expect(runCreator()).toBeUndefined();
});

it('should duplicate the input elements', () => {
runCreator();

expect($$('input')).toHaveLength(1);

assertNotNull($<HTMLButtonElement>('.js-add-input')).click();

expect($$('input')).toHaveLength(2);
});

it('should duplicate the input elements when the button is before the inputs', () => {
const form = assertNotNull($<HTMLFormElement>('form'));
const buttonDiv = assertNotNull($<HTMLDivElement>('.js-button-container'));
removeEl(buttonDiv);
form.insertAdjacentElement('afterbegin', buttonDiv);
runCreator();

assertNotNull($<HTMLButtonElement>('.js-add-input')).click();

expect($$('input')).toHaveLength(2);
});

it('should not create more input elements than the limit', () => {
runCreator();

for (let i = 0; i < 5; i += 1) {
assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
}

expect($$('input')).toHaveLength(3);
});

it('should remove duplicated input elements', () => {
runCreator();

assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();

expect($$('input')).toHaveLength(1);
});

it('should not remove the last input element', () => {
runCreator();

assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
for (let i = 0; i < 5; i += 1) {
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
}

expect($$('input')).toHaveLength(1);
});
});
14 changes: 3 additions & 11 deletions assets/js/__tests__/ujs.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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));
});
});
});
Expand Down
Binary file added assets/js/__tests__/upload-test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/js/__tests__/upload-test.webm
Binary file not shown.
178 changes: 178 additions & 0 deletions assets/js/__tests__/upload.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
Loading

0 comments on commit 69d36dc

Please sign in to comment.