Move HtmlReporter components to their own files

This commit is contained in:
Steve Gravrock
2025-10-07 20:19:05 -07:00
parent 01f050eeaa
commit bd89ef66c8
11 changed files with 1670 additions and 1554 deletions

File diff suppressed because it is too large Load Diff

208
src/html/AlertsView.js Normal file
View File

@@ -0,0 +1,208 @@
jasmineRequire.AlertsView = function(j$) {
'use strict';
const { createDom } = j$.private.htmlReporterUtils;
const errorBarClassName = 'jasmine-bar jasmine-errored';
const afterAllMessagePrefix = 'AfterAll ';
class AlertsView {
#urlBuilder;
constructor(urlBuilder) {
this.#urlBuilder = urlBuilder;
this.rootEl = createDom('div', { className: 'jasmine-alert' });
}
addDuration(ms) {
this.add('jasmine-duration', 'finished in ' + ms / 1000 + 's');
}
addSkipped(numExecuted, numDefined) {
// TODO: backfill tests for this
this.add(
'jasmine-bar jasmine-skipped',
createDom(
'a',
{ href: this.#urlBuilder.runAllHref(), title: 'Run all specs' },
`Ran ${numExecuted} of ${numDefined} specs - run all`
)
);
}
addFailureToggle(onClickFailures, onClickSpecList) {
const failuresLink = createDom(
'a',
{ className: 'jasmine-failures-menu', href: '#' },
'Failures'
);
let specListLink = createDom(
'a',
{ className: 'jasmine-spec-list-menu', href: '#' },
'Spec List'
);
failuresLink.onclick = function() {
onClickFailures();
return false;
};
specListLink.onclick = function() {
onClickSpecList();
return false;
};
this.add('jasmine-menu jasmine-bar jasmine-spec-list', [
createDom('span', {}, 'Spec List | '),
failuresLink
]);
this.add('jasmine-menu jasmine-bar jasmine-failure-list', [
specListLink,
createDom('span', {}, ' | Failures ')
]);
}
addGlobalFailure(failure) {
this.add(errorBarClassName, this.#globalFailureMessage(failure));
}
// TODO check test coverage
addSeedBar(doneResult, stateBuilder, order) {
let statusBarMessage = '';
let statusBarClassName = 'jasmine-overall-result jasmine-bar ';
const globalFailures =
(doneResult && doneResult.failedExpectations) || [];
const failed = stateBuilder.failureCount + globalFailures.length > 0;
if (stateBuilder.totalSpecsDefined > 0 || failed) {
statusBarMessage +=
pluralize('spec', stateBuilder.specsExecuted) +
', ' +
pluralize('failure', stateBuilder.failureCount);
if (stateBuilder.pendingSpecCount) {
statusBarMessage +=
', ' + pluralize('pending spec', stateBuilder.pendingSpecCount);
}
}
if (doneResult.overallStatus === 'passed') {
statusBarClassName += ' jasmine-passed ';
} else if (doneResult.overallStatus === 'incomplete') {
statusBarClassName += ' jasmine-incomplete ';
statusBarMessage =
'Incomplete: ' +
doneResult.incompleteReason +
', ' +
statusBarMessage;
} else {
statusBarClassName += ' jasmine-failed ';
}
let seedBar;
if (order && order.random) {
seedBar = createDom(
'span',
{ className: 'jasmine-seed-bar' },
', randomized with seed ',
createDom(
'a',
{
title: 'randomized with seed ' + order.seed,
href: this.#urlBuilder.seedHref(order.seed)
},
order.seed
)
);
}
this.add(statusBarClassName, [statusBarMessage, seedBar]);
}
// TODO check test coverage
#globalFailureMessage(failure) {
if (failure.globalErrorType === 'load') {
const prefix = 'Error during loading: ' + failure.message;
if (failure.filename) {
return prefix + ' in ' + failure.filename + ' line ' + failure.lineno;
} else {
return prefix;
}
} else if (failure.globalErrorType === 'afterAll') {
return afterAllMessagePrefix + failure.message;
} else {
return failure.message;
}
}
addDeprecationWarning(dw) {
const children = [];
let context;
switch (dw.runnableType) {
case 'spec':
context = '(in spec: ' + dw.runnableName + ')';
break;
case 'suite':
context = '(in suite: ' + dw.runnableName + ')';
break;
default:
context = '';
}
for (const line of dw.message.split('\n')) {
children.push(line);
children.push(createDom('br'));
}
children[0] = 'DEPRECATION: ' + children[0];
children.push(context);
if (dw.stack) {
children.push(this.#createExpander(dw.stack));
}
this.add('jasmine-bar jasmine-warning', children);
}
// TODO private?
add(className, children) {
this.rootEl.appendChild(createDom('span', { className }, children));
}
#createExpander(stackTrace) {
const expandLink = createDom('a', { href: '#' }, 'Show stack trace');
const root = createDom(
'div',
{ className: 'jasmine-expander' },
expandLink,
createDom(
'div',
{ className: 'jasmine-expander-contents jasmine-stack-trace' },
stackTrace
)
);
expandLink.addEventListener('click', function(e) {
e.preventDefault();
if (root.classList.contains('jasmine-expanded')) {
root.classList.remove('jasmine-expanded');
expandLink.textContent = 'Show stack trace';
} else {
root.classList.add('jasmine-expanded');
expandLink.textContent = 'Hide stack trace';
}
});
return root;
}
}
function pluralize(singular, count) {
const word = count == 1 ? singular : singular + 's';
return '' + count + ' ' + word;
}
return AlertsView;
};

153
src/html/Banner.js Normal file
View File

@@ -0,0 +1,153 @@
jasmineRequire.Banner = function(j$) {
'use strict';
const { createDom } = j$.private.htmlReporterUtils;
class Banner {
#navigateWithNewParam;
constructor(navigateWithNewParam) {
this.#navigateWithNewParam = navigateWithNewParam;
this.rootEl = createDom(
'div',
{ className: 'jasmine-banner' },
createDom('a', {
className: 'jasmine-title',
href: 'http://jasmine.github.io/',
target: '_blank'
}),
createDom('span', { className: 'jasmine-version' }, j$.version)
);
}
showOptionsMenu(config) {
this.rootEl.appendChild(this.#optionsMenu(config));
}
#optionsMenu(config) {
const optionsMenuDom = createDom(
'div',
{ className: 'jasmine-run-options' },
createDom('span', { className: 'jasmine-trigger' }, 'Options'),
createDom(
'div',
{ className: 'jasmine-payload' },
createDom(
'div',
{ className: 'jasmine-stop-on-failure' },
createDom('input', {
className: 'jasmine-fail-fast',
id: 'jasmine-fail-fast',
type: 'checkbox'
}),
createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-fail-fast' },
'stop execution on spec failure'
)
),
createDom(
'div',
{ className: 'jasmine-throw-failures' },
createDom('input', {
className: 'jasmine-throw',
id: 'jasmine-throw-failures',
type: 'checkbox'
}),
createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-throw-failures' },
'stop spec on expectation failure'
)
),
createDom(
'div',
{ className: 'jasmine-random-order' },
createDom('input', {
className: 'jasmine-random',
id: 'jasmine-random-order',
type: 'checkbox'
}),
createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-random-order' },
'run tests in random order'
)
),
createDom(
'div',
{ className: 'jasmine-hide-disabled' },
createDom('input', {
className: 'jasmine-disabled',
id: 'jasmine-hide-disabled',
type: 'checkbox'
}),
createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-hide-disabled' },
'hide disabled tests'
)
)
)
);
const failFastCheckbox = optionsMenuDom.querySelector(
'#jasmine-fail-fast'
);
failFastCheckbox.checked = config.stopOnSpecFailure;
failFastCheckbox.onclick = () => {
this.#navigateWithNewParam(
'stopOnSpecFailure',
!config.stopOnSpecFailure
);
};
const throwCheckbox = optionsMenuDom.querySelector(
'#jasmine-throw-failures'
);
throwCheckbox.checked = config.stopSpecOnExpectationFailure;
throwCheckbox.onclick = () => {
this.#navigateWithNewParam(
'stopSpecOnExpectationFailure',
!config.stopSpecOnExpectationFailure
);
};
const randomCheckbox = optionsMenuDom.querySelector(
'#jasmine-random-order'
);
randomCheckbox.checked = config.random;
randomCheckbox.onclick = () => {
this.#navigateWithNewParam('random', !config.random);
};
const hideDisabled = optionsMenuDom.querySelector(
'#jasmine-hide-disabled'
);
hideDisabled.checked = config.hideDisabled;
// TODO: backfill tests for this!
hideDisabled.onclick = () => {
this.#navigateWithNewParam('hideDisabled', !config.hideDisabled);
};
const optionsTrigger = optionsMenuDom.querySelector('.jasmine-trigger'),
optionsPayload = optionsMenuDom.querySelector('.jasmine-payload'),
isOpen = /\bjasmine-open\b/;
optionsTrigger.onclick = function() {
if (isOpen.test(optionsPayload.className)) {
optionsPayload.className = optionsPayload.className.replace(
isOpen,
''
);
} else {
optionsPayload.className += ' jasmine-open';
}
};
return optionsMenuDom;
}
}
return Banner;
};

161
src/html/FailuresView.js Normal file
View File

@@ -0,0 +1,161 @@
jasmineRequire.FailuresView = function(j$) {
'use strict';
const { createDom } = j$.private.htmlReporterUtils;
class FailuresView {
#urlBuilder;
#failureEls;
constructor(urlBuilder) {
this.#urlBuilder = urlBuilder;
this.#failureEls = [];
this.rootEl = createDom(
'div',
{ className: 'jasmine-results' },
createDom('div', { className: 'jasmine-failures' })
);
}
append(result, parent) {
// TODO: Figure out why the reuslt is wrong if we build the DOM node later
this.#failureEls.push(this.#makeFailureEl(result, parent));
}
// TODO move this to state builder or something
any() {
return this.#failureEls.length > 0;
}
show() {
const failureNode = this.rootEl.querySelector('.jasmine-failures');
for (const el of this.#failureEls) {
failureNode.appendChild(el);
}
}
#makeFailureEl(result, parent) {
const failure = createDom(
'div',
{ className: 'jasmine-spec-detail jasmine-failed' },
this.#failureDescription(result, parent),
createDom('div', { className: 'jasmine-messages' })
);
const messages = failure.childNodes[1];
for (let i = 0; i < result.failedExpectations.length; i++) {
const expectation = result.failedExpectations[i];
messages.appendChild(
createDom(
'div',
{ className: 'jasmine-result-message' },
expectation.message
)
);
messages.appendChild(
createDom(
'div',
{ className: 'jasmine-stack-trace' },
expectation.stack
)
);
}
if (result.failedExpectations.length === 0) {
messages.appendChild(
createDom(
'div',
{ className: 'jasmine-result-message' },
'Spec has no expectations'
)
);
}
if (result.debugLogs) {
messages.appendChild(this.#debugLogTable(result.debugLogs));
}
return failure;
}
#failureDescription(result, suite) {
const wrapper = createDom(
'div',
{ className: 'jasmine-description' },
createDom(
'a',
{
title: result.description,
href: this.#urlBuilder.specHref(result)
},
result.description
)
);
let suiteLink;
while (suite && suite.parent) {
wrapper.insertBefore(
document.createTextNode(' > '),
wrapper.firstChild
);
suiteLink = createDom(
'a',
{ href: this.#urlBuilder.suiteHref(suite) },
suite.result.description
);
wrapper.insertBefore(suiteLink, wrapper.firstChild);
suite = suite.parent;
}
return wrapper;
}
#debugLogTable(debugLogs) {
const tbody = createDom('tbody');
for (const entry of debugLogs) {
tbody.appendChild(
createDom(
'tr',
{},
createDom('td', {}, entry.timestamp.toString()),
createDom(
'td',
{ className: 'jasmine-debug-log-msg' },
entry.message
)
)
);
}
return createDom(
'div',
{ className: 'jasmine-debug-log' },
createDom(
'div',
{ className: 'jasmine-debug-log-header' },
'Debug logs'
),
createDom(
'table',
{},
createDom(
'thead',
{},
createDom(
'tr',
{},
createDom('th', {}, 'Time (ms)'),
createDom('th', {}, 'Message')
)
),
tbody
)
);
}
}
return FailuresView;
};

View File

@@ -1,80 +1,7 @@
jasmineRequire.HtmlReporter = function(j$) {
'use strict';
class ResultsStateBuilder {
constructor() {
this.topResults = new j$.private.ResultsNode({}, '', null);
this.currentParent = this.topResults;
this.totalSpecsDefined = 0;
this.specsExecuted = 0;
this.failureCount = 0;
this.pendingSpecCount = 0;
this.deprecationWarnings = [];
}
suiteStarted(result) {
this.currentParent.addChild(result, 'suite');
this.currentParent = this.currentParent.last();
}
suiteDone(result) {
this.currentParent.updateResult(result);
this.#addDeprecationWarnings(result, 'suite');
if (this.currentParent !== this.topResults) {
this.currentParent = this.currentParent.parent;
}
if (result.status === 'failed') {
this.failureCount++;
}
}
specDone(result) {
this.currentParent.addChild(result, 'spec');
this.#addDeprecationWarnings(result, 'spec');
if (result.status !== 'excluded') {
this.specsExecuted++;
}
if (result.status === 'failed') {
this.failureCount++;
}
if (result.status == 'pending') {
this.pendingSpecCount++;
}
}
jasmineStarted(result) {
this.totalSpecsDefined = result.totalSpecsDefined;
}
jasmineDone(result) {
if (result.failedExpectations) {
this.failureCount += result.failedExpectations.length;
}
this.#addDeprecationWarnings(result);
}
#addDeprecationWarnings(result, runnableType) {
if (result.deprecationWarnings) {
for (const dw of result.deprecationWarnings) {
this.deprecationWarnings.push({
message: dw.message,
stack: dw.stack,
runnableName: result.fullName,
runnableType: runnableType
});
}
}
}
}
const errorBarClassName = 'jasmine-bar jasmine-errored';
const afterAllMessagePrefix = 'AfterAll ';
const { createDom, noExpectations } = j$.private.htmlReporterUtils;
/**
* @class HtmlReporter
@@ -105,7 +32,7 @@ jasmineRequire.HtmlReporter = function(j$) {
this.#getContainer = options.getContainer;
this.#navigateWithNewParam =
options.navigateWithNewParam || function() {};
this.#urlBuilder = new UrlBuilder(
this.#urlBuilder = new j$.private.UrlBuilder(
options.addToExistingQueryString || defaultQueryString
);
this.#filterSpecs = options.filterSpecs;
@@ -120,13 +47,13 @@ jasmineRequire.HtmlReporter = function(j$) {
this.#clearPrior();
this.#config = this.#env ? this.#env.configuration() : {};
this.#stateBuilder = new ResultsStateBuilder();
this.#stateBuilder = new j$.private.ResultsStateBuilder();
this.#alerts = new AlertsView(this.#urlBuilder);
this.#symbols = new SymbolsView();
this.#banner = new Banner(this.#navigateWithNewParam);
this.#failures = new FailuresView(this.#urlBuilder);
this.#htmlReporterMain = j$.private.createDom(
this.#alerts = new j$.private.AlertsView(this.#urlBuilder);
this.#symbols = new j$.private.SymbolsView();
this.#banner = new j$.private.Banner(this.#navigateWithNewParam);
this.#failures = new j$.private.FailuresView(this.#urlBuilder);
this.#htmlReporterMain = createDom(
'div',
{ className: 'jasmine_html-reporter' },
this.#banner.rootEl,
@@ -202,7 +129,10 @@ jasmineRequire.HtmlReporter = function(j$) {
}
const results = this.#find('.jasmine-results');
const summary = new SummaryTreeView(this.#urlBuilder, this.#filterSpecs);
const summary = new j$.private.SummaryTreeView(
this.#urlBuilder,
this.#filterSpecs
);
summary.addResults(this.#stateBuilder.topResults);
results.appendChild(summary.rootEl);
@@ -239,701 +169,9 @@ jasmineRequire.HtmlReporter = function(j$) {
}
}
function hasActiveSpec(resultNode) {
if (resultNode.type === 'spec' && resultNode.result.status !== 'excluded') {
return true;
}
if (resultNode.type === 'suite') {
for (let i = 0, j = resultNode.children.length; i < j; i++) {
if (hasActiveSpec(resultNode.children[i])) {
return true;
}
}
}
}
function noExpectations(result) {
const allExpectations =
result.failedExpectations.length + result.passedExpectations.length;
return (
allExpectations === 0 &&
(result.status === 'passed' || result.status === 'failed')
);
}
function pluralize(singular, count) {
const word = count == 1 ? singular : singular + 's';
return '' + count + ' ' + word;
}
function defaultQueryString(key, value) {
return '?' + key + '=' + value;
}
class AlertsView {
#urlBuilder;
constructor(urlBuilder) {
this.#urlBuilder = urlBuilder;
this.rootEl = j$.private.createDom('div', { className: 'jasmine-alert' });
}
addDuration(ms) {
this.add('jasmine-duration', 'finished in ' + ms / 1000 + 's');
}
addSkipped(numExecuted, numDefined) {
// TODO: backfill tests for this
this.add(
'jasmine-bar jasmine-skipped',
j$.private.createDom(
'a',
{ href: this.#urlBuilder.runAllHref(), title: 'Run all specs' },
`Ran ${numExecuted} of ${numDefined} specs - run all`
)
);
}
addFailureToggle(onClickFailures, onClickSpecList) {
const failuresLink = j$.private.createDom(
'a',
{ className: 'jasmine-failures-menu', href: '#' },
'Failures'
);
let specListLink = j$.private.createDom(
'a',
{ className: 'jasmine-spec-list-menu', href: '#' },
'Spec List'
);
failuresLink.onclick = function() {
onClickFailures();
return false;
};
specListLink.onclick = function() {
onClickSpecList();
return false;
};
this.add('jasmine-menu jasmine-bar jasmine-spec-list', [
j$.private.createDom('span', {}, 'Spec List | '),
failuresLink
]);
this.add('jasmine-menu jasmine-bar jasmine-failure-list', [
specListLink,
j$.private.createDom('span', {}, ' | Failures ')
]);
}
addGlobalFailure(failure) {
this.add(errorBarClassName, this.#globalFailureMessage(failure));
}
// TODO check test coverage
addSeedBar(doneResult, stateBuilder, order) {
let statusBarMessage = '';
let statusBarClassName = 'jasmine-overall-result jasmine-bar ';
const globalFailures =
(doneResult && doneResult.failedExpectations) || [];
const failed = stateBuilder.failureCount + globalFailures.length > 0;
if (stateBuilder.totalSpecsDefined > 0 || failed) {
statusBarMessage +=
pluralize('spec', stateBuilder.specsExecuted) +
', ' +
pluralize('failure', stateBuilder.failureCount);
if (stateBuilder.pendingSpecCount) {
statusBarMessage +=
', ' + pluralize('pending spec', stateBuilder.pendingSpecCount);
}
}
if (doneResult.overallStatus === 'passed') {
statusBarClassName += ' jasmine-passed ';
} else if (doneResult.overallStatus === 'incomplete') {
statusBarClassName += ' jasmine-incomplete ';
statusBarMessage =
'Incomplete: ' +
doneResult.incompleteReason +
', ' +
statusBarMessage;
} else {
statusBarClassName += ' jasmine-failed ';
}
let seedBar;
if (order && order.random) {
seedBar = j$.private.createDom(
'span',
{ className: 'jasmine-seed-bar' },
', randomized with seed ',
j$.private.createDom(
'a',
{
title: 'randomized with seed ' + order.seed,
href: this.#urlBuilder.seedHref(order.seed)
},
order.seed
)
);
}
this.add(statusBarClassName, [statusBarMessage, seedBar]);
}
// TODO check test coverage
#globalFailureMessage(failure) {
if (failure.globalErrorType === 'load') {
const prefix = 'Error during loading: ' + failure.message;
if (failure.filename) {
return prefix + ' in ' + failure.filename + ' line ' + failure.lineno;
} else {
return prefix;
}
} else if (failure.globalErrorType === 'afterAll') {
return afterAllMessagePrefix + failure.message;
} else {
return failure.message;
}
}
addDeprecationWarning(dw) {
const children = [];
let context;
switch (dw.runnableType) {
case 'spec':
context = '(in spec: ' + dw.runnableName + ')';
break;
case 'suite':
context = '(in suite: ' + dw.runnableName + ')';
break;
default:
context = '';
}
for (const line of dw.message.split('\n')) {
children.push(line);
children.push(j$.private.createDom('br'));
}
children[0] = 'DEPRECATION: ' + children[0];
children.push(context);
if (dw.stack) {
children.push(this.#createExpander(dw.stack));
}
this.add('jasmine-bar jasmine-warning', children);
}
// TODO private?
add(className, children) {
this.rootEl.appendChild(
j$.private.createDom('span', { className }, children)
);
}
#createExpander(stackTrace) {
const expandLink = j$.private.createDom(
'a',
{ href: '#' },
'Show stack trace'
);
const root = j$.private.createDom(
'div',
{ className: 'jasmine-expander' },
expandLink,
j$.private.createDom(
'div',
{ className: 'jasmine-expander-contents jasmine-stack-trace' },
stackTrace
)
);
expandLink.addEventListener('click', function(e) {
e.preventDefault();
if (root.classList.contains('jasmine-expanded')) {
root.classList.remove('jasmine-expanded');
expandLink.textContent = 'Show stack trace';
} else {
root.classList.add('jasmine-expanded');
expandLink.textContent = 'Hide stack trace';
}
});
return root;
}
}
class Banner {
#navigateWithNewParam;
constructor(navigateWithNewParam) {
this.#navigateWithNewParam = navigateWithNewParam;
this.rootEl = j$.private.createDom(
'div',
{ className: 'jasmine-banner' },
j$.private.createDom('a', {
className: 'jasmine-title',
href: 'http://jasmine.github.io/',
target: '_blank'
}),
j$.private.createDom(
'span',
{ className: 'jasmine-version' },
j$.version
)
);
}
showOptionsMenu(config) {
this.rootEl.appendChild(this.#optionsMenu(config));
}
#optionsMenu(config) {
const optionsMenuDom = j$.private.createDom(
'div',
{ className: 'jasmine-run-options' },
j$.private.createDom(
'span',
{ className: 'jasmine-trigger' },
'Options'
),
j$.private.createDom(
'div',
{ className: 'jasmine-payload' },
j$.private.createDom(
'div',
{ className: 'jasmine-stop-on-failure' },
j$.private.createDom('input', {
className: 'jasmine-fail-fast',
id: 'jasmine-fail-fast',
type: 'checkbox'
}),
j$.private.createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-fail-fast' },
'stop execution on spec failure'
)
),
j$.private.createDom(
'div',
{ className: 'jasmine-throw-failures' },
j$.private.createDom('input', {
className: 'jasmine-throw',
id: 'jasmine-throw-failures',
type: 'checkbox'
}),
j$.private.createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-throw-failures' },
'stop spec on expectation failure'
)
),
j$.private.createDom(
'div',
{ className: 'jasmine-random-order' },
j$.private.createDom('input', {
className: 'jasmine-random',
id: 'jasmine-random-order',
type: 'checkbox'
}),
j$.private.createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-random-order' },
'run tests in random order'
)
),
j$.private.createDom(
'div',
{ className: 'jasmine-hide-disabled' },
j$.private.createDom('input', {
className: 'jasmine-disabled',
id: 'jasmine-hide-disabled',
type: 'checkbox'
}),
j$.private.createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-hide-disabled' },
'hide disabled tests'
)
)
)
);
const failFastCheckbox = optionsMenuDom.querySelector(
'#jasmine-fail-fast'
);
failFastCheckbox.checked = config.stopOnSpecFailure;
failFastCheckbox.onclick = () => {
this.#navigateWithNewParam(
'stopOnSpecFailure',
!config.stopOnSpecFailure
);
};
const throwCheckbox = optionsMenuDom.querySelector(
'#jasmine-throw-failures'
);
throwCheckbox.checked = config.stopSpecOnExpectationFailure;
throwCheckbox.onclick = () => {
this.#navigateWithNewParam(
'stopSpecOnExpectationFailure',
!config.stopSpecOnExpectationFailure
);
};
const randomCheckbox = optionsMenuDom.querySelector(
'#jasmine-random-order'
);
randomCheckbox.checked = config.random;
randomCheckbox.onclick = () => {
this.#navigateWithNewParam('random', !config.random);
};
const hideDisabled = optionsMenuDom.querySelector(
'#jasmine-hide-disabled'
);
hideDisabled.checked = config.hideDisabled;
// TODO: backfill tests for this!
hideDisabled.onclick = () => {
this.#navigateWithNewParam('hideDisabled', !config.hideDisabled);
};
const optionsTrigger = optionsMenuDom.querySelector('.jasmine-trigger'),
optionsPayload = optionsMenuDom.querySelector('.jasmine-payload'),
isOpen = /\bjasmine-open\b/;
optionsTrigger.onclick = function() {
if (isOpen.test(optionsPayload.className)) {
optionsPayload.className = optionsPayload.className.replace(
isOpen,
''
);
} else {
optionsPayload.className += ' jasmine-open';
}
};
return optionsMenuDom;
}
}
class SymbolsView {
constructor() {
this.rootEl = j$.private.createDom('ul', {
className: 'jasmine-symbol-summary'
});
}
append(result, config) {
this.rootEl.appendChild(
j$.private.createDom('li', {
className: this.#className(result, config),
id: 'spec_' + result.id,
title: result.fullName
})
);
}
#className(result, config) {
if (noExpectations(result) && result.status === 'passed') {
return 'jasmine-empty';
} else if (result.status === 'excluded') {
if (config.hideDisabled) {
return 'jasmine-excluded-no-display';
} else {
return 'jasmine-excluded';
}
} else {
return 'jasmine-' + result.status;
}
}
}
class SummaryTreeView {
#urlBuilder;
#filterSpecs;
constructor(urlBuilder, filterSpecs) {
this.#urlBuilder = urlBuilder;
this.#filterSpecs = filterSpecs;
this.rootEl = j$.private.createDom('div', {
className: 'jasmine-summary'
});
}
addResults(resultsTree) {
this.#addResults(resultsTree, this.rootEl);
}
#addResults(resultsTree, domParent) {
let specListNode;
for (let i = 0; i < resultsTree.children.length; i++) {
const resultNode = resultsTree.children[i];
if (this.#filterSpecs && !hasActiveSpec(resultNode)) {
continue;
}
if (resultNode.type === 'suite') {
const suiteListNode = j$.private.createDom(
'ul',
{ className: 'jasmine-suite', id: 'suite-' + resultNode.result.id },
j$.private.createDom(
'li',
{
className:
'jasmine-suite-detail jasmine-' + resultNode.result.status
},
j$.private.createDom(
'a',
{ href: this.#urlBuilder.specHref(resultNode.result) },
resultNode.result.description
)
)
);
this.#addResults(resultNode, suiteListNode);
domParent.appendChild(suiteListNode);
}
if (resultNode.type === 'spec') {
if (domParent.getAttribute('class') !== 'jasmine-specs') {
specListNode = j$.private.createDom('ul', {
className: 'jasmine-specs'
});
domParent.appendChild(specListNode);
}
let specDescription = resultNode.result.description;
if (noExpectations(resultNode.result)) {
specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription;
}
if (resultNode.result.status === 'pending') {
if (resultNode.result.pendingReason !== '') {
specDescription +=
' PENDING WITH MESSAGE: ' + resultNode.result.pendingReason;
} else {
specDescription += ' PENDING';
}
}
specListNode.appendChild(
j$.private.createDom(
'li',
{
className: 'jasmine-' + resultNode.result.status,
id: 'spec-' + resultNode.result.id
},
j$.private.createDom(
'a',
{ href: this.#urlBuilder.specHref(resultNode.result) },
specDescription
),
j$.private.createDom(
'span',
{ className: 'jasmine-spec-duration' },
'(' + resultNode.result.duration + 'ms)'
)
)
);
}
}
}
}
class FailuresView {
#urlBuilder;
#failureEls;
constructor(urlBuilder) {
this.#urlBuilder = urlBuilder;
this.#failureEls = [];
this.rootEl = j$.private.createDom(
'div',
{ className: 'jasmine-results' },
j$.private.createDom('div', { className: 'jasmine-failures' })
);
}
append(result, parent) {
// TODO: Figure out why the reuslt is wrong if we build the DOM node later
this.#failureEls.push(this.#makeFailureEl(result, parent));
}
// TODO move this to state builder or something
any() {
return this.#failureEls.length > 0;
}
show() {
const failureNode = this.rootEl.querySelector('.jasmine-failures');
for (const el of this.#failureEls) {
failureNode.appendChild(el);
}
}
#makeFailureEl(result, parent) {
const failure = j$.private.createDom(
'div',
{ className: 'jasmine-spec-detail jasmine-failed' },
this.#failureDescription(result, parent),
j$.private.createDom('div', { className: 'jasmine-messages' })
);
const messages = failure.childNodes[1];
for (let i = 0; i < result.failedExpectations.length; i++) {
const expectation = result.failedExpectations[i];
messages.appendChild(
j$.private.createDom(
'div',
{ className: 'jasmine-result-message' },
expectation.message
)
);
messages.appendChild(
j$.private.createDom(
'div',
{ className: 'jasmine-stack-trace' },
expectation.stack
)
);
}
if (result.failedExpectations.length === 0) {
messages.appendChild(
j$.private.createDom(
'div',
{ className: 'jasmine-result-message' },
'Spec has no expectations'
)
);
}
if (result.debugLogs) {
messages.appendChild(this.#debugLogTable(result.debugLogs));
}
return failure;
}
#failureDescription(result, suite) {
const wrapper = j$.private.createDom(
'div',
{ className: 'jasmine-description' },
j$.private.createDom(
'a',
{
title: result.description,
href: this.#urlBuilder.specHref(result)
},
result.description
)
);
let suiteLink;
while (suite && suite.parent) {
wrapper.insertBefore(
document.createTextNode(' > '),
wrapper.firstChild
);
suiteLink = j$.private.createDom(
'a',
{ href: this.#urlBuilder.suiteHref(suite) },
suite.result.description
);
wrapper.insertBefore(suiteLink, wrapper.firstChild);
suite = suite.parent;
}
return wrapper;
}
#debugLogTable(debugLogs) {
const tbody = j$.private.createDom('tbody');
for (const entry of debugLogs) {
tbody.appendChild(
j$.private.createDom(
'tr',
{},
j$.private.createDom('td', {}, entry.timestamp.toString()),
j$.private.createDom(
'td',
{ className: 'jasmine-debug-log-msg' },
entry.message
)
)
);
}
return j$.private.createDom(
'div',
{ className: 'jasmine-debug-log' },
j$.private.createDom(
'div',
{ className: 'jasmine-debug-log-header' },
'Debug logs'
),
j$.private.createDom(
'table',
{},
j$.private.createDom(
'thead',
{},
j$.private.createDom(
'tr',
{},
j$.private.createDom('th', {}, 'Time (ms)'),
j$.private.createDom('th', {}, 'Message')
)
),
tbody
)
);
}
}
class UrlBuilder {
#addToExistingQueryString;
constructor(addToExistingQueryString) {
this.#addToExistingQueryString = function(k, v) {
// include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
return (
(window.location.pathname || '') + addToExistingQueryString(k, v)
);
};
}
suiteHref(suite) {
const els = [];
while (suite && suite.parent) {
els.unshift(suite.result.description);
suite = suite.parent;
}
return this.#addToExistingQueryString('spec', els.join(' '));
}
specHref(result) {
return this.#addToExistingQueryString('spec', result.fullName);
}
runAllHref() {
return this.#addToExistingQueryString('spec', '');
}
seedHref(seed) {
return this.#addToExistingQueryString('seed', seed);
}
}
return HtmlReporter;
};

View File

@@ -0,0 +1,77 @@
jasmineRequire.ResultsStateBuilder = function(j$) {
'use strict';
class ResultsStateBuilder {
constructor() {
this.topResults = new j$.private.ResultsNode({}, '', null);
this.currentParent = this.topResults;
this.totalSpecsDefined = 0;
this.specsExecuted = 0;
this.failureCount = 0;
this.pendingSpecCount = 0;
this.deprecationWarnings = [];
}
suiteStarted(result) {
this.currentParent.addChild(result, 'suite');
this.currentParent = this.currentParent.last();
}
suiteDone(result) {
this.currentParent.updateResult(result);
this.#addDeprecationWarnings(result, 'suite');
if (this.currentParent !== this.topResults) {
this.currentParent = this.currentParent.parent;
}
if (result.status === 'failed') {
this.failureCount++;
}
}
specDone(result) {
this.currentParent.addChild(result, 'spec');
this.#addDeprecationWarnings(result, 'spec');
if (result.status !== 'excluded') {
this.specsExecuted++;
}
if (result.status === 'failed') {
this.failureCount++;
}
if (result.status == 'pending') {
this.pendingSpecCount++;
}
}
jasmineStarted(result) {
this.totalSpecsDefined = result.totalSpecsDefined;
}
jasmineDone(result) {
if (result.failedExpectations) {
this.failureCount += result.failedExpectations.length;
}
this.#addDeprecationWarnings(result);
}
#addDeprecationWarnings(result, runnableType) {
if (result.deprecationWarnings) {
for (const dw of result.deprecationWarnings) {
this.deprecationWarnings.push({
message: dw.message,
stack: dw.stack,
runnableName: result.fullName,
runnableType: runnableType
});
}
}
}
}
return ResultsStateBuilder;
};

108
src/html/SummaryTreeView.js Normal file
View File

@@ -0,0 +1,108 @@
jasmineRequire.SummaryTreeView = function(j$) {
'use strict';
const { createDom, noExpectations } = j$.private.htmlReporterUtils;
class SummaryTreeView {
#urlBuilder;
#filterSpecs;
constructor(urlBuilder, filterSpecs) {
this.#urlBuilder = urlBuilder;
this.#filterSpecs = filterSpecs;
this.rootEl = createDom('div', {
className: 'jasmine-summary'
});
}
addResults(resultsTree) {
this.#addResults(resultsTree, this.rootEl);
}
#addResults(resultsTree, domParent) {
let specListNode;
for (let i = 0; i < resultsTree.children.length; i++) {
const resultNode = resultsTree.children[i];
if (this.#filterSpecs && !hasActiveSpec(resultNode)) {
continue;
}
if (resultNode.type === 'suite') {
const suiteListNode = createDom(
'ul',
{ className: 'jasmine-suite', id: 'suite-' + resultNode.result.id },
createDom(
'li',
{
className:
'jasmine-suite-detail jasmine-' + resultNode.result.status
},
createDom(
'a',
{ href: this.#urlBuilder.specHref(resultNode.result) },
resultNode.result.description
)
)
);
this.#addResults(resultNode, suiteListNode);
domParent.appendChild(suiteListNode);
}
if (resultNode.type === 'spec') {
if (domParent.getAttribute('class') !== 'jasmine-specs') {
specListNode = createDom('ul', {
className: 'jasmine-specs'
});
domParent.appendChild(specListNode);
}
let specDescription = resultNode.result.description;
if (noExpectations(resultNode.result)) {
specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription;
}
if (resultNode.result.status === 'pending') {
if (resultNode.result.pendingReason !== '') {
specDescription +=
' PENDING WITH MESSAGE: ' + resultNode.result.pendingReason;
} else {
specDescription += ' PENDING';
}
}
specListNode.appendChild(
createDom(
'li',
{
className: 'jasmine-' + resultNode.result.status,
id: 'spec-' + resultNode.result.id
},
createDom(
'a',
{ href: this.#urlBuilder.specHref(resultNode.result) },
specDescription
),
createDom(
'span',
{ className: 'jasmine-spec-duration' },
'(' + resultNode.result.duration + 'ms)'
)
)
);
}
}
}
}
function hasActiveSpec(resultNode) {
if (resultNode.type === 'spec' && resultNode.result.status !== 'excluded') {
return true;
}
if (resultNode.type === 'suite') {
for (let i = 0, j = resultNode.children.length; i < j; i++) {
if (hasActiveSpec(resultNode.children[i])) {
return true;
}
}
}
}
return SummaryTreeView;
};

39
src/html/SymbolsView.js Normal file
View File

@@ -0,0 +1,39 @@
jasmineRequire.SymbolsView = function(j$) {
'use strict';
const { createDom, noExpectations } = j$.private.htmlReporterUtils;
class SymbolsView {
constructor() {
this.rootEl = createDom('ul', {
className: 'jasmine-symbol-summary'
});
}
append(result, config) {
this.rootEl.appendChild(
createDom('li', {
className: this.#className(result, config),
id: 'spec_' + result.id,
title: result.fullName
})
);
}
#className(result, config) {
if (noExpectations(result) && result.status === 'passed') {
return 'jasmine-empty';
} else if (result.status === 'excluded') {
if (config.hideDisabled) {
return 'jasmine-excluded-no-display';
} else {
return 'jasmine-excluded';
}
} else {
return 'jasmine-' + result.status;
}
}
}
return SymbolsView;
};

41
src/html/UrlBuilder.js Normal file
View File

@@ -0,0 +1,41 @@
jasmineRequire.UrlBuilder = function() {
'use strict';
class UrlBuilder {
#addToExistingQueryString;
constructor(addToExistingQueryString) {
this.#addToExistingQueryString = function(k, v) {
// include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
return (
(window.location.pathname || '') + addToExistingQueryString(k, v)
);
};
}
suiteHref(suite) {
const els = [];
while (suite && suite.parent) {
els.unshift(suite.result.description);
suite = suite.parent;
}
return this.#addToExistingQueryString('spec', els.join(' '));
}
specHref(result) {
return this.#addToExistingQueryString('spec', result.fullName);
}
runAllHref() {
return this.#addToExistingQueryString('spec', '');
}
seedHref(seed) {
return this.#addToExistingQueryString('seed', seed);
}
}
return UrlBuilder;
};

View File

@@ -1,4 +1,4 @@
jasmineRequire.createDom = function(j$) {
jasmineRequire.htmlReporterUtils = function(j$) {
'use strict';
function createDom(type, attrs, childrenArrayOrVarArgs) {
@@ -38,5 +38,15 @@ jasmineRequire.createDom = function(j$) {
return el;
}
return createDom;
function noExpectations(result) {
const allExpectations =
result.failedExpectations.length + result.passedExpectations.length;
return (
allExpectations === 0 &&
(result.status === 'passed' || result.status === 'failed')
);
}
return { createDom, noExpectations };
};

View File

@@ -3,7 +3,14 @@ var jasmineRequire = window.jasmineRequire || require('./jasmine.js');
jasmineRequire.html = function(j$) {
j$.private.ResultsNode = jasmineRequire.ResultsNode();
j$.private.createDom = jasmineRequire.createDom(j$);
j$.private.ResultsStateBuilder = jasmineRequire.ResultsStateBuilder(j$);
j$.private.htmlReporterUtils = jasmineRequire.htmlReporterUtils(j$);
j$.private.AlertsView = jasmineRequire.AlertsView(j$);
j$.private.Banner = jasmineRequire.Banner(j$);
j$.private.SymbolsView = jasmineRequire.SymbolsView(j$);
j$.private.SummaryTreeView = jasmineRequire.SummaryTreeView(j$);
j$.private.FailuresView = jasmineRequire.FailuresView(j$);
j$.private.UrlBuilder = jasmineRequire.UrlBuilder();
j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
j$.QueryString = jasmineRequire.QueryString();
j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter();