mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[eric] frontend: port the esbuild test runner from the debt branch so npm test works here; 139 tests green
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
"build": "webpack --mode=production",
|
||||
"build:watch": "webpack --mode=development --watch",
|
||||
"dev": "webpack serve --mode=development",
|
||||
"clean": "rm -rf dist"
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node scripts/run-tests.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Frontend test runner, zero new dependencies: esbuild (already a dependency of
|
||||
// the build chain) bundles each *.test.ts(x) with its imports and path aliases
|
||||
// resolved, then Node's built-in test runner executes the result.
|
||||
//
|
||||
// Why not jest/vitest: this worktree's node_modules is shared, and the whole job
|
||||
// here is running our own pure logic. If component/DOM tests are ever needed,
|
||||
// that is the moment to add a real DOM environment, not before.
|
||||
import { build } from 'esbuild';
|
||||
import { globSync } from 'node:fs';
|
||||
import { mkdirSync, rmSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.dirname(fileURLToPath(import.meta.url)) + '/..';
|
||||
const tests = globSync('src/**/*.test.{ts,tsx}', { cwd: root }).sort();
|
||||
if (tests.length === 0) {
|
||||
console.error('no test files found (src/**/*.test.ts(x))');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build INSIDE the package, not tmpdir: anything left external (react-dom/server) has to
|
||||
// resolve through frontend/node_modules, and a tmpdir has no node_modules to walk up to.
|
||||
// One fixed dir, wiped up front, so a Ctrl-C'd run litters the repo once instead of forever.
|
||||
let status = 1;
|
||||
const outDir = path.join(root, '.test-build');
|
||||
rmSync(outDir, { recursive: true, force: true });
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
try {
|
||||
await build({
|
||||
entryPoints: tests.map((t) => path.join(root, t)),
|
||||
outdir: outDir,
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
target: 'node22',
|
||||
sourcemap: 'inline',
|
||||
logLevel: 'warning',
|
||||
// node builtins stay external, and so does react-dom/server: esbuild's CJS interop
|
||||
// cannot follow its conditional require chain, so bundling it dies at import time.
|
||||
// react rides along as external for a subtler reason: react-dom/server resolves its OWN react
|
||||
// from node_modules, so bundling a second copy leaves the hook dispatcher null and every
|
||||
// component that calls useContext dies with "Cannot read properties of null".
|
||||
external: ['node:*', 'react-dom/server', 'react'],
|
||||
alias: { '@': path.join(root, 'src'), '@toolui': path.join(root, 'src/toolui') },
|
||||
loader: { '.css': 'empty', '.svg': 'empty', '.png': 'empty', '.woff2': 'empty', '.mp4': 'empty' },
|
||||
});
|
||||
const built = globSync('**/*.mjs', { cwd: outDir }).concat(globSync('**/*.js', { cwd: outDir }));
|
||||
const setup = path.join(root, 'scripts/test-globals.mjs');
|
||||
const res = spawnSync(process.execPath, ['--import', setup, '--test', ...built.map((f) => path.join(outDir, f))],
|
||||
{ stdio: 'inherit', cwd: root });
|
||||
status = res.status ?? 1;
|
||||
} finally {
|
||||
// Exit AFTER this block, never inside the try: process.exit() skips finally outright,
|
||||
// which is how every single run used to leave its build dir behind.
|
||||
rmSync(outDir, { recursive: true, force: true });
|
||||
}
|
||||
process.exit(status);
|
||||
@@ -0,0 +1,54 @@
|
||||
// Just enough browser surface for a module that touches `window` at import time
|
||||
// to LOAD inside node:test. This is deliberately not a DOM: if a test ever needs
|
||||
// real rendering, that is the moment to bring in a proper DOM environment.
|
||||
const noop = () => {};
|
||||
const store = new Map();
|
||||
const storage = {
|
||||
getItem: (k) => (store.has(String(k)) ? store.get(String(k)) : null),
|
||||
setItem: (k, v) => void store.set(String(k), String(v)),
|
||||
removeItem: (k) => void store.delete(String(k)),
|
||||
clear: () => void store.clear(),
|
||||
key: (i) => [...store.keys()][i] ?? null,
|
||||
get length() { return store.size; },
|
||||
};
|
||||
const el = () => ({
|
||||
style: {}, classList: { add: noop, remove: noop, contains: () => false, toggle: noop },
|
||||
setAttribute: noop, getAttribute: () => null, removeAttribute: noop,
|
||||
appendChild: noop, removeChild: noop, addEventListener: noop, removeEventListener: noop,
|
||||
getBoundingClientRect: () => ({ x: 0, y: 0, top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0 }),
|
||||
querySelector: () => null, querySelectorAll: () => [], contains: () => false, focus: noop, click: noop,
|
||||
dataset: {}, children: [], parentElement: null, textContent: '',
|
||||
});
|
||||
const doc = {
|
||||
...el(),
|
||||
documentElement: el(), body: el(), head: el(),
|
||||
createElement: el, createTextNode: () => ({}), getElementById: () => null,
|
||||
readyState: 'complete', visibilityState: 'visible', cookie: '',
|
||||
};
|
||||
const observer = class { observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } };
|
||||
const notWired = () => { throw new Error('network is not wired in tests; stub the module under test'); };
|
||||
const win = {
|
||||
document: doc, localStorage: storage, sessionStorage: storage,
|
||||
location: { href: 'http://localhost/', pathname: '/', search: '', hash: '', origin: 'http://localhost' },
|
||||
navigator: { userAgent: 'node', onLine: true, language: 'en-US', clipboard: { writeText: async () => {} } },
|
||||
addEventListener: noop, removeEventListener: noop, dispatchEvent: () => true,
|
||||
matchMedia: () => ({ matches: false, addEventListener: noop, removeEventListener: noop, addListener: noop, removeListener: noop }),
|
||||
getComputedStyle: () => ({ getPropertyValue: () => '' }),
|
||||
requestAnimationFrame: (cb) => setTimeout(() => cb(Date.now()), 0),
|
||||
cancelAnimationFrame: (id) => clearTimeout(id),
|
||||
innerWidth: 1440, innerHeight: 900, devicePixelRatio: 2,
|
||||
scrollTo: noop, open: () => null, alert: noop, confirm: () => false,
|
||||
fetch: globalThis.fetch ?? notWired, WebSocket: class { close() {} send() {} addEventListener() {} removeEventListener() {} },
|
||||
ResizeObserver: observer, IntersectionObserver: observer, MutationObserver: observer,
|
||||
performance: globalThis.performance, crypto: globalThis.crypto,
|
||||
setTimeout, clearTimeout, setInterval, clearInterval,
|
||||
};
|
||||
win.window = win; win.self = win; win.top = win; win.parent = win;
|
||||
for (const [k, v] of Object.entries({ window: win, document: doc, navigator: win.navigator,
|
||||
localStorage: storage, sessionStorage: storage, location: win.location,
|
||||
matchMedia: win.matchMedia, getComputedStyle: win.getComputedStyle,
|
||||
requestAnimationFrame: win.requestAnimationFrame, cancelAnimationFrame: win.cancelAnimationFrame,
|
||||
ResizeObserver: observer, IntersectionObserver: observer, MutationObserver: observer,
|
||||
WebSocket: win.WebSocket })) {
|
||||
if (!(k in globalThis)) Object.defineProperty(globalThis, k, { value: v, writable: true, configurable: true });
|
||||
}
|
||||
Reference in New Issue
Block a user