Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions common/test/worker-adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* @nevware21/ts-utils
* https://github.com/nevware21/ts-utils
*
* Copyright (c) 2026 NevWare21 Solutions LLC
* Licensed under the MIT license.
*/

/**
* Worker Test Adapter - Runs in main page context
* Creates Web Worker, loads tests, and reports results to Karma
*/

(function () {
"use strict";

function initWorkerAdapter() {
var karma = window.__karma__;

if (!karma) {
console.log("[worker-adapter] __karma__ not available yet, will retry");
setTimeout(initWorkerAdapter, 50);
return;
}

console.log("[worker-adapter] Starting worker test setup");

if (window.mocha && typeof window.mocha.run === "function") {
window.mocha.run = function () {
console.log("[worker-adapter] Mocha.run intercepted - running tests in worker");
startWorkerTests();
return {
on: function () { return this; }
};
};
} else {
console.log("[worker-adapter] Mocha not found");
}

function startWorkerTests() {
console.log("[worker-adapter] Initializing worker tests");

var bundleFiles = [];
var moduleShims = [];
var jsFiles = [];
var files = karma.files;
var mochaFile = null;
var commonjsFile = null;

console.log("[worker-adapter] Total files from karma: " + Object.keys(files).length);

for (var file in files) {
if (!files.hasOwnProperty(file)) {
continue;
}

if (/karma-typescript-bundle-.*\.js(\?|$)/.test(file)) {
bundleFiles.push(file);
continue;
}

if (!mochaFile && /\/node_modules\/mocha\/mocha\.js(\?|$)/.test(file)) {
mochaFile = file;
continue;
}

if (!commonjsFile && /\/node_modules\/karma-typescript\/dist\/client\/commonjs\.js(\?|$)/.test(file)) {
commonjsFile = file;
continue;
}

if (/\.js(\?|$)/.test(file) && file.indexOf("/base/") === 0) {
jsFiles.push(file);
continue;
}
}

var testFiles = [];
var sourceFiles = [];
var testSupportFiles = [];
for (var i = 0; i < jsFiles.length; i++) {
if (/\/lib\/test\/src\/.*\.test\.js(\?|$)/.test(jsFiles[i])) {
testFiles.push(jsFiles[i]);
} else if (/^\/base\/lib\/src\/.*\.js(\?|$)/.test(jsFiles[i])) {
sourceFiles.push(jsFiles[i]);
} else if (/^\/base\/lib\/test\/src\/.*\.js(\?|$)/.test(jsFiles[i])) {
testSupportFiles.push(jsFiles[i]);
}
}

console.log("[worker-adapter] Found " + bundleFiles.length + " bundle files, " +
sourceFiles.length + " source files, " +
testSupportFiles.length + " support files, " +
testFiles.length + " test files, and " +
moduleShims.length + " module shims");

var workerScript = null;
for (var fileName in files) {
if (files.hasOwnProperty(fileName) && fileName.indexOf("worker-test-runner.js") !== -1) {
workerScript = fileName;
break;
}
}

if (!workerScript) {
console.error("[worker-adapter] Could not find worker-test-runner.js");
karma.error("[worker-test] Worker runner not found");
karma.complete({});
return;
}

console.log("[worker-adapter] Found worker script at: " + workerScript);

try {
var worker = new Worker(workerScript);
console.log("[worker-adapter] Worker created");

worker.onmessage = function (event) {
var msg = event.data;

switch (msg.type) {
case "log":
console.log("[worker] " + msg.message);
break;

case "ready":
console.log("[worker-adapter] Worker ready, sending files");
var filesToSend = bundleFiles.concat(sourceFiles, testSupportFiles, testFiles);
console.log("[worker-adapter] Sending " + filesToSend.length + " total files to worker");
worker.postMessage({
type: "loadTests",
files: filesToSend,
entrypoints: [],
moduleShims: moduleShims,
basePath: (karma.config && karma.config.basePath) ? karma.config.basePath : "",
mochaUrl: mochaFile || "/base/node_modules/mocha/mocha.js",
commonjsUrl: commonjsFile || "/base/node_modules/karma-typescript/dist/client/commonjs.js"
});
break;

case "result":
karma.result(msg.result);
break;

case "complete":
karma.complete(msg.coverage ? { coverage: msg.coverage } : {});
break;
Comment on lines +145 to +147
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After receiving the worker "complete" message, the Web Worker is left running. In watch/debug sessions this can leak workers and keep resources alive. Consider terminating the worker after calling karma.complete (and similarly on error paths).

Copilot uses AI. Check for mistakes.

case "error":
console.error("[worker-adapter] Worker error: " + msg.error);
karma.error("[worker-test] " + msg.error);
karma.complete({});
break;
Comment on lines +149 to +153
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the worker "error" message, the worker is not terminated. Consider terminating the worker after reporting the error/complete to avoid leaking a broken worker instance in watch/debug runs.

Copilot uses AI. Check for mistakes.
}
};

worker.onerror = function (error) {
console.error("[worker-adapter] Worker error event: " + error.message);
karma.error("[worker-test] Worker error: " + error.message);
karma.complete({});
};
} catch (err) {
console.error("[worker-adapter] Failed to create worker: " + err.message);
karma.error("[worker-test] Failed to create worker: " + err.message);
karma.complete({});
}
}
}

initWorkerAdapter();
})();
218 changes: 218 additions & 0 deletions common/test/worker-test-runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/*
* @nevware21/ts-utils
* https://github.com/nevware21/ts-utils
*
* Copyright (c) 2026 NevWare21 Solutions LLC
* Licensed under the MIT license.
*/

/**
* Worker Test Runner - Runs in Web Worker context
* Loads and executes test files in worker environment
*/

(function () {
"use strict";

function sendMessage(msg) {
self.postMessage(msg);
}

function workerLog(message) {
sendMessage({
type: "log",
message: message
});
}

self.onmessage = function (event) {
var msg = event.data;

if (msg.type === "loadTests") {
loadAndRunTests(msg);
}
};

function loadAndRunTests(payload) {
var files = payload.files || [];
var requestedEntrypoints = payload.entrypoints || [];
var moduleShims = payload.moduleShims || [];
var basePath = payload.basePath || "";
var mochaUrl = payload.mochaUrl;
var commonjsUrl = payload.commonjsUrl;

workerLog("Loading " + files.length + " files and running " + requestedEntrypoints.length + " entrypoints");

if (!mochaUrl || !commonjsUrl) {
sendMessage({
type: "error",
error: "Missing mocha or commonjs runtime URL"
});
return;
}

try {
// Polyfill process for karma-typescript commonjs runtime
if (typeof self.process === "undefined") {
self.process = {
env: { NODE_ENV: "test" },
cwd: function () { return "/"; },
browser: true
};
}

importScripts(mochaUrl);
if (!self.mocha || !self.mocha.setup) {
throw new Error("Mocha did not load in worker");
}
self.mocha.setup({
ui: "bdd",
reporter: function () {}
});

self.wrappers = self.wrappers || {};

moduleShims.forEach(function (shim) {
importScripts(shim.file);
registerShimWrapper(shim.name, shim.file, basePath);
});

for (var i = 0; i < files.length; i++) {
importScripts(files[i]);
if (i % 50 === 0 || i === files.length - 1) {
workerLog("Loaded " + (i + 1) + "/" + files.length + " files");
}
}

var wrapperKeys = Object.keys(self.wrappers || {});
workerLog("Available wrappers: " + wrapperKeys.length);
if (wrapperKeys.length > 0) {
workerLog("Sample wrappers: " + wrapperKeys.slice(0, 3).join(", "));
}

var resolvedEntrypoints = wrapperKeys.filter(function (key) {
return /\/lib\/test\/src\/.*\.test\.(ts|js)$/.test(key);
});
workerLog("Found " + resolvedEntrypoints.length + " test wrappers");
if (resolvedEntrypoints.length === 0 && requestedEntrypoints.length > 0) {
workerLog("No wrapper entrypoints found; falling back to requested entrypoints");
self.entrypointFilenames = requestedEntrypoints.slice();
} else {
self.entrypointFilenames = resolvedEntrypoints;
}

importScripts(commonjsUrl);

var runner = self.mocha.run();

function getSuiteTitles(test) {
var titles = [];
var parent = test && test.parent;

while (parent) {
if (parent.title) {
titles.unshift(parent.title);
}
parent = parent.parent;
}

return titles;
}

runner.on("pass", function (test) {
var suiteTitles = getSuiteTitles(test);
sendMessage({
type: "result",
result: {
description: test.title,
suite: suiteTitles,
success: true,
skipped: false,
time: test.duration,
log: []
}
});
});

runner.on("pending", function (test) {
var suiteTitles = getSuiteTitles(test);
sendMessage({
type: "result",
result: {
description: test.title,
suite: suiteTitles,
success: true,
skipped: true,
log: []
}
});
});

runner.on("fail", function (test, err) {
var suiteTitles = getSuiteTitles(test);
sendMessage({
type: "result",
result: {
description: test.title,
suite: suiteTitles,
success: false,
skipped: false,
log: [err && err.message ? err.message : "Test failed"],
time: test.duration
}
});
});

runner.on("end", function () {
sendMessage({
type: "complete",
coverage: self.__coverage__
});
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The worker sends the final "complete" message but does not close itself, so it can remain alive and retain memory after the run. Consider calling self.close() (after posting the complete message) to terminate cleanly.

Suggested change
});
});
self.close();

Copilot uses AI. Check for mistakes.
});
} catch (err) {
sendMessage({
type: "error",
error: err.message || String(err)
});
}
}

function registerShimWrapper(name, file, basePath) {
var exportsObj = resolveShimExport(name);
if (!exportsObj) {
workerLog("Shim export not found for " + name);
return;
}
var absPath = toAbsolutePath(basePath, file);

self.wrappers[absPath] = [function (require, module, exports) {
module.exports = exportsObj;
}, absPath, {}];
}

function resolveShimExport(name) {
if (name === "ts-utils") {
return self.nevware21 && self.nevware21["ts-utils"];
}
if (name === "ts-async") {
return self.nevware21 && self.nevware21["ts-async"];
}
if (name === "chromacon") {
return self.nevware21 && self.nevware21.chromacon;
}
return null;
}

function toAbsolutePath(basePath, file) {
if (!basePath) {
return file;
}
var normalizedBase = basePath.replace(/\\/g, "/").replace(/\/$/, "");
if (file.indexOf("/base/") === 0) {
return normalizedBase + file.substring("/base".length);
}
return file;
}

sendMessage({ type: "ready" });
})();
Loading