test: run frontend unit tests in browsers (#38860)

Run them in headless [vitest browser
mode](https://vitest.dev/guide/browser/) in chromium and firefox.
Similar UX than current tests, it's about 5 times as slow (goes from 1s
to 5s on my machine), but definitely worth it as it removes all
happy-dom problems.

---------

Signed-off-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-08-18 00:22:54 +02:00
committed by GitHub
parent 55e7cafcb6
commit df71d5f5e2
22 changed files with 211 additions and 146 deletions
+6 -5
View File
@@ -26,15 +26,16 @@ test('createElementFromAttrs', () => {
});
test('querySingleVisibleElem', () => {
let el = createElementFromHTML('<div></div>');
const el = document.createElement('div');
document.body.append(el); // layout, and thus visibility, is only computed in the document
expect(querySingleVisibleElem(el, 'span')).toBeNull();
el = createElementFromHTML('<div><span>foo</span></div>');
el.innerHTML = '<span>foo</span>';
expect(querySingleVisibleElem(el, 'span')!.textContent).toEqual('foo');
el = createElementFromHTML('<div><span style="display: none;">foo</span><span>bar</span></div>');
el.innerHTML = '<span style="display: none;">foo</span><span>bar</span>';
expect(querySingleVisibleElem(el, 'span')!.textContent).toEqual('bar');
el = createElementFromHTML('<div><span class="some-class tw-hidden">foo</span><span>bar</span></div>');
el.innerHTML = '<span class="some-class tw-hidden">foo</span><span>bar</span>';
expect(querySingleVisibleElem(el, 'span')!.textContent).toEqual('bar');
el = createElementFromHTML('<div><span>foo</span><span>bar</span></div>');
el.innerHTML = '<span>foo</span><span>bar</span>';
expect(() => querySingleVisibleElem(el, 'span')).toThrow('Expected exactly one visible element');
});
+1 -8
View File
@@ -1,7 +1,6 @@
import {debounce} from './func.ts';
import type {Promisable} from '../types.ts';
import type $ from 'jquery';
import {isInFrontendUnitTest} from './testhelper.ts';
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
type ElementArg = Element | string | ArrayLikeIterable<Element> | ReturnType<typeof $>;
@@ -73,11 +72,6 @@ export function queryElemSiblings<T extends Element>(el: Element, selector = '*'
/** it works like jQuery.children: only the direct children are selected */
export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
if (isInFrontendUnitTest()) {
// https://github.com/capricorn86/happy-dom/issues/1620 : ":scope" doesn't work
const selected = Array.from<T>(parent.children as any).filter((child) => child.matches(selector));
return applyElemsCallback<T>(selected, fn);
}
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
}
@@ -261,8 +255,7 @@ export function isElemVisible(el: HTMLElement): boolean {
// Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
// This function DOESN'T account for all possible visibility scenarios, its behavior is covered by the tests of "querySingleVisibleElem"
if (!el) return false;
// checking el.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout
return Boolean(!el.classList.contains('tw-hidden') && (el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none');
return Boolean(!el.classList.contains('tw-hidden') && (el.offsetWidth || el.offsetHeight || el.getClientRects().length));
}
export function createElementFromHTML<T extends Element>(htmlString: string): T {
+5 -7
View File
@@ -1,10 +1,8 @@
import {readFile} from 'node:fs/promises';
import * as path from 'node:path';
import globTestData from './glob.test.txt';
import {globCompile} from './glob.ts';
async function loadGlobTestData(): Promise<{caseNames: string[], caseDataMap: Record<string, string>}> {
const fileContent = await readFile(path.join(import.meta.dirname, 'glob.test.txt'), 'utf8');
const fileLines = fileContent.split('\n');
function loadGlobTestData(): {caseNames: string[], caseDataMap: Record<string, string>} {
const fileLines = globTestData.split('\n');
const caseDataMap: Record<string, string> = {};
const caseNameMap: Record<string, boolean> = {};
for (let line of fileLines) {
@@ -103,8 +101,8 @@ function loadGlobGolangCases() {
];
}
test('GlobCompiler', async () => {
const {caseNames, caseDataMap} = await loadGlobTestData();
test('GlobCompiler', () => {
const {caseNames, caseDataMap} = loadGlobTestData();
expect(caseNames.length).toBe(10); // should have 10 test cases
for (const caseName of caseNames) {
const pattern = caseDataMap[`pattern_${caseName}`];
+1 -3
View File
@@ -1,9 +1,7 @@
import {GET} from '../modules/fetch.ts';
import {matchEmoji, matchMention} from './match.ts';
vi.mock('../modules/fetch.ts', () => ({
GET: vi.fn(),
}));
vi.mock('../modules/fetch.ts', () => ({GET: vi.fn()}));
const testMentions = [
{key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'},
+12 -5
View File
@@ -1,8 +1,15 @@
// there could be different "testing" concepts, for example: backend's "setting.IsInTesting"
// even if backend is in testing mode, frontend could be complied in production mode
// so this function only checks if the frontend is in unit testing mode (usually from *.test.ts files)
export function isInFrontendUnitTest() {
return import.meta.env.MODE === 'test';
import {onTestFinished} from 'vitest';
/** Record and block navigations, as a real browser forbids stubbing "window.location" */
export function captureNavigations() {
const navigations: Array<{url: string, type: NavigationType}> = [];
const onNavigate = (e: NavigateEvent) => {
navigations.push({url: e.destination.url, type: e.navigationType});
e.preventDefault();
};
window.navigation.addEventListener('navigate', onNavigate);
onTestFinished(() => window.navigation.removeEventListener('navigate', onNavigate));
return navigations;
}
/** strip common indentation from a string and trim it */