@netatwork/mocha-utils
Version:
Utility package for mocha based test setup
225 lines (205 loc) • 8.45 kB
JavaScript
/* eslint-disable @typescript-eslint/explicit-function-return-type */
// @ts-check
// Note: As this file is directly served in the browser, this is intentionally written in JavaScript, and devoid of any external deps.
try {
/**
* @typedef {Window & { mocha: Mocha; Mocha: typeof Mocha }} MochaWindow
*/
const windw = ( /** @type {MochaWindow} */( /** @type {unknown} */(window)));
const {
EVENT_RUN_END,
EVENT_SUITE_BEGIN,
EVENT_SUITE_END,
EVENT_TEST_FAIL,
EVENT_TEST_PASS,
EVENT_TEST_PENDING,
} = windw.Mocha.Runner.constants;
/** @type {typeof import('mocha').reporters.Base} */
let OriginalReporterFunction;
/**
*
*
* @class MochaReporter
*/
class MochaReporter {
/** @type {import('mocha').reporters.Base} */
_originalReporter;
/** @type {HTMLDivElement | undefined} */
dots;
/** @type {HTMLDivElement | undefined} */
summary;
/** @type {HTMLUListElement | undefined} */
specs;
/**
* @param {import('mocha').Runner} runner
*/
constructor(runner) {
// this will retain the original reporter functionality as well.
this._originalReporter = new OriginalReporterFunction(runner);
const container = this.createResultContainer();
if (container === null) { return; }
let currentSuiteContainer = /** @type {HTMLUListElement} */ (this.specs);
runner
.on(EVENT_SUITE_BEGIN, (suite) => {
if (suite.root) { return; }
const item = this.createListItem(suite.title);
const list = document.createElement('ul');
item.appendChild(list);
currentSuiteContainer.appendChild(item);
currentSuiteContainer = list;
})
.on(EVENT_SUITE_END, () => {
currentSuiteContainer = /** @type {HTMLUListElement} */ (currentSuiteContainer.parentElement);
})
.on(EVENT_TEST_PASS, /** @param {import('mocha').Test} test */(test) => {
this.append(test, currentSuiteContainer);
})
.on(EVENT_TEST_PENDING, /** @param {import('mocha').Test} test */(test) => {
this.append(test, currentSuiteContainer);
})
.on(EVENT_TEST_FAIL, /** @param {import('mocha').Test} test */(test, error) => {
this.append(test, currentSuiteContainer, error);
})
.once(EVENT_RUN_END, () => {
const stats = /** @type {Mocha.Stats} */ (runner.stats);
const span = document.createElement('span');
const numFailures = stats.failures ?? 0;
span.textContent = (numFailures > 0 ? numFailures + ' failures. ' : '')
+ stats.passes
+ ' of '
+ stats.tests
+ ' tests passed.'
+ ((stats.pending ?? 0) > 0 ? ' ' + stats.pending + ' pending.' : '');
const time = document.createElement('span');
time.textContent = `Finished in ${(stats?.duration || 0) / 1000} s`;
const summary = /** @type {HTMLDivElement} */ (this.summary);
summary.append(span, time);
summary.dataset[numFailures ? 'failure' : 'success'] = '';
const ulDiv = document.createElement('div');
ulDiv.classList.add('ul-container');
ulDiv.append(/** @type {HTMLUListElement} */(this.specs));
container.append(
/** @type {HTMLDivElement} */(summary),
ulDiv
);
});
}
/**
* @returns {(HTMLDivElement | null)}
*/
createResultContainer() {
try {
const container = document.createElement('div');
container.classList.add('kmhtml');
const dots = this.dots = document.createElement('div');
dots.classList.add('dots');
this.summary = document.createElement('div');
this.summary.classList.add('summary');
this.specs = document.createElement('ul');
container.appendChild(dots);
document.body.appendChild(container);
return container;
} catch (e) {
return null;
}
}
/**
* @param {import('mocha').Test} test
* @param {HTMLUListElement} suiteContainer
* @param {Error | undefined} error
*/
append(
test,
suiteContainer,
error = undefined,
) {
let $class = '';
switch (true) {
case test.isPassed():
$class = 'passed';
break;
case test.isFailed():
$class = 'failed';
break;
case test.isPending():
$class = 'pending';
break;
}
const title = test.fullTitle();
const span = document.createElement('span');
span.innerHTML = '•';
span.title = title;
span.classList.add($class, 'dot');
span.style.color = $class;
(/** @type {HTMLDivElement} */(this.dots)).appendChild(span);
const specResult = this.createSpecResult(test, $class, error);
suiteContainer.appendChild(specResult);
span.addEventListener('click', () => {
specResult.scrollIntoView();
specResult.querySelector('a')?.focus();
});
}
/**
* @param {import('mocha').Test} test
* @param {string} [$class='']
* @param {Error | undefined} error
* @returns {HTMLLIElement}
*/
createSpecResult(
test,
$class = '',
error = undefined,
) {
const li = this.createListItem(test.title, test.fullTitle(), $class);
if (test.isFailed()) {
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = 'Error(s)';
details.append(summary);
const errDiv = document.createElement('pre');
const err = /** @type {Error} */(error);
errDiv.innerHTML = err.message + '\n' + err.stack;
details.append(errDiv);
li.append(details);
}
return li;
}
/**
* @param {string} title
* @param {string} [fullTitle=title]
* @param {string} [$class='']
*/
createListItem(title, fullTitle = title, $class = '') {
const li = document.createElement('li');
if ($class) {
li.classList.add($class);
}
const anchor = document.createElement('a');
anchor.innerText = title;
const loc = new URL(location.href);
loc.pathname = 'debug.html';
loc.searchParams.set('grep', fullTitle);
anchor.href = loc.href;
anchor.target = '_blank';
li.appendChild(anchor);
return li;
}
}
const mocha = windw.mocha;
OriginalReporterFunction = /** @type {typeof import('mocha').reporters.Base} */ (mocha['_reporter']);
mocha['_reporter'] = MochaReporter;
const karmaRoot = window.parent.document;
karmaRoot.head.querySelectorAll('[data-karma-css-fix]').forEach(e => e.remove());
const karmaCssFix = karmaRoot.createElement('style');
karmaCssFix.dataset.karmaCssFix = '';
karmaCssFix.innerHTML = `
body {
display: flex;
flex-direction: column;
height: 100dvh;
}
`;
karmaRoot.head.appendChild(karmaCssFix);
} catch (e) {
console.log('Not running the kmhtml reporter. Note that this is a dev-only reporter, meant to be used in browser.');
}