test: cover frontend modules
browser-globals.test.js loads every page module through vm with a fake window, the way the pages do, and asserts the global each one exposes (HtmlEscape/escapeHtml, DataActions, ApiClient, ListState, SseStream, PngMetadata, ViewRouter, PromptListSort, ComfyMetadata). sse-stream gains cases for a POST body, missing handlers and close() before or during delivery. Every pure module is above 85% line coverage under node --test --experimental-test-coverage.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
// Run with: node --test tests/js/
|
||||
// Each page module is a UMD-style IIFE: CommonJS export under Node, a window
|
||||
// global in the browser. This loads every module the way a browser would and
|
||||
// checks the global name the pages rely on.
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
|
||||
const MODULES = {
|
||||
"html-escape.js": ["HtmlEscape", "escapeHtml", "escapeAttr"],
|
||||
"data-actions.js": ["DataActions"],
|
||||
"api-client.js": ["ApiClient"],
|
||||
"list-state.js": ["ListState"],
|
||||
"sse-stream.js": ["SseStream"],
|
||||
"png-metadata.js": ["PngMetadata"],
|
||||
"view-router.js": ["ViewRouter"],
|
||||
"prompt-list-sort.js": ["PromptListSort"],
|
||||
"comfy-metadata.js": ["ComfyMetadata"],
|
||||
};
|
||||
|
||||
function loadInFakeBrowser(file) {
|
||||
const source = fs.readFileSync(path.join(__dirname, "../../web/js", file), "utf8");
|
||||
const window = {};
|
||||
window.window = window;
|
||||
const context = vm.createContext({
|
||||
window,
|
||||
globalThis: window,
|
||||
console,
|
||||
TextDecoder,
|
||||
TextEncoder,
|
||||
AbortController,
|
||||
URLSearchParams,
|
||||
Symbol,
|
||||
Object,
|
||||
Array,
|
||||
Number,
|
||||
String,
|
||||
Math,
|
||||
Set,
|
||||
Map,
|
||||
JSON,
|
||||
Error,
|
||||
Promise,
|
||||
DataView,
|
||||
Uint8Array,
|
||||
ArrayBuffer,
|
||||
});
|
||||
vm.runInContext(source, context, { filename: file });
|
||||
return window;
|
||||
}
|
||||
|
||||
for (const [file, globals] of Object.entries(MODULES)) {
|
||||
test(`${file} exposes ${globals.join(", ")} on window when module is undefined`, () => {
|
||||
const window = loadInFakeBrowser(file);
|
||||
for (const name of globals) {
|
||||
assert.ok(window[name], `${name} missing`);
|
||||
assert.ok(["object", "function"].includes(typeof window[name]), `${name} is a ${typeof window[name]}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("the browser escapeHtml global is the same function as HtmlEscape.escapeHtml", () => {
|
||||
const window = loadInFakeBrowser("html-escape.js");
|
||||
assert.equal(window.escapeHtml, window.HtmlEscape.escapeHtml);
|
||||
assert.equal(window.escapeAttr, window.escapeHtml);
|
||||
assert.equal(window.escapeHtml(`<a href="x">`), "<a href="x">");
|
||||
});
|
||||
|
||||
test("the browser ViewRouter global resolves the tags deep link", () => {
|
||||
const window = loadInFakeBrowser("view-router.js");
|
||||
assert.equal(window.ViewRouter.resolveView("#/tags/a"), "tags");
|
||||
});
|
||||
@@ -104,6 +104,50 @@ test("close() aborts the request and suppresses later callbacks", async () => {
|
||||
assert.equal(errors, 0);
|
||||
});
|
||||
|
||||
test("connect forwards a request body and an Accept header", async () => {
|
||||
let init = null;
|
||||
const fetchImpl = async (_url, i) => { init = i; return new Response(streamOf([]), { status: 200 }); };
|
||||
const source = connect("/x", { method: "POST", body: '{"a":1}', fetchImpl });
|
||||
await until(() => source.readyState === source.CLOSED);
|
||||
assert.equal(init.body, '{"a":1}');
|
||||
assert.equal(init.headers.Accept, "text/event-stream");
|
||||
});
|
||||
|
||||
test("events arriving with no onmessage handler are dropped without error", async () => {
|
||||
const fetchImpl = async () => new Response(streamOf(["data: 1\n\n", "data: 2\n\n"]), { status: 200 });
|
||||
const source = connect("/x", { fetchImpl });
|
||||
let errors = 0;
|
||||
source.onerror = () => errors++;
|
||||
await until(() => source.readyState === source.CLOSED);
|
||||
assert.equal(errors, 0);
|
||||
});
|
||||
|
||||
test("close() during delivery stops further messages and skips onclose", async () => {
|
||||
const seen = [];
|
||||
let closed = false;
|
||||
const fetchImpl = async () => new Response(streamOf(["data: 1\n\ndata: 2\n\ndata: 3\n\n"]), { status: 200 });
|
||||
const source = connect("/x", { fetchImpl });
|
||||
source.onmessage = (e) => { seen.push(e.data); if (e.data === "1") source.close(); };
|
||||
source.onclose = () => { closed = true; };
|
||||
await until(() => source.readyState === source.CLOSED);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
assert.deepEqual(seen, ["1"]);
|
||||
assert.equal(closed, false);
|
||||
});
|
||||
|
||||
test("close() before the response arrives discards it silently", async () => {
|
||||
let resolveFetch;
|
||||
let errors = 0;
|
||||
const fetchImpl = () => new Promise((resolve) => { resolveFetch = resolve; });
|
||||
const source = connect("/x", { fetchImpl });
|
||||
source.onerror = () => errors++;
|
||||
source.close();
|
||||
resolveFetch(new Response(streamOf(["data: late\n\n"]), { status: 200 }));
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
assert.equal(errors, 0);
|
||||
assert.equal(source.readyState, source.CLOSED);
|
||||
});
|
||||
|
||||
test("connect defaults to GET and the global fetch", async () => {
|
||||
const original = globalThis.fetch;
|
||||
let init = null;
|
||||
|
||||
Reference in New Issue
Block a user