TauriTavern/tests/browser/runtime.mjs
Darkatse 304d6d3cfe perf(test): remove two avoidable waits from the check suite
happy-dom's Element.toString() serializes outerHTML, and Popper stringifies nodes while the app loads, so each browser runtime spent most of its startup serializing the document. The LAN pairing test's stale address was a bound socket that macOS leaves unanswered until the 2 s probe timeout; a listener that closes every connection fails at once on every platform.
2026-10-07 18:29:30 -04:00

140 lines
6.5 KiB
JavaScript

import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import vm from 'node:vm';
import { Window } from 'happy-dom';
import { installHostIdentity, HOSTS } from '../helpers/host-identity.mjs';
const root = fileURLToPath(new URL('../../src/', import.meta.url));
// These tests drive the application through happyDOM.waitUntilComplete(), which drains its
// pending timers in real time: one navigation run schedules ~57 s of nominal delay, mostly
// 1 s save debounces and 200 ms utility delays.
//
// What the assertions actually depend on is the order these timers fire in, not their length,
// so every delay is divided rather than capped. happy-dom's own timer.maxTimeout caps instead,
// which flattens a 200 ms debounce and a 1 s save onto the same deadline and stops the debounce
// from coalescing two saves; dividing keeps them 5:1 apart at any factor. The suite passes
// unchanged from 5 up to 400, where the shortest window is down to 1 ms, so the work inside
// these windows is sub-millisecond and 10 keeps ~20 ms of room for a much slower machine.
// Past 10 the remaining gain is under 0.1 s, which is not worth spending that margin on.
const TIMER_SCALE = 10;
export function createBrowserRuntime() {
const window = new Window({
url: 'http://localhost/',
settings: { disableCSSFileLoading: true, disableJavaScriptFileLoading: true },
});
// happy-dom #2182: Node's getter must work on subclasses, as it does in browsers.
// DOMPurify reads this getter directly to avoid DOM clobbering.
const nodeName = Object.getOwnPropertyDescriptor(window.Node.prototype, 'nodeName');
Object.defineProperty(window.Node.prototype, 'nodeName', {
...nodeName,
get() {
for (let proto = Object.getPrototypeOf(this); proto !== window.Node.prototype; proto = Object.getPrototypeOf(proto)) {
const getter = Object.getOwnPropertyDescriptor(proto, 'nodeName')?.get;
if (getter) return getter.call(this);
}
return nodeName.get.call(this);
},
});
// Elements must stringify as in browsers, or Popper serializes the document during startup.
delete window.Element.prototype.toString;
window.document.write(readFileSync(path.join(root, 'index.html'), 'utf8')
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, ''));
for (const file of ['jquery-3.5.1.min.js', 'jquery-ui.min.js', 'jquery.transit.min.js',
'jquery-cookie-1.4.1.min.js', 'cropper.min.js', 'jquery-cropper.min.js',
'toastr.min.js', 'select2.min.js', 'pagination.js']) {
vm.runInContext(readFileSync(path.join(root, 'lib', file), 'utf8'), window, { filename: file });
}
// Exercise actual module evaluation without starting chat/settings IO through DOM-ready callbacks.
window.jQuery.holdReady(true);
window.structuredClone = structuredClone;
installHostIdentity(HOSTS.windows, window);
// happy-dom's TransformStream is a Node Transform, not the Web Streams constructor.
window.TransformStream = TransformStream;
// happy-dom exposes option elements but omits the browser's Option constructor.
window.Option = function (text = '', value = text, defaultSelected = false, selected = false) {
const option = window.document.createElement('option');
option.text = text;
option.value = value;
option.defaultSelected = defaultSelected;
option.selected = selected;
return option;
};
const scheduleTimeout = window.setTimeout.bind(window);
window.setTimeout = (callback, delay = 0, ...args) =>
scheduleTimeout(callback, Math.ceil(delay / TIMER_SCALE), ...args);
window.fetch = async () => { throw new Error('Unexpected network request'); };
window.localStorage.setItem('tt:embeddedRuntimeProfile', 'off');
const listeners = new Map();
window.__TAURI__ = {
core: {
async invoke(command, args) {
switch (command) {
case 'is_ready': return true;
case 'backend_error_bridge_ready': return [];
case 'wait_for_backend_ready': return;
case 'get_tauritavern_settings': return {
panel_runtime_profile: 'off', embedded_runtime_profile: 'off',
dynamic_theme: { enabled: false, wallpaper_enabled: false },
};
case 'read_frontend_template': return readFileSync(path.join(root, 'scripts/templates', args.name), 'utf8');
default: throw new Error(`Unexpected IPC command: ${command}`);
}
},
convertFileSrc: file => `http://localhost/${file}`,
},
event: {
async listen(name, callback) {
listeners.set(name, callback);
return () => listeners.delete(name);
},
},
window: { getCurrentWindow: () => ({}) },
};
const modules = new Map();
let imports = Promise.resolve();
const resolve = (specifier, owner) => specifier.startsWith('/')
? path.join(root, specifier) : fileURLToPath(new URL(specifier, owner.identifier));
const link = (specifier, owner) => getModule(resolve(specifier, owner));
function getModule(file) {
file = path.resolve(root, file);
if (modules.has(file)) return modules.get(file);
const module = new vm.SourceTextModule(readFileSync(file, 'utf8'), {
context: window,
identifier: pathToFileURL(file).href,
initializeImportMeta(meta) { meta.url = pathToFileURL(file).href; },
importModuleDynamically(specifier, owner) {
// Force Host-ready imports to finish before the bootloader starts script.js.
const pending = imports.then(() => load(resolve(specifier, owner)));
imports = pending.catch(() => {});
return pending;
},
});
modules.set(file, module);
return module;
}
async function load(file) {
const module = getModule(file);
if (module.status === 'unlinked') await module.link(link);
await module.evaluate();
return module;
}
async function startHost() {
await load('lib.js');
await load('tauri-main.js');
await window.__TAURITAVERN_MAIN_READY__;
let completed;
while (completed !== imports) {
completed = imports;
await completed;
}
}
return { window, listeners, getModule, load, startHost };
}