Files
tbd/ui/test/dom-setup.cjs
ajet 47f6cbf2d1
CI / Test + coverage (push) Successful in 1m33s
add ui tests to ci
2026-09-19 19:17:22 -09:00

67 lines
2.9 KiB
JavaScript

// Preloaded by `npm test` (node -r) before any compiled code runs: gives the ClojureScript
// tests a browser-like global scope (window, document, location, ...) backed by jsdom.
//
// It has to happen first: React DOM decides at load time whether the browser supports the
// `input` event; without a `window` then, <input onChange> never fires under test.
const { JSDOM } = require("jsdom");
const { window } = new JSDOM('<!doctype html><html><body><div id="app"></div></body></html>', {
url: "http://localhost:3000/",
});
// Copy the window's globals over, but leave Node's own (fetch, setTimeout, navigator, ...) alone.
for (const key of Object.getOwnPropertyNames(window)) {
if (!(key in globalThis)) {
Object.defineProperty(globalThis, key, {
configurable: true,
get: () => window[key],
set: (value) => Object.defineProperty(globalThis, key, { value, writable: true, configurable: true }),
});
}
}
globalThis.window = window;
// WebMCP, for `require("@jason.today/webmcp/src/webmcp.js")`. Its only export is
// `module.exports = WebMCP`, guarded by `typeof module`, in a file its package.json declares as an
// ES module. The browser build wraps it as CommonJS, so there it exports the class; Node's
// require() loads it as ESM, where `module` doesn't exist, and gets nothing. So run the same
// source with a `module` to export to, and hand that to require.
const Module = require("module");
const fs = require("fs");
const webmcp = require.resolve("@jason.today/webmcp/src/webmcp.js");
const cjs = new Module(webmcp);
cjs.filename = webmcp;
new Function("module", "exports", fs.readFileSync(webmcp, "utf8"))(cjs, cjs.exports);
cjs.loaded = true;
require.cache[webmcp] = cjs;
// A run that doesn't finish must not look like a pass. An async test that throws before it calls
// `done` (or whose promise chain rejects) never ends: the process then hangs, or runs out of things
// to do and exits 0 without a word. Fail instead, and say which test it was.
function fail(message) {
console.error(`\nFAILED: ${message}`);
process.exit(1);
}
process.on("unhandledRejection", (reason) =>
fail(`unhandled promise rejection (an async test without a catch?): ${(reason && reason.stack) || reason}`));
// tbd.ui.test-util keeps `uiTestsCurrent` up to date: the test that is running, and since when.
const STUCK_MS = 30000;
globalThis.uiTestsCurrent = { name: "loading the tests", since: Date.now() };
setInterval(() => {
const { name, since } = globalThis.uiTestsCurrent;
if (Date.now() - since > STUCK_MS) {
fail(`stuck for ${STUCK_MS / 1000}s in ${name} (an async test that never called done?)`);
}
}, 1000).unref();
// ... and sets `uiTestsFinished` once cljs.test has printed its summary.
process.on("exit", (code) => {
if (code === 0 && !globalThis.uiTestsFinished) {
console.error("\nFAILED: the tests ended without reporting a result");
process.exitCode = 1;
}
});