Switch jest unit tests to vitest (#243)

* Switch jest unit tests to vitest

* Cleanup vite config after debugging
This commit is contained in:
David Joseph Guzsik 2024-04-30 20:44:26 +02:00 committed by GitHub
parent dd8c2c81d9
commit 2417f40d37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1341 additions and 3203 deletions

2
.gitignore vendored
View file

@ -59,5 +59,5 @@ npm-debug.log
/native/**/target /native/**/target
/.cargo /.cargo
# Jest coverage # Vitest coverage
/assets/coverage /assets/coverage

View file

@ -1,3 +1,2 @@
js/vendor/* js/vendor/*
vite.config.ts vite.config.ts
jest.config.js

View file

@ -10,7 +10,7 @@ parserOptions:
plugins: plugins:
- '@typescript-eslint' - '@typescript-eslint'
- jest - vitest
globals: globals:
ga: false ga: false
@ -276,12 +276,14 @@ overrides:
'@typescript-eslint/no-extra-parens': 2 '@typescript-eslint/no-extra-parens': 2
no-shadow: 0 no-shadow: 0
'@typescript-eslint/no-shadow': 2 '@typescript-eslint/no-shadow': 2
# Jest Tests (also written in TypeScript) # Unit Tests (also written in TypeScript)
# Disable rules that do not make sense in test files (e.g. testing for undefined input values should be allowed) # Disable rules that do not make sense in test files (e.g. testing for undefined input values should be allowed)
- files: - files:
- '*.spec.ts' - '*.spec.ts'
- 'test/*.ts' - 'test/*.ts'
extends: extends:
- 'plugin:jest/recommended' - 'plugin:vitest/legacy-recommended'
rules: rules:
no-undefined: 0 no-undefined: 0
no-unused-expressions: 0
vitest/valid-expect: 0

View file

@ -1,13 +0,0 @@
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;
}
}

View file

@ -1,42 +0,0 @@
export default {
collectCoverage: true,
collectCoverageFrom: [
'js/**/*.{js,ts}',
],
coveragePathIgnorePatterns: [
'/node_modules/',
'/.*\\.test\\.ts$',
'.*\\.d\\.ts$',
],
coverageDirectory: '<rootDir>/coverage/',
coverageThreshold: {
global: {
statements: 0,
branches: 0,
functions: 0,
lines: 0,
},
'./js/utils/**/*.ts': {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
preset: 'ts-jest/presets/js-with-ts-esm',
setupFilesAfterEnv: ['<rootDir>/test/jest-setup.ts'],
testEnvironment: './fix-jsdom.ts',
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
moduleNameMapper: {
'./js/(.*)': '<rootDir>/js/$1',
},
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: '<rootDir>/tsconfig.json',
useESM: true,
}]
},
globals: {
extensionsToTreatAsEsm: ['.ts', '.js'],
}
};

View file

@ -1,6 +1,7 @@
import { inputDuplicatorCreator } from '../input-duplicator'; import { inputDuplicatorCreator } from '../input-duplicator';
import { assertNotNull } from '../utils/assert'; import { assertNotNull } from '../utils/assert';
import { $, $$, removeEl } from '../utils/dom'; import { $, $$, removeEl } from '../utils/dom';
import { fireEvent } from '@testing-library/dom';
describe('Input duplicator functionality', () => { describe('Input duplicator functionality', () => {
beforeEach(() => { beforeEach(() => {
@ -41,7 +42,7 @@ describe('Input duplicator functionality', () => {
expect($$('input')).toHaveLength(1); expect($$('input')).toHaveLength(1);
assertNotNull($<HTMLButtonElement>('.js-add-input')).click(); fireEvent.click(assertNotNull($<HTMLButtonElement>('.js-add-input')));
expect($$('input')).toHaveLength(2); expect($$('input')).toHaveLength(2);
}); });
@ -53,7 +54,7 @@ describe('Input duplicator functionality', () => {
form.insertAdjacentElement('afterbegin', buttonDiv); form.insertAdjacentElement('afterbegin', buttonDiv);
runCreator(); runCreator();
assertNotNull($<HTMLButtonElement>('.js-add-input')).click(); fireEvent.click(assertNotNull($<HTMLButtonElement>('.js-add-input')));
expect($$('input')).toHaveLength(2); expect($$('input')).toHaveLength(2);
}); });
@ -62,7 +63,7 @@ describe('Input duplicator functionality', () => {
runCreator(); runCreator();
for (let i = 0; i < 5; i += 1) { for (let i = 0; i < 5; i += 1) {
assertNotNull($<HTMLButtonElement>('.js-add-input')).click(); fireEvent.click(assertNotNull($<HTMLButtonElement>('.js-add-input')));
} }
expect($$('input')).toHaveLength(3); expect($$('input')).toHaveLength(3);
@ -71,8 +72,8 @@ describe('Input duplicator functionality', () => {
it('should remove duplicated input elements', () => { it('should remove duplicated input elements', () => {
runCreator(); runCreator();
assertNotNull($<HTMLButtonElement>('.js-add-input')).click(); fireEvent.click(assertNotNull($<HTMLButtonElement>('.js-add-input')));
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click(); fireEvent.click(assertNotNull($<HTMLAnchorElement>('.js-remove-input')));
expect($$('input')).toHaveLength(1); expect($$('input')).toHaveLength(1);
}); });
@ -80,10 +81,10 @@ describe('Input duplicator functionality', () => {
it('should not remove the last input element', () => { it('should not remove the last input element', () => {
runCreator(); runCreator();
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click(); fireEvent.click(assertNotNull($<HTMLAnchorElement>('.js-remove-input')));
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click(); fireEvent.click(assertNotNull($<HTMLAnchorElement>('.js-remove-input')));
for (let i = 0; i < 5; i += 1) { for (let i = 0; i < 5; i += 1) {
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click(); fireEvent.click(assertNotNull($<HTMLAnchorElement>('.js-remove-input')));
} }
expect($$('input')).toHaveLength(1); expect($$('input')).toHaveLength(1);

View file

@ -1,7 +1,7 @@
import fetchMock from 'jest-fetch-mock';
import { fireEvent, waitFor } from '@testing-library/dom'; import { fireEvent, waitFor } from '@testing-library/dom';
import { assertType } from '../utils/assert'; import { assertType } from '../utils/assert';
import '../ujs'; import '../ujs';
import { fetchMock } from '../../test/fetch-mock';
const mockEndpoint = 'http://localhost/endpoint'; const mockEndpoint = 'http://localhost/endpoint';
const mockVerb = 'POST'; const mockVerb = 'POST';
@ -38,7 +38,7 @@ describe('Remote utilities', () => {
} }
document.documentElement.insertAdjacentElement('beforeend', a); document.documentElement.insertAdjacentElement('beforeend', a);
a.click(); fireEvent.click(a, { button: 0 });
return a; return a;
}; };
@ -88,7 +88,7 @@ describe('Remote utilities', () => {
a.dataset.method = mockVerb; a.dataset.method = mockVerb;
document.documentElement.insertAdjacentElement('beforeend', a); document.documentElement.insertAdjacentElement('beforeend', a);
a.click(); fireEvent.click(a);
return a; return a;
}; };
@ -128,7 +128,7 @@ describe('Remote utilities', () => {
...Object.getOwnPropertyDescriptors(oldWindowLocation), ...Object.getOwnPropertyDescriptors(oldWindowLocation),
reload: { reload: {
configurable: true, configurable: true,
value: jest.fn(), value: vi.fn(),
}, },
}, },
); );
@ -155,7 +155,7 @@ describe('Remote utilities', () => {
const submitForm = () => { const submitForm = () => {
const form = configureForm(); const form = configureForm();
form.method = mockVerb; form.method = mockVerb;
form.submit(); fireEvent.submit(form);
return form; return form;
}; };
@ -176,7 +176,7 @@ describe('Remote utilities', () => {
it('should submit a PUT request with put data-method specified', () => { it('should submit a PUT request with put data-method specified', () => {
const form = configureForm(); const form = configureForm();
form.dataset.method = 'put'; form.dataset.method = 'put';
form.submit(); fireEvent.submit(form);
expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenNthCalledWith(1, mockEndpoint, { expect(fetch).toHaveBeenNthCalledWith(1, mockEndpoint, {
method: 'PUT', method: 'PUT',
@ -201,7 +201,7 @@ describe('Remote utilities', () => {
})); }));
it('should reload the page on 300 multiple choices response', () => { it('should reload the page on 300 multiple choices response', () => {
jest.spyOn(global, 'fetch').mockResolvedValue(new Response('', { status: 300})); vi.spyOn(global, 'fetch').mockResolvedValue(new Response('', { status: 300}));
submitForm(); submitForm();
return waitFor(() => expect(window.location.reload).toHaveBeenCalledTimes(1)); return waitFor(() => expect(window.location.reload).toHaveBeenCalledTimes(1));
@ -211,28 +211,29 @@ describe('Remote utilities', () => {
describe('Form utilities', () => { describe('Form utilities', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => { vi.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => {
cb(1); cb(1);
return 1; return 1;
}); });
}); });
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); vi.clearAllMocks();
}); });
describe('[data-confirm]', () => { describe('[data-confirm]', () => {
const createA = () => { const createA = () => {
const a = document.createElement('a'); const a = document.createElement('a');
a.dataset.confirm = 'confirm'; a.dataset.confirm = 'confirm';
a.href = mockEndpoint; // We cannot use mockEndpoint here since anything except a hash change will log an error in the test output
a.href = '#hash';
document.documentElement.insertAdjacentElement('beforeend', a); document.documentElement.insertAdjacentElement('beforeend', a);
return a; return a;
}; };
it('should cancel the event on failed confirm', () => { it('should cancel the event on failed confirm', () => {
const a = createA(); const a = createA();
const confirm = jest.spyOn(window, 'confirm').mockImplementationOnce(() => false); const confirm = vi.spyOn(window, 'confirm').mockImplementationOnce(() => false);
const event = new MouseEvent('click', { bubbles: true, cancelable: true }); const event = new MouseEvent('click', { bubbles: true, cancelable: true });
expect(fireEvent(a, event)).toBe(false); expect(fireEvent(a, event)).toBe(false);
@ -241,7 +242,7 @@ describe('Form utilities', () => {
it('should allow the event on confirm', () => { it('should allow the event on confirm', () => {
const a = createA(); const a = createA();
const confirm = jest.spyOn(window, 'confirm').mockImplementationOnce(() => true); const confirm = vi.spyOn(window, 'confirm').mockImplementationOnce(() => true);
const event = new MouseEvent('click', { bubbles: true, cancelable: true }); const event = new MouseEvent('click', { bubbles: true, cancelable: true });
expect(fireEvent(a, event)).toBe(true); expect(fireEvent(a, event)).toBe(true);
@ -276,7 +277,7 @@ describe('Form utilities', () => {
it('should disable submit button containing a text child on click', () => { it('should disable submit button containing a text child on click', () => {
const [ , button ] = createFormAndButton(submitText, loadingText); const [ , button ] = createFormAndButton(submitText, loadingText);
button.click(); fireEvent.click(button);
expect(button.textContent).toEqual(' Loading...'); expect(button.textContent).toEqual(' Loading...');
expect(button.dataset.enableWith).toEqual(submitText); expect(button.dataset.enableWith).toEqual(submitText);
@ -284,7 +285,7 @@ describe('Form utilities', () => {
it('should disable submit button containing element children on click', () => { it('should disable submit button containing element children on click', () => {
const [ , button ] = createFormAndButton(submitMarkup, loadingMarkup); const [ , button ] = createFormAndButton(submitMarkup, loadingMarkup);
button.click(); fireEvent.click(button);
expect(button.innerHTML).toEqual(loadingMarkup); expect(button.innerHTML).toEqual(loadingMarkup);
expect(button.dataset.enableWith).toEqual(submitMarkup); expect(button.dataset.enableWith).toEqual(submitMarkup);
@ -293,7 +294,7 @@ describe('Form utilities', () => {
it('should not disable anything when the form is invalid', () => { it('should not disable anything when the form is invalid', () => {
const [ form, button ] = createFormAndButton(submitText, loadingText); const [ form, button ] = createFormAndButton(submitText, loadingText);
form.insertAdjacentHTML('afterbegin', '<input type="text" name="valid" required="true" />'); form.insertAdjacentHTML('afterbegin', '<input type="text" name="valid" required="true" />');
button.click(); fireEvent.click(button);
expect(button.textContent).toEqual(submitText); expect(button.textContent).toEqual(submitText);
expect(button.dataset.enableWith).not.toBeDefined(); expect(button.dataset.enableWith).not.toBeDefined();
@ -301,7 +302,7 @@ describe('Form utilities', () => {
it('should reset submit button containing a text child on completion', () => { it('should reset submit button containing a text child on completion', () => {
const [ form, button ] = createFormAndButton(submitText, loadingText); const [ form, button ] = createFormAndButton(submitText, loadingText);
button.click(); fireEvent.click(button);
fireEvent(form, new CustomEvent('reset', { bubbles: true })); fireEvent(form, new CustomEvent('reset', { bubbles: true }));
expect(button.textContent?.trim()).toEqual(submitText); expect(button.textContent?.trim()).toEqual(submitText);
@ -310,7 +311,7 @@ describe('Form utilities', () => {
it('should reset submit button containing element children on completion', () => { it('should reset submit button containing element children on completion', () => {
const [ form, button ] = createFormAndButton(submitMarkup, loadingMarkup); const [ form, button ] = createFormAndButton(submitMarkup, loadingMarkup);
button.click(); fireEvent.click(button);
fireEvent(form, new CustomEvent('reset', { bubbles: true })); fireEvent(form, new CustomEvent('reset', { bubbles: true }));
expect(button.innerHTML).toEqual(submitMarkup); expect(button.innerHTML).toEqual(submitMarkup);
@ -319,7 +320,7 @@ describe('Form utilities', () => {
it('should reset disabled form elements on pageshow', () => { it('should reset disabled form elements on pageshow', () => {
const [ , button ] = createFormAndButton(submitText, loadingText); const [ , button ] = createFormAndButton(submitText, loadingText);
button.click(); fireEvent.click(button);
fireEvent(window, new CustomEvent('pageshow')); fireEvent(window, new CustomEvent('pageshow'));
expect(button.textContent?.trim()).toEqual(submitText); expect(button.textContent?.trim()).toEqual(submitText);

View file

@ -1,7 +1,7 @@
import { $, $$, removeEl } from '../utils/dom'; import { $, $$, removeEl } from '../utils/dom';
import { assertNotNull, assertNotUndefined } from '../utils/assert'; import { assertNotNull, assertNotUndefined } from '../utils/assert';
import fetchMock from 'jest-fetch-mock'; import { fetchMock } from '../../test/fetch-mock';
import { fixEventListeners } from '../../test/fix-event-listeners'; import { fixEventListeners } from '../../test/fix-event-listeners';
import { fireEvent, waitFor } from '@testing-library/dom'; import { fireEvent, waitFor } from '@testing-library/dom';
import { promises } from 'fs'; import { promises } from 'fs';
@ -13,8 +13,8 @@ import { setupImageUpload } from '../upload';
const scrapeResponse = { const scrapeResponse = {
description: 'test', description: 'test',
images: [ images: [
{url: 'http://localhost/images/1', camo_url: 'http://localhost/images/1'}, { url: 'http://localhost/images/1', camo_url: 'http://localhost/images/1' },
{url: 'http://localhost/images/2', camo_url: 'http://localhost/images/2'}, { url: 'http://localhost/images/2', camo_url: 'http://localhost/images/2' },
], ],
source_url: 'http://localhost/images', source_url: 'http://localhost/images',
author_name: 'test', author_name: 'test',
@ -47,6 +47,7 @@ describe('Image upload form', () => {
fixEventListeners(window); fixEventListeners(window);
let form: HTMLFormElement; let form: HTMLFormElement;
let imgPreviews: HTMLDivElement; let imgPreviews: HTMLDivElement;
let fileField: HTMLInputElement; let fileField: HTMLInputElement;
@ -57,6 +58,10 @@ describe('Image upload form', () => {
let sourceEl: HTMLInputElement; let sourceEl: HTMLInputElement;
let descrEl: HTMLTextAreaElement; let descrEl: HTMLTextAreaElement;
const assertFetchButtonIsDisabled = () => {
if (!fetchButton.hasAttribute('disabled')) throw new Error('fetchButton is not disabled');
};
beforeEach(() => { beforeEach(() => {
document.documentElement.insertAdjacentHTML('beforeend', ` document.documentElement.insertAdjacentHTML('beforeend', `
<form action="/images"> <form action="/images">
@ -91,28 +96,37 @@ describe('Image upload form', () => {
}); });
it('should disable fetch button on empty source', () => { it('should disable fetch button on empty source', () => {
fireEvent.input(remoteUrl, { target: { value: '' }}); fireEvent.input(remoteUrl, { target: { value: '' } });
expect(fetchButton.disabled).toBe(true); expect(fetchButton.disabled).toBe(true);
}); });
it('should enable fetch button on non-empty source', () => { it('should enable fetch button on non-empty source', () => {
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' }}); fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
expect(fetchButton.disabled).toBe(false); expect(fetchButton.disabled).toBe(false);
}); });
it('should create a preview element when an image file is uploaded', () => { it('should create a preview element when an image file is uploaded', () => {
fireEvent.change(fileField, { target: { files: [mockPng] }}); fireEvent.change(fileField, { target: { files: [mockPng] } });
return waitFor(() => expect(imgPreviews.querySelectorAll('img')).toHaveLength(1)); return waitFor(() => {
assertFetchButtonIsDisabled();
expect(imgPreviews.querySelectorAll('img')).toHaveLength(1);
});
}); });
it('should create a preview element when a Matroska video file is uploaded', () => { it('should create a preview element when a Matroska video file is uploaded', () => {
fireEvent.change(fileField, { target: { files: [mockWebm] }}); fireEvent.change(fileField, { target: { files: [mockWebm] } });
return waitFor(() => expect(imgPreviews.querySelectorAll('video')).toHaveLength(1)); 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() => { it('should block navigation away after an image file is attached, but not after form submission', async() => {
fireEvent.change(fileField, { target: { files: [mockPng] }}); fireEvent.change(fileField, { target: { files: [mockPng] } });
await waitFor(() => { expect(imgPreviews.querySelectorAll('img')).toHaveLength(1); }); await waitFor(() => {
assertFetchButtonIsDisabled();
expect(imgPreviews.querySelectorAll('img')).toHaveLength(1);
});
const failedUnloadEvent = new Event('beforeunload', { cancelable: true }); const failedUnloadEvent = new Event('beforeunload', { cancelable: true });
expect(fireEvent(window, failedUnloadEvent)).toBe(false); expect(fireEvent(window, failedUnloadEvent)).toBe(false);
@ -122,7 +136,7 @@ describe('Image upload form', () => {
event.preventDefault(); event.preventDefault();
resolve(); resolve();
}); });
form.submit(); fireEvent.submit(form);
}); });
const succeededUnloadEvent = new Event('beforeunload', { cancelable: true }); const succeededUnloadEvent = new Event('beforeunload', { cancelable: true });
@ -131,11 +145,11 @@ describe('Image upload form', () => {
it('should scrape images when the fetch button is clicked', async() => { it('should scrape images when the fetch button is clicked', async() => {
fetchMock.mockResolvedValue(new Response(JSON.stringify(scrapeResponse), { status: 200 })); fetchMock.mockResolvedValue(new Response(JSON.stringify(scrapeResponse), { status: 200 }));
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' }}); fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
await new Promise<void>(resolve => { await new Promise<void>(resolve => {
tagsEl.addEventListener('addtag', (event: Event) => { tagsEl.addEventListener('addtag', (event: Event) => {
expect((event as CustomEvent).detail).toEqual({name: 'artist:test'}); expect((event as CustomEvent).detail).toEqual({ name: 'artist:test' });
resolve(); resolve();
}); });
@ -153,8 +167,8 @@ describe('Image upload form', () => {
it('should show null scrape result', () => { it('should show null scrape result', () => {
fetchMock.mockResolvedValue(new Response(JSON.stringify(nullResponse), { status: 200 })); fetchMock.mockResolvedValue(new Response(JSON.stringify(nullResponse), { status: 200 }));
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' }}); fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
fetchButton.click(); fireEvent.click(fetchButton);
return waitFor(() => { return waitFor(() => {
expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledTimes(1);
@ -166,8 +180,8 @@ describe('Image upload form', () => {
it('should show error scrape result', () => { it('should show error scrape result', () => {
fetchMock.mockResolvedValue(new Response(JSON.stringify(errorResponse), { status: 200 })); fetchMock.mockResolvedValue(new Response(JSON.stringify(errorResponse), { status: 200 }));
fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' }}); fireEvent.input(remoteUrl, { target: { value: 'http://localhost/images/1' } });
fetchButton.click(); fireEvent.click(fetchButton);
return waitFor(() => { return waitFor(() => {
expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledTimes(1);

View file

@ -29,11 +29,11 @@ describe('DOM Utilities', () => {
describe('$', () => { describe('$', () => {
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); vi.restoreAllMocks();
}); });
it('should call the native querySelector method on document by default', () => { it('should call the native querySelector method on document by default', () => {
const spy = jest.spyOn(document, 'querySelector'); const spy = vi.spyOn(document, 'querySelector');
mockSelectors.forEach((selector, nthCall) => { mockSelectors.forEach((selector, nthCall) => {
$(selector); $(selector);
@ -43,7 +43,7 @@ describe('DOM Utilities', () => {
it('should call the native querySelector method on the passed element', () => { it('should call the native querySelector method on the passed element', () => {
const mockElement = document.createElement('br'); const mockElement = document.createElement('br');
const spy = jest.spyOn(mockElement, 'querySelector'); const spy = vi.spyOn(mockElement, 'querySelector');
mockSelectors.forEach((selector, nthCall) => { mockSelectors.forEach((selector, nthCall) => {
// FIXME This will not be necessary once the file is properly typed // FIXME This will not be necessary once the file is properly typed
@ -55,11 +55,11 @@ describe('DOM Utilities', () => {
describe('$$', () => { describe('$$', () => {
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); vi.restoreAllMocks();
}); });
it('should call the native querySelectorAll method on document by default', () => { it('should call the native querySelectorAll method on document by default', () => {
const spy = jest.spyOn(document, 'querySelectorAll'); const spy = vi.spyOn(document, 'querySelectorAll');
mockSelectors.forEach((selector, nthCall) => { mockSelectors.forEach((selector, nthCall) => {
$$(selector); $$(selector);
@ -69,7 +69,7 @@ describe('DOM Utilities', () => {
it('should call the native querySelectorAll method on the passed element', () => { it('should call the native querySelectorAll method on the passed element', () => {
const mockElement = document.createElement('br'); const mockElement = document.createElement('br');
const spy = jest.spyOn(mockElement, 'querySelectorAll'); const spy = vi.spyOn(mockElement, 'querySelectorAll');
mockSelectors.forEach((selector, nthCall) => { mockSelectors.forEach((selector, nthCall) => {
// FIXME This will not be necessary once the file is properly typed // FIXME This will not be necessary once the file is properly typed
@ -83,7 +83,7 @@ describe('DOM Utilities', () => {
it(`should remove the ${hiddenClass} class from the provided element`, () => { it(`should remove the ${hiddenClass} class from the provided element`, () => {
const mockElement = createHiddenElement('div'); const mockElement = createHiddenElement('div');
showEl(mockElement); showEl(mockElement);
expect(mockElement).not.toHaveClass(hiddenClass); expect(mockElement).not.to.have.class(hiddenClass);
}); });
it(`should remove the ${hiddenClass} class from all provided elements`, () => { it(`should remove the ${hiddenClass} class from all provided elements`, () => {
@ -93,9 +93,9 @@ describe('DOM Utilities', () => {
createHiddenElement('strong'), createHiddenElement('strong'),
]; ];
showEl(mockElements); showEl(mockElements);
expect(mockElements[0]).not.toHaveClass(hiddenClass); expect(mockElements[0]).not.to.have.class(hiddenClass);
expect(mockElements[1]).not.toHaveClass(hiddenClass); expect(mockElements[1]).not.to.have.class(hiddenClass);
expect(mockElements[2]).not.toHaveClass(hiddenClass); expect(mockElements[2]).not.to.have.class(hiddenClass);
}); });
it(`should remove the ${hiddenClass} class from elements provided in multiple arrays`, () => { it(`should remove the ${hiddenClass} class from elements provided in multiple arrays`, () => {
@ -108,10 +108,10 @@ describe('DOM Utilities', () => {
createHiddenElement('em'), createHiddenElement('em'),
]; ];
showEl(mockElements1, mockElements2); showEl(mockElements1, mockElements2);
expect(mockElements1[0]).not.toHaveClass(hiddenClass); expect(mockElements1[0]).not.to.have.class(hiddenClass);
expect(mockElements1[1]).not.toHaveClass(hiddenClass); expect(mockElements1[1]).not.to.have.class(hiddenClass);
expect(mockElements2[0]).not.toHaveClass(hiddenClass); expect(mockElements2[0]).not.to.have.class(hiddenClass);
expect(mockElements2[1]).not.toHaveClass(hiddenClass); expect(mockElements2[1]).not.to.have.class(hiddenClass);
}); });
}); });
@ -119,7 +119,7 @@ describe('DOM Utilities', () => {
it(`should add the ${hiddenClass} class to the provided element`, () => { it(`should add the ${hiddenClass} class to the provided element`, () => {
const mockElement = document.createElement('div'); const mockElement = document.createElement('div');
hideEl(mockElement); hideEl(mockElement);
expect(mockElement).toHaveClass(hiddenClass); expect(mockElement).to.have.class(hiddenClass);
}); });
it(`should add the ${hiddenClass} class to all provided elements`, () => { it(`should add the ${hiddenClass} class to all provided elements`, () => {
@ -129,9 +129,9 @@ describe('DOM Utilities', () => {
document.createElement('strong'), document.createElement('strong'),
]; ];
hideEl(mockElements); hideEl(mockElements);
expect(mockElements[0]).toHaveClass(hiddenClass); expect(mockElements[0]).to.have.class(hiddenClass);
expect(mockElements[1]).toHaveClass(hiddenClass); expect(mockElements[1]).to.have.class(hiddenClass);
expect(mockElements[2]).toHaveClass(hiddenClass); expect(mockElements[2]).to.have.class(hiddenClass);
}); });
it(`should add the ${hiddenClass} class to elements provided in multiple arrays`, () => { it(`should add the ${hiddenClass} class to elements provided in multiple arrays`, () => {
@ -144,10 +144,10 @@ describe('DOM Utilities', () => {
document.createElement('em'), document.createElement('em'),
]; ];
hideEl(mockElements1, mockElements2); hideEl(mockElements1, mockElements2);
expect(mockElements1[0]).toHaveClass(hiddenClass); expect(mockElements1[0]).to.have.class(hiddenClass);
expect(mockElements1[1]).toHaveClass(hiddenClass); expect(mockElements1[1]).to.have.class(hiddenClass);
expect(mockElements2[0]).toHaveClass(hiddenClass); expect(mockElements2[0]).to.have.class(hiddenClass);
expect(mockElements2[1]).toHaveClass(hiddenClass); expect(mockElements2[1]).to.have.class(hiddenClass);
}); });
}); });
@ -155,7 +155,7 @@ describe('DOM Utilities', () => {
it('should set the disabled attribute to true', () => { it('should set the disabled attribute to true', () => {
const mockElement = document.createElement('button'); const mockElement = document.createElement('button');
disableEl(mockElement); disableEl(mockElement);
expect(mockElement).toBeDisabled(); expect(mockElement).to.have.property('disabled', true);
}); });
it('should set the disabled attribute to true on all provided elements', () => { it('should set the disabled attribute to true on all provided elements', () => {
@ -164,8 +164,8 @@ describe('DOM Utilities', () => {
document.createElement('button'), document.createElement('button'),
]; ];
disableEl(mockElements); disableEl(mockElements);
expect(mockElements[0]).toBeDisabled(); expect(mockElements[0]).to.have.property('disabled', true);
expect(mockElements[1]).toBeDisabled(); expect(mockElements[1]).to.have.property('disabled', true);
}); });
it('should set the disabled attribute to true on elements provided in multiple arrays', () => { it('should set the disabled attribute to true on elements provided in multiple arrays', () => {
@ -178,10 +178,10 @@ describe('DOM Utilities', () => {
document.createElement('button'), document.createElement('button'),
]; ];
disableEl(mockElements1, mockElements2); disableEl(mockElements1, mockElements2);
expect(mockElements1[0]).toBeDisabled(); expect(mockElements1[0]).to.have.property('disabled', true);
expect(mockElements1[1]).toBeDisabled(); expect(mockElements1[1]).to.have.property('disabled', true);
expect(mockElements2[0]).toBeDisabled(); expect(mockElements2[0]).to.have.property('disabled', true);
expect(mockElements2[1]).toBeDisabled(); expect(mockElements2[1]).to.have.property('disabled', true);
}); });
}); });
@ -189,7 +189,7 @@ describe('DOM Utilities', () => {
it('should set the disabled attribute to false', () => { it('should set the disabled attribute to false', () => {
const mockElement = document.createElement('button'); const mockElement = document.createElement('button');
enableEl(mockElement); enableEl(mockElement);
expect(mockElement).toBeEnabled(); expect(mockElement).to.have.property('disabled', false);
}); });
it('should set the disabled attribute to false on all provided elements', () => { it('should set the disabled attribute to false on all provided elements', () => {
@ -198,8 +198,8 @@ describe('DOM Utilities', () => {
document.createElement('button'), document.createElement('button'),
]; ];
enableEl(mockElements); enableEl(mockElements);
expect(mockElements[0]).toBeEnabled(); expect(mockElements[0]).to.have.property('disabled', false);
expect(mockElements[1]).toBeEnabled(); expect(mockElements[1]).to.have.property('disabled', false);
}); });
it('should set the disabled attribute to false on elements provided in multiple arrays', () => { it('should set the disabled attribute to false on elements provided in multiple arrays', () => {
@ -212,10 +212,10 @@ describe('DOM Utilities', () => {
document.createElement('button'), document.createElement('button'),
]; ];
enableEl(mockElements1, mockElements2); enableEl(mockElements1, mockElements2);
expect(mockElements1[0]).toBeEnabled(); expect(mockElements1[0]).to.have.property('disabled', false);
expect(mockElements1[1]).toBeEnabled(); expect(mockElements1[1]).to.have.property('disabled', false);
expect(mockElements2[0]).toBeEnabled(); expect(mockElements2[0]).to.have.property('disabled', false);
expect(mockElements2[1]).toBeEnabled(); expect(mockElements2[1]).to.have.property('disabled', false);
}); });
}); });
@ -223,11 +223,11 @@ describe('DOM Utilities', () => {
it(`should toggle the ${hiddenClass} class on the provided element`, () => { it(`should toggle the ${hiddenClass} class on the provided element`, () => {
const mockVisibleElement = document.createElement('div'); const mockVisibleElement = document.createElement('div');
toggleEl(mockVisibleElement); toggleEl(mockVisibleElement);
expect(mockVisibleElement).toHaveClass(hiddenClass); expect(mockVisibleElement).to.have.class(hiddenClass);
const mockHiddenElement = createHiddenElement('div'); const mockHiddenElement = createHiddenElement('div');
toggleEl(mockHiddenElement); toggleEl(mockHiddenElement);
expect(mockHiddenElement).not.toHaveClass(hiddenClass); expect(mockHiddenElement).not.to.have.class(hiddenClass);
}); });
it(`should toggle the ${hiddenClass} class on all provided elements`, () => { it(`should toggle the ${hiddenClass} class on all provided elements`, () => {
@ -238,10 +238,10 @@ describe('DOM Utilities', () => {
createHiddenElement('em'), createHiddenElement('em'),
]; ];
toggleEl(mockElements); toggleEl(mockElements);
expect(mockElements[0]).toHaveClass(hiddenClass); expect(mockElements[0]).to.have.class(hiddenClass);
expect(mockElements[1]).not.toHaveClass(hiddenClass); expect(mockElements[1]).not.to.have.class(hiddenClass);
expect(mockElements[2]).toHaveClass(hiddenClass); expect(mockElements[2]).to.have.class(hiddenClass);
expect(mockElements[3]).not.toHaveClass(hiddenClass); expect(mockElements[3]).not.to.have.class(hiddenClass);
}); });
it(`should toggle the ${hiddenClass} class on elements provided in multiple arrays`, () => { it(`should toggle the ${hiddenClass} class on elements provided in multiple arrays`, () => {
@ -254,10 +254,10 @@ describe('DOM Utilities', () => {
document.createElement('em'), document.createElement('em'),
]; ];
toggleEl(mockElements1, mockElements2); toggleEl(mockElements1, mockElements2);
expect(mockElements1[0]).not.toHaveClass(hiddenClass); expect(mockElements1[0]).not.to.have.class(hiddenClass);
expect(mockElements1[1]).toHaveClass(hiddenClass); expect(mockElements1[1]).to.have.class(hiddenClass);
expect(mockElements2[0]).not.toHaveClass(hiddenClass); expect(mockElements2[0]).not.to.have.class(hiddenClass);
expect(mockElements2[1]).toHaveClass(hiddenClass); expect(mockElements2[1]).to.have.class(hiddenClass);
}); });
}); });
@ -311,7 +311,7 @@ describe('DOM Utilities', () => {
describe('removeEl', () => { describe('removeEl', () => {
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); vi.restoreAllMocks();
}); });
it('should NOT throw error if element has no parent', () => { it('should NOT throw error if element has no parent', () => {
@ -324,7 +324,7 @@ describe('DOM Utilities', () => {
const childNode = document.createElement('p'); const childNode = document.createElement('p');
parentNode.appendChild(childNode); parentNode.appendChild(childNode);
const spy = jest.spyOn(parentNode, 'removeChild'); const spy = vi.spyOn(parentNode, 'removeChild');
removeEl(childNode); removeEl(childNode);
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
@ -361,8 +361,8 @@ describe('DOM Utilities', () => {
const mockClassTwo = 'class-two'; const mockClassTwo = 'class-two';
const el = makeEl('p', { className: `${mockClassOne} ${mockClassTwo}` }); const el = makeEl('p', { className: `${mockClassOne} ${mockClassTwo}` });
expect(el.nodeName).toEqual('P'); expect(el.nodeName).toEqual('P');
expect(el).toHaveClass(mockClassOne); expect(el).to.have.class(mockClassOne);
expect(el).toHaveClass(mockClassTwo); expect(el).to.have.class(mockClassTwo);
}); });
}); });
@ -374,7 +374,7 @@ describe('DOM Utilities', () => {
}); });
it('should call callback on left click', () => { it('should call callback on left click', () => {
const mockCallback = jest.fn(); const mockCallback = vi.fn();
const element = document.createElement('div'); const element = document.createElement('div');
cleanup = onLeftClick(mockCallback, element as unknown as Document); cleanup = onLeftClick(mockCallback, element as unknown as Document);
@ -384,7 +384,7 @@ describe('DOM Utilities', () => {
}); });
it('should NOT call callback on non-left click', () => { it('should NOT call callback on non-left click', () => {
const mockCallback = jest.fn(); const mockCallback = vi.fn();
const element = document.createElement('div'); const element = document.createElement('div');
cleanup = onLeftClick(mockCallback, element as unknown as Document); cleanup = onLeftClick(mockCallback, element as unknown as Document);
@ -395,7 +395,7 @@ describe('DOM Utilities', () => {
}); });
it('should add click event listener to the document by default', () => { it('should add click event listener to the document by default', () => {
const mockCallback = jest.fn(); const mockCallback = vi.fn();
cleanup = onLeftClick(mockCallback); cleanup = onLeftClick(mockCallback);
fireEvent.click(document.body); fireEvent.click(document.body);
@ -404,7 +404,7 @@ describe('DOM Utilities', () => {
}); });
it('should return a cleanup function that removes the listener', () => { it('should return a cleanup function that removes the listener', () => {
const mockCallback = jest.fn(); const mockCallback = vi.fn();
const element = document.createElement('div'); const element = document.createElement('div');
const localCleanup = onLeftClick(mockCallback, element as unknown as Document); const localCleanup = onLeftClick(mockCallback, element as unknown as Document);
@ -424,8 +424,8 @@ describe('DOM Utilities', () => {
describe('whenReady', () => { describe('whenReady', () => {
it('should call callback immediately if document ready state is not loading', () => { it('should call callback immediately if document ready state is not loading', () => {
const mockReadyStateValue = getRandomArrayItem<DocumentReadyState>(['complete', 'interactive']); const mockReadyStateValue = getRandomArrayItem<DocumentReadyState>(['complete', 'interactive']);
const readyStateSpy = jest.spyOn(document, 'readyState', 'get').mockReturnValue(mockReadyStateValue); const readyStateSpy = vi.spyOn(document, 'readyState', 'get').mockReturnValue(mockReadyStateValue);
const mockCallback = jest.fn(); const mockCallback = vi.fn();
try { try {
whenReady(mockCallback); whenReady(mockCallback);
@ -437,9 +437,9 @@ describe('DOM Utilities', () => {
}); });
it('should add event listener with callback if document ready state is loading', () => { it('should add event listener with callback if document ready state is loading', () => {
const readyStateSpy = jest.spyOn(document, 'readyState', 'get').mockReturnValue('loading'); const readyStateSpy = vi.spyOn(document, 'readyState', 'get').mockReturnValue('loading');
const addEventListenerSpy = jest.spyOn(document, 'addEventListener'); const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
const mockCallback = jest.fn(); const mockCallback = vi.fn();
try { try {
whenReady(mockCallback); whenReady(mockCallback);

View file

@ -30,7 +30,7 @@ describe('Draggable Utilities', () => {
const draggingClass = 'dragging'; const draggingClass = 'dragging';
const dragContainerClass = 'drag-container'; const dragContainerClass = 'drag-container';
const dragOverClass = 'over'; const dragOverClass = 'over';
let documentEventListenerSpy: jest.SpyInstance; let documentEventListenerSpy: MockInstance;
let mockDragContainer: HTMLDivElement; let mockDragContainer: HTMLDivElement;
let mockDraggable: HTMLDivElement; let mockDraggable: HTMLDivElement;
@ -45,7 +45,7 @@ describe('Draggable Utilities', () => {
// Redirect all document event listeners to this element for easier cleanup // Redirect all document event listeners to this element for easier cleanup
documentEventListenerSpy = jest.spyOn(document, 'addEventListener').mockImplementation((...params) => { documentEventListenerSpy = vi.spyOn(document, 'addEventListener').mockImplementation((...params) => {
mockDragContainer.addEventListener(...params); mockDragContainer.addEventListener(...params);
}); });
}); });
@ -63,7 +63,7 @@ describe('Draggable Utilities', () => {
fireEvent(mockDraggable, mockEvent); fireEvent(mockDraggable, mockEvent);
expect(mockDraggable).toHaveClass(draggingClass); expect(mockDraggable).to.have.class(draggingClass);
}); });
it('should add dummy data to the dragstart event if it\'s empty', () => { it('should add dummy data to the dragstart event if it\'s empty', () => {
@ -146,7 +146,7 @@ describe('Draggable Utilities', () => {
fireEvent(mockDraggable, mockEvent); fireEvent(mockDraggable, mockEvent);
expect(mockDraggable).toHaveClass(dragOverClass); expect(mockDraggable).to.have.class(dragOverClass);
}); });
}); });
@ -159,7 +159,7 @@ describe('Draggable Utilities', () => {
fireEvent(mockDraggable, mockEvent); fireEvent(mockDraggable, mockEvent);
expect(mockDraggable).not.toHaveClass(dragOverClass); expect(mockDraggable).not.to.have.class(dragOverClass);
}); });
}); });
@ -170,13 +170,13 @@ describe('Draggable Utilities', () => {
const mockStartEvent = createDragEvent('dragstart'); const mockStartEvent = createDragEvent('dragstart');
fireEvent(mockDraggable, mockStartEvent); fireEvent(mockDraggable, mockStartEvent);
expect(mockDraggable).toHaveClass(draggingClass); expect(mockDraggable).to.have.class(draggingClass);
const mockDropEvent = createDragEvent('drop'); const mockDropEvent = createDragEvent('drop');
fireEvent(mockDraggable, mockDropEvent); fireEvent(mockDraggable, mockDropEvent);
expect(mockDropEvent.defaultPrevented).toBe(true); expect(mockDropEvent.defaultPrevented).toBe(true);
expect(mockDraggable).not.toHaveClass(draggingClass); expect(mockDraggable).not.to.have.class(draggingClass);
}); });
it('should cancel the event and insert source before target if dropped on left side', () => { it('should cancel the event and insert source before target if dropped on left side', () => {
@ -188,11 +188,11 @@ describe('Draggable Utilities', () => {
const mockStartEvent = createDragEvent('dragstart'); const mockStartEvent = createDragEvent('dragstart');
fireEvent(mockSecondDraggable, mockStartEvent); fireEvent(mockSecondDraggable, mockStartEvent);
expect(mockSecondDraggable).toHaveClass(draggingClass); expect(mockSecondDraggable).to.have.class(draggingClass);
const mockDropEvent = createDragEvent('drop'); const mockDropEvent = createDragEvent('drop');
Object.assign(mockDropEvent, { clientX: 124 }); Object.assign(mockDropEvent, { clientX: 124 });
const boundingBoxSpy = jest.spyOn(mockDraggable, 'getBoundingClientRect').mockReturnValue({ const boundingBoxSpy = vi.spyOn(mockDraggable, 'getBoundingClientRect').mockReturnValue({
left: 100, left: 100,
width: 50, width: 50,
} as unknown as DOMRect); } as unknown as DOMRect);
@ -200,7 +200,7 @@ describe('Draggable Utilities', () => {
try { try {
expect(mockDropEvent.defaultPrevented).toBe(true); expect(mockDropEvent.defaultPrevented).toBe(true);
expect(mockSecondDraggable).not.toHaveClass(draggingClass); expect(mockSecondDraggable).not.to.have.class(draggingClass);
expect(mockSecondDraggable.nextElementSibling).toBe(mockDraggable); expect(mockSecondDraggable.nextElementSibling).toBe(mockDraggable);
} }
finally { finally {
@ -217,11 +217,11 @@ describe('Draggable Utilities', () => {
const mockStartEvent = createDragEvent('dragstart'); const mockStartEvent = createDragEvent('dragstart');
fireEvent(mockSecondDraggable, mockStartEvent); fireEvent(mockSecondDraggable, mockStartEvent);
expect(mockSecondDraggable).toHaveClass(draggingClass); expect(mockSecondDraggable).to.have.class(draggingClass);
const mockDropEvent = createDragEvent('drop'); const mockDropEvent = createDragEvent('drop');
Object.assign(mockDropEvent, { clientX: 125 }); Object.assign(mockDropEvent, { clientX: 125 });
const boundingBoxSpy = jest.spyOn(mockDraggable, 'getBoundingClientRect').mockReturnValue({ const boundingBoxSpy = vi.spyOn(mockDraggable, 'getBoundingClientRect').mockReturnValue({
left: 100, left: 100,
width: 50, width: 50,
} as unknown as DOMRect); } as unknown as DOMRect);
@ -229,7 +229,7 @@ describe('Draggable Utilities', () => {
try { try {
expect(mockDropEvent.defaultPrevented).toBe(true); expect(mockDropEvent.defaultPrevented).toBe(true);
expect(mockSecondDraggable).not.toHaveClass(draggingClass); expect(mockSecondDraggable).not.to.have.class(draggingClass);
expect(mockDraggable.nextElementSibling).toBe(mockSecondDraggable); expect(mockDraggable.nextElementSibling).toBe(mockSecondDraggable);
} }
finally { finally {
@ -259,7 +259,7 @@ describe('Draggable Utilities', () => {
const mockStartEvent = createDragEvent('dragstart'); const mockStartEvent = createDragEvent('dragstart');
fireEvent(mockDraggable, mockStartEvent); fireEvent(mockDraggable, mockStartEvent);
expect(mockDraggable).toHaveClass(draggingClass); expect(mockDraggable).to.have.class(draggingClass);
const mockOverElement = createDraggableElement(); const mockOverElement = createDraggableElement();
mockOverElement.classList.add(dragOverClass); mockOverElement.classList.add(dragOverClass);
@ -270,8 +270,8 @@ describe('Draggable Utilities', () => {
const mockDropEvent = createDragEvent('dragend'); const mockDropEvent = createDragEvent('dragend');
fireEvent(mockDraggable, mockDropEvent); fireEvent(mockDraggable, mockDropEvent);
expect(mockDraggable).not.toHaveClass(draggingClass); expect(mockDraggable).not.to.have.class(draggingClass);
expect(mockOverElement).not.toHaveClass(dragOverClass); expect(mockOverElement).not.to.have.class(dragOverClass);
}); });
}); });
@ -291,7 +291,7 @@ describe('Draggable Utilities', () => {
initDraggables(); initDraggables();
const mockEvent = createDragEvent('dragstart'); const mockEvent = createDragEvent('dragstart');
const draggableClosestSpy = jest.spyOn(mockDraggable, 'closest').mockReturnValue(null); const draggableClosestSpy = vi.spyOn(mockDraggable, 'closest').mockReturnValue(null);
try { try {
fireEvent(mockDraggable, mockEvent); fireEvent(mockDraggable, mockEvent);

View file

@ -8,7 +8,7 @@ describe('Event utils', () => {
describe('fire', () => { describe('fire', () => {
it('should call the native dispatchEvent method on the element', () => { it('should call the native dispatchEvent method on the element', () => {
const mockElement = document.createElement('div'); const mockElement = document.createElement('div');
const dispatchEventSpy = jest.spyOn(mockElement, 'dispatchEvent'); const dispatchEventSpy = vi.spyOn(mockElement, 'dispatchEvent');
const mockDetail = getRandomArrayItem([0, 'test', null]); const mockDetail = getRandomArrayItem([0, 'test', null]);
fire(mockElement, mockEvent, mockDetail); fire(mockElement, mockEvent, mockDetail);
@ -42,7 +42,7 @@ describe('Event utils', () => {
mockButton.classList.add('mock-button'); mockButton.classList.add('mock-button');
mockInnerElement.appendChild(mockButton); mockInnerElement.appendChild(mockButton);
const mockHandler = jest.fn(); const mockHandler = vi.fn();
on(mockElement, 'click', `.${innerClass}`, mockHandler); on(mockElement, 'click', `.${innerClass}`, mockHandler);
fireEvent(mockButton, new Event('click', { bubbles: true })); fireEvent(mockButton, new Event('click', { bubbles: true }));
@ -58,7 +58,7 @@ describe('Event utils', () => {
describe('leftClick', () => { describe('leftClick', () => {
it('should fire on left click', () => { it('should fire on left click', () => {
const mockButton = document.createElement('button'); const mockButton = document.createElement('button');
const mockHandler = jest.fn(); const mockHandler = vi.fn();
mockButton.addEventListener('click', e => leftClick(mockHandler)(e, mockButton)); mockButton.addEventListener('click', e => leftClick(mockHandler)(e, mockButton));
@ -69,7 +69,7 @@ describe('Event utils', () => {
it('should NOT fire on any other click', () => { it('should NOT fire on any other click', () => {
const mockButton = document.createElement('button'); const mockButton = document.createElement('button');
const mockHandler = jest.fn(); const mockHandler = vi.fn();
const mockButtonNumber = getRandomArrayItem([1, 2, 3, 4, 5]); const mockButtonNumber = getRandomArrayItem([1, 2, 3, 4, 5]);
mockButton.addEventListener('click', e => leftClick(mockHandler)(e, mockButton)); mockButton.addEventListener('click', e => leftClick(mockHandler)(e, mockButton));
@ -83,7 +83,7 @@ describe('Event utils', () => {
describe('delegate', () => { describe('delegate', () => {
it('should call the native addEventListener method on the element', () => { it('should call the native addEventListener method on the element', () => {
const mockElement = document.createElement('div'); const mockElement = document.createElement('div');
const addEventListenerSpy = jest.spyOn(mockElement, 'addEventListener'); const addEventListenerSpy = vi.spyOn(mockElement, 'addEventListener');
delegate(mockElement, mockEvent, {}); delegate(mockElement, mockEvent, {});
@ -102,7 +102,7 @@ describe('Event utils', () => {
const mockButton = document.createElement('button'); const mockButton = document.createElement('button');
mockElement.appendChild(mockButton); mockElement.appendChild(mockButton);
const mockHandler = jest.fn(); const mockHandler = vi.fn();
delegate(mockElement, 'click', { [`.${parentClass}`]: mockHandler }); delegate(mockElement, 'click', { [`.${parentClass}`]: mockHandler });
fireEvent(mockButton, new Event('click', { bubbles: true })); fireEvent(mockButton, new Event('click', { bubbles: true }));
@ -127,8 +127,8 @@ describe('Event utils', () => {
const mockButton = document.createElement('button'); const mockButton = document.createElement('button');
mockWrapperElement.appendChild(mockButton); mockWrapperElement.appendChild(mockButton);
const mockParentHandler = jest.fn(); const mockParentHandler = vi.fn();
const mockWrapperHandler = jest.fn().mockReturnValue(false); const mockWrapperHandler = vi.fn().mockReturnValue(false);
delegate(mockElement, 'click', { delegate(mockElement, 'click', {
[`.${wrapperClass}`]: mockWrapperHandler, [`.${wrapperClass}`]: mockWrapperHandler,
[`.${parentClass}`]: mockParentHandler, [`.${parentClass}`]: mockParentHandler,

View file

@ -4,6 +4,7 @@ import { mockStorage } from '../../../test/mock-storage';
import { createEvent, fireEvent } from '@testing-library/dom'; import { createEvent, fireEvent } from '@testing-library/dom';
import { EventType } from '@testing-library/dom/types/events'; import { EventType } from '@testing-library/dom/types/events';
import { SpoilerType } from '../../../types/booru-object'; import { SpoilerType } from '../../../types/booru-object';
import { beforeEach } from 'vitest';
describe('Image utils', () => { describe('Image utils', () => {
const hiddenClass = 'hidden'; const hiddenClass = 'hidden';
@ -82,6 +83,10 @@ describe('Image utils', () => {
}, },
}); });
beforeEach(() => {
mockServeHidpiValue = null;
});
describe('video thumbnail', () => { describe('video thumbnail', () => {
type CreateMockElementsOptions = { type CreateMockElementsOptions = {
extension: string; extension: string;
@ -109,7 +114,7 @@ describe('Image utils', () => {
}); });
} }
mockElement.appendChild(mockVideo); mockElement.appendChild(mockVideo);
const playSpy = jest.spyOn(mockVideo, 'play').mockReturnValue(Promise.resolve()); const playSpy = vi.spyOn(mockVideo, 'play').mockReturnValue(Promise.resolve());
const mockSpoilerOverlay = createMockSpoilerOverlay(); const mockSpoilerOverlay = createMockSpoilerOverlay();
mockElement.appendChild(mockSpoilerOverlay); mockElement.appendChild(mockSpoilerOverlay);
@ -141,7 +146,7 @@ describe('Image utils', () => {
const result = showThumb(mockElement); const result = showThumb(mockElement);
expect(mockImage).toHaveClass(hiddenClass); expect(mockImage).to.have.class(hiddenClass);
expect(mockVideo.children).toHaveLength(2); expect(mockVideo.children).toHaveLength(2);
const webmSourceElement = mockVideo.children[0]; const webmSourceElement = mockVideo.children[0];
@ -155,10 +160,10 @@ describe('Image utils', () => {
expect(mp4SourceElement.getAttribute('type')).toEqual('video/mp4'); expect(mp4SourceElement.getAttribute('type')).toEqual('video/mp4');
expect(mp4SourceElement.getAttribute('src')).toEqual(webmSource.replace('webm', 'mp4')); expect(mp4SourceElement.getAttribute('src')).toEqual(webmSource.replace('webm', 'mp4'));
expect(mockVideo).not.toHaveClass(hiddenClass); expect(mockVideo).not.to.have.class(hiddenClass);
expect(playSpy).toHaveBeenCalledTimes(1); expect(playSpy).toHaveBeenCalledTimes(1);
expect(mockSpoilerOverlay).toHaveClass(hiddenClass); expect(mockSpoilerOverlay).to.have.class(hiddenClass);
expect(result).toBe(true); expect(result).toBe(true);
}); });
@ -168,7 +173,7 @@ describe('Image utils', () => {
const { mockElement } = createMockElements({ const { mockElement } = createMockElements({
extension: 'webm', extension: 'webm',
}); });
const jsonParseSpy = jest.spyOn(JSON, 'parse'); const jsonParseSpy = vi.spyOn(JSON, 'parse');
mockElement.removeAttribute(missingAttributeName); mockElement.removeAttribute(missingAttributeName);
@ -233,7 +238,7 @@ describe('Image utils', () => {
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]); expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]);
expect(mockSizeImage.srcset).toBe(''); expect(mockSizeImage.srcset).toBe('');
expect(mockSpoilerOverlay).toHaveClass(hiddenClass); expect(mockSpoilerOverlay).to.have.class(hiddenClass);
expect(result).toBe(true); expect(result).toBe(true);
}); });
@ -250,7 +255,7 @@ describe('Image utils', () => {
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]); expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]);
expect(mockSizeImage.srcset).toBe(''); expect(mockSizeImage.srcset).toBe('');
expect(mockSpoilerOverlay).toHaveClass(hiddenClass); expect(mockSpoilerOverlay).to.have.class(hiddenClass);
expect(result).toBe(true); expect(result).toBe(true);
}); });
@ -267,8 +272,8 @@ describe('Image utils', () => {
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize].replace('webm', 'gif')); expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize].replace('webm', 'gif'));
expect(mockSizeImage.srcset).toBe(''); expect(mockSizeImage.srcset).toBe('');
expect(mockSpoilerOverlay).not.toHaveClass(hiddenClass); expect(mockSpoilerOverlay).not.to.have.class(hiddenClass);
expect(mockSpoilerOverlay).toHaveTextContent('WebM'); expect(mockSpoilerOverlay).to.have.text('WebM');
expect(result).toBe(true); expect(result).toBe(true);
}); });
@ -291,7 +296,7 @@ describe('Image utils', () => {
expect(mockSizeImage.srcset).toContain(`${mockSizeUrls[size]} 1x`); expect(mockSizeImage.srcset).toContain(`${mockSizeUrls[size]} 1x`);
expect(mockSizeImage.srcset).toContain(`${mockSizeUrls[x2size]} 2x`); expect(mockSizeImage.srcset).toContain(`${mockSizeUrls[x2size]} 2x`);
expect(mockSpoilerOverlay).toHaveClass(hiddenClass); expect(mockSpoilerOverlay).to.have.class(hiddenClass);
return result; return result;
}; };
@ -318,7 +323,7 @@ describe('Image utils', () => {
expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]); expect(mockSizeImage.src).toBe(mockSizeUrls[mockSize]);
expect(mockSizeImage.srcset).toBe(''); expect(mockSizeImage.srcset).toBe('');
expect(mockSpoilerOverlay).toHaveClass(hiddenClass); expect(mockSpoilerOverlay).to.have.class(hiddenClass);
expect(result).toBe(true); expect(result).toBe(true);
}); });
}); });
@ -359,9 +364,9 @@ describe('Image utils', () => {
showBlock(mockElement); showBlock(mockElement);
expect(mockFilteredImageElement).toHaveClass(hiddenClass); expect(mockFilteredImageElement).to.have.class(hiddenClass);
expect(mockShowElement).not.toHaveClass(hiddenClass); expect(mockShowElement).not.to.have.class(hiddenClass);
expect(mockShowElement).toHaveClass(spoilerPendingClass); expect(mockShowElement).to.have.class(spoilerPendingClass);
}); });
it('should not throw if image-filtered element is missing', () => { it('should not throw if image-filtered element is missing', () => {
@ -382,7 +387,7 @@ describe('Image utils', () => {
it('should return early if picture AND video elements are missing', () => { it('should return early if picture AND video elements are missing', () => {
const mockElement = document.createElement('div'); const mockElement = document.createElement('div');
const querySelectorSpy = jest.spyOn(mockElement, 'querySelector'); const querySelectorSpy = vi.spyOn(mockElement, 'querySelector');
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason); hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
@ -399,9 +404,9 @@ describe('Image utils', () => {
const mockElement = document.createElement('div'); const mockElement = document.createElement('div');
const mockVideo = document.createElement('video'); const mockVideo = document.createElement('video');
mockElement.appendChild(mockVideo); mockElement.appendChild(mockVideo);
const pauseSpy = jest.spyOn(mockVideo, 'pause').mockReturnValue(undefined); const pauseSpy = vi.spyOn(mockVideo, 'pause').mockReturnValue(undefined);
const querySelectorSpy = jest.spyOn(mockElement, 'querySelector'); const querySelectorSpy = vi.spyOn(mockElement, 'querySelector');
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason); hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
@ -411,7 +416,7 @@ describe('Image utils', () => {
expect(querySelectorSpy).toHaveBeenNthCalledWith(2, 'video'); expect(querySelectorSpy).toHaveBeenNthCalledWith(2, 'video');
expect(querySelectorSpy).toHaveBeenNthCalledWith(3, 'img'); expect(querySelectorSpy).toHaveBeenNthCalledWith(3, 'img');
expect(querySelectorSpy).toHaveBeenNthCalledWith(4, `.${spoilerOverlayClass}`); expect(querySelectorSpy).toHaveBeenNthCalledWith(4, `.${spoilerOverlayClass}`);
expect(mockVideo).not.toHaveClass(hiddenClass); expect(mockVideo).not.to.have.class(hiddenClass);
} }
finally { finally {
querySelectorSpy.mockRestore(); querySelectorSpy.mockRestore();
@ -423,7 +428,7 @@ describe('Image utils', () => {
const mockElement = document.createElement('div'); const mockElement = document.createElement('div');
const mockVideo = document.createElement('video'); const mockVideo = document.createElement('video');
mockElement.appendChild(mockVideo); mockElement.appendChild(mockVideo);
const pauseSpy = jest.spyOn(mockVideo, 'pause').mockReturnValue(undefined); const pauseSpy = vi.spyOn(mockVideo, 'pause').mockReturnValue(undefined);
const mockImage = document.createElement('img'); const mockImage = document.createElement('img');
mockImage.classList.add(hiddenClass); mockImage.classList.add(hiddenClass);
mockElement.appendChild(mockImage); mockElement.appendChild(mockImage);
@ -434,11 +439,11 @@ describe('Image utils', () => {
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason); hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
try { try {
expect(mockImage).not.toHaveClass(hiddenClass); expect(mockImage).not.to.have.class(hiddenClass);
expect(mockImage).toHaveAttribute('src', mockSpoilerUri); expect(mockImage).to.have.attribute('src', mockSpoilerUri);
expect(mockOverlay).toHaveTextContent(mockSpoilerReason); expect(mockOverlay).to.have.text(mockSpoilerReason);
expect(mockVideo).toBeEmptyDOMElement(); expect(mockVideo).not.to.have.descendants('*');
expect(mockVideo).toHaveClass(hiddenClass); expect(mockVideo).to.have.class(hiddenClass);
expect(pauseSpy).toHaveBeenCalled(); expect(pauseSpy).toHaveBeenCalled();
} }
finally { finally {
@ -452,8 +457,8 @@ describe('Image utils', () => {
const mockPicture = document.createElement('picture'); const mockPicture = document.createElement('picture');
mockElement.appendChild(mockPicture); mockElement.appendChild(mockPicture);
const imgQuerySelectorSpy = jest.spyOn(mockElement, 'querySelector'); const imgQuerySelectorSpy = vi.spyOn(mockElement, 'querySelector');
const pictureQuerySelectorSpy = jest.spyOn(mockPicture, 'querySelector'); const pictureQuerySelectorSpy = vi.spyOn(mockPicture, 'querySelector');
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason); hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
@ -483,24 +488,24 @@ describe('Image utils', () => {
hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason); hideThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
expect(mockImage).toHaveAttribute('srcset', ''); expect(mockImage).to.have.attribute('srcset', '');
expect(mockImage).toHaveAttribute('src', mockSpoilerUri); expect(mockImage).to.have.attribute('src', mockSpoilerUri);
expect(mockOverlay).toContainHTML(mockSpoilerReason); expect(mockOverlay).to.contain.html(mockSpoilerReason);
expect(mockOverlay).not.toHaveClass(hiddenClass); expect(mockOverlay).not.to.have.class(hiddenClass);
}); });
}); });
describe('spoilerThumb', () => { describe('spoilerThumb', () => {
const testSpoilerThumb = (handlers?: [EventType, EventType]) => { const testSpoilerThumb = (handlers?: [EventType, EventType]) => {
const { mockElement, mockSpoilerOverlay, mockSizeImage } = createMockElementWithPicture('jpg'); const { mockElement, mockSpoilerOverlay, mockSizeImage } = createMockElementWithPicture('jpg');
const addEventListenerSpy = jest.spyOn(mockElement, 'addEventListener'); const addEventListenerSpy = vi.spyOn(mockElement, 'addEventListener');
spoilerThumb(mockElement, mockSpoilerUri, mockSpoilerReason); spoilerThumb(mockElement, mockSpoilerUri, mockSpoilerReason);
// Element should be hidden by the call // Element should be hidden by the call
expect(mockSizeImage).toHaveAttribute('src', mockSpoilerUri); expect(mockSizeImage).to.have.attribute('src', mockSpoilerUri);
expect(mockSpoilerOverlay).not.toHaveClass(hiddenClass); expect(mockSpoilerOverlay).not.to.have.class(hiddenClass);
expect(mockSpoilerOverlay).toContainHTML(mockSpoilerReason); expect(mockSpoilerOverlay).to.contain.html(mockSpoilerReason);
// If addEventListener calls are not expected, bail // If addEventListener calls are not expected, bail
if (!handlers) { if (!handlers) {
@ -521,8 +526,8 @@ describe('Image utils', () => {
if (firstHandler === 'click') { if (firstHandler === 'click') {
expect(clickEvent.defaultPrevented).toBe(true); expect(clickEvent.defaultPrevented).toBe(true);
} }
expect(mockSizeImage).not.toHaveAttribute('src', mockSpoilerUri); expect(mockSizeImage).not.to.have.attribute('src', mockSpoilerUri);
expect(mockSpoilerOverlay).toHaveClass(hiddenClass); expect(mockSpoilerOverlay).to.have.class(hiddenClass);
if (firstHandler === 'click') { if (firstHandler === 'click') {
// Second attempt to click a shown spoiler should not cause default prevention // Second attempt to click a shown spoiler should not cause default prevention
@ -534,9 +539,9 @@ describe('Image utils', () => {
// Moving the mouse away should hide the image and show the overlay again // Moving the mouse away should hide the image and show the overlay again
const mouseLeaveEvent = createEvent.mouseLeave(mockElement); const mouseLeaveEvent = createEvent.mouseLeave(mockElement);
fireEvent(mockElement, mouseLeaveEvent); fireEvent(mockElement, mouseLeaveEvent);
expect(mockSizeImage).toHaveAttribute('src', mockSpoilerUri); expect(mockSizeImage).to.have.attribute('src', mockSpoilerUri);
expect(mockSpoilerOverlay).not.toHaveClass(hiddenClass); expect(mockSpoilerOverlay).not.to.have.class(hiddenClass);
expect(mockSpoilerOverlay).toContainHTML(mockSpoilerReason); expect(mockSpoilerOverlay).to.contain.html(mockSpoilerReason);
}; };
let lastSpoilerType: SpoilerType; let lastSpoilerType: SpoilerType;
@ -613,10 +618,10 @@ describe('Image utils', () => {
spoilerBlock(mockElement, mockSpoilerUri, mockSpoilerReason); spoilerBlock(mockElement, mockSpoilerUri, mockSpoilerReason);
expect(mockImage).toHaveAttribute('src', mockSpoilerUri); expect(mockImage).to.have.attribute('src', mockSpoilerUri);
expect(mockExplanation).toContainHTML(mockSpoilerReason); expect(mockExplanation).to.contain.html(mockSpoilerReason);
expect(mockImageShow).toHaveClass(hiddenClass); expect(mockImageShow).to.have.class(hiddenClass);
expect(mockImageFiltered).not.toHaveClass(hiddenClass); expect(mockImageFiltered).not.to.have.class(hiddenClass);
}); });
it('should not throw if image-filtered element is missing', () => { it('should not throw if image-filtered element is missing', () => {

View file

@ -1,5 +1,5 @@
import { fetchHtml, fetchJson, handleError } from '../requests'; import { fetchHtml, fetchJson, handleError } from '../requests';
import fetchMock from 'jest-fetch-mock'; import { fetchMock } from '../../../test/fetch-mock.ts';
describe('Request utils', () => { describe('Request utils', () => {
const mockEndpoint = '/endpoint'; const mockEndpoint = '/endpoint';

View file

@ -117,11 +117,11 @@ describe('Store utilities', () => {
it('should attach a storage event listener and fire when the provide key changes', () => { it('should attach a storage event listener and fire when the provide key changes', () => {
const mockKey = `mock-watch-key-${getRandomIntBetween(1, 10)}`; const mockKey = `mock-watch-key-${getRandomIntBetween(1, 10)}`;
const mockValue = Math.random(); const mockValue = Math.random();
const mockCallback = jest.fn(); const mockCallback = vi.fn();
setStorageValue({ setStorageValue({
[mockKey]: JSON.stringify(mockValue), [mockKey]: JSON.stringify(mockValue),
}); });
const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
const cleanup = store.watch(mockKey, mockCallback); const cleanup = store.watch(mockKey, mockCallback);

3973
assets/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,8 +3,8 @@
"scripts": { "scripts": {
"deploy": "cross-env NODE_ENV=production tsc && cross-env NODE_ENV=production vite build", "deploy": "cross-env NODE_ENV=production tsc && cross-env NODE_ENV=production vite build",
"lint": "eslint . --ext .js,.ts", "lint": "eslint . --ext .js,.ts",
"test": "jest --ci", "test": "vitest run --coverage",
"test:watch": "jest --watch", "test:watch": "vitest watch --coverage",
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"preview": "vite preview" "preview": "vite preview"
@ -25,12 +25,12 @@
}, },
"devDependencies": { "devDependencies": {
"@testing-library/dom": "^10.1.0", "@testing-library/dom": "^10.1.0",
"@testing-library/jest-dom": "^6.4.2", "@types/chai-dom": "^1.11.3",
"@types/jest": "^29.5.12", "@vitest/coverage-v8": "^1.5.3",
"eslint-plugin-jest": "^28.3.0", "chai-dom": "^1.12.0",
"eslint-plugin-jest-dom": "^5.4.0", "eslint-plugin-vitest": "^0.5.4",
"jest": "^29.7.0", "jsdom": "^24.0.0",
"jest-fetch-mock": "^3.0.3", "vitest": "^1.5.3",
"ts-jest": "^29.1.2" "vitest-fetch-mock": "^0.2.2"
} }
} }

View file

@ -0,0 +1,4 @@
import createFetchMock from 'vitest-fetch-mock';
import { vi } from 'vitest';
export const fetchMock = createFetchMock(vi);

View file

@ -1,21 +0,0 @@
import '@testing-library/jest-dom';
import { matchNone } from '../js/query/boolean';
window.booru = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
timeAgo: () => {},
csrfToken: 'mockCsrfToken',
hiddenTag: '/mock-tagblocked.svg',
hiddenTagList: [],
ignoredTagList: [],
imagesWithDownvotingDisabled: [],
spoilerType: 'off',
spoileredTagList: [],
userCanEditFilter: false,
userIsSignedIn: false,
watchedTagList: [],
hiddenFilter: matchNone(),
spoileredFilter: matchNone(),
interactions: [],
tagsVersion: 5
};

View file

@ -1,9 +1,9 @@
export function mockDateNow(initialDateNow: number): void { export function mockDateNow(initialDateNow: number): void {
beforeAll(() => { beforeAll(() => {
jest.useFakeTimers().setSystemTime(initialDateNow); vi.useFakeTimers().setSystemTime(initialDateNow);
}); });
afterAll(() => { afterAll(() => {
jest.useRealTimers(); vi.useRealTimers();
}); });
} }

View file

@ -1,9 +1,11 @@
import { MockInstance } from 'vitest';
type MockStorageKeys = 'getItem' | 'setItem' | 'removeItem'; type MockStorageKeys = 'getItem' | 'setItem' | 'removeItem';
export function mockStorage<Keys extends MockStorageKeys>(options: Pick<Storage, Keys>): { [k in `${Keys}Spy`]: jest.SpyInstance } { export function mockStorage<Keys extends MockStorageKeys>(options: Pick<Storage, Keys>): { [k in `${Keys}Spy`]: MockInstance } {
const getItemSpy = 'getItem' in options ? jest.spyOn(Storage.prototype, 'getItem') : undefined; const getItemSpy = 'getItem' in options ? vi.spyOn(Storage.prototype, 'getItem') : undefined;
const setItemSpy = 'setItem' in options ? jest.spyOn(Storage.prototype, 'setItem') : undefined; const setItemSpy = 'setItem' in options ? vi.spyOn(Storage.prototype, 'setItem') : undefined;
const removeItemSpy = 'removeItem' in options ? jest.spyOn(Storage.prototype, 'removeItem') : undefined; const removeItemSpy = 'removeItem' in options ? vi.spyOn(Storage.prototype, 'removeItem') : undefined;
beforeAll(() => { beforeAll(() => {
getItemSpy && getItemSpy.mockImplementation((options as Storage).getItem); getItemSpy && getItemSpy.mockImplementation((options as Storage).getItem);
@ -26,7 +28,7 @@ export function mockStorage<Keys extends MockStorageKeys>(options: Pick<Storage,
return { getItemSpy, setItemSpy, removeItemSpy } as ReturnType<typeof mockStorage>; return { getItemSpy, setItemSpy, removeItemSpy } as ReturnType<typeof mockStorage>;
} }
type MockStorageImplApi = { [k in `${MockStorageKeys}Spy`]: jest.SpyInstance } & { type MockStorageImplApi = { [k in `${MockStorageKeys}Spy`]: MockInstance } & {
/** /**
* Forces the mock storage back to its default (empty) state * Forces the mock storage back to its default (empty) state
* @param value * @param value

View file

@ -0,0 +1,38 @@
import { matchNone } from '../js/query/boolean';
import chai from 'chai';
import chaiDom from 'chai-dom';
import { URL } from 'node:url';
import { Blob } from 'node:buffer';
import { fireEvent } from '@testing-library/dom';
chai.use(chaiDom);
window.booru = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
timeAgo: () => {},
csrfToken: 'mockCsrfToken',
hiddenTag: '/mock-tagblocked.svg',
hiddenTagList: [],
ignoredTagList: [],
imagesWithDownvotingDisabled: [],
spoilerType: 'off',
spoileredTagList: [],
userCanEditFilter: false,
userIsSignedIn: false,
watchedTagList: [],
hiddenFilter: matchNone(),
spoileredFilter: matchNone(),
interactions: [],
tagsVersion: 5
};
// 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
Object.assign(globalThis, { URL, Blob });
// Prevents an error when calling `form.submit()` directly in
// the code that is being tested
HTMLFormElement.prototype.submit = function() {
fireEvent.submit(this);
};

View file

@ -19,6 +19,8 @@
"noEmit": true, "noEmit": true,
"strict": true "strict": true,
"types": ["vitest/globals"]
} }
} }

View file

@ -1,12 +1,15 @@
/// <reference types="vitest" />
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import autoprefixer from 'autoprefixer'; import autoprefixer from 'autoprefixer';
import { defineConfig, UserConfig, ConfigEnv } from 'vite'; import { defineConfig, UserConfig, ConfigEnv } from 'vite';
export default defineConfig(({ command }: ConfigEnv): UserConfig => { export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
const isDev = command !== 'build'; const isDev = command !== 'build' && mode !== 'test';
if (isDev) { if (isDev) {
// Terminate the watcher when Phoenix quits
// @see https://moroz.dev/blog/integrating-vite-js-with-phoenix-1-6
process.stdin.on('close', () => { process.stdin.on('close', () => {
// eslint-disable-next-line no-process-exit // eslint-disable-next-line no-process-exit
process.exit(0); process.exit(0);
@ -61,6 +64,34 @@ export default defineConfig(({ command }: ConfigEnv): UserConfig => {
postcss: { postcss: {
plugins: [autoprefixer] plugins: [autoprefixer]
} }
},
test: {
globals: true,
environment: 'jsdom',
// TODO Jest --randomize CLI flag equivalent, consider enabling in the future
// sequence: { shuffle: true },
setupFiles: './test/vitest-setup.ts',
coverage: {
reporter: ['text', 'html'],
include: ['js/**/*.{js,ts}'],
exclude: [
'node_modules/',
'.*\\.test\\.ts$',
'.*\\.d\\.ts$',
],
thresholds: {
statements: 0,
branches: 0,
functions: 0,
lines: 0,
'**/utils/**/*.ts': {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
}
}
} }
}; };
}); });