diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js
index cbbf328c..9180d69f 100644
--- a/lib/jasmine-core/jasmine-html.js
+++ b/lib/jasmine-core/jasmine-html.js
@@ -27,6 +27,7 @@ var jasmineRequire = window.jasmineRequire || require('./jasmine.js');
jasmineRequire.html = function(j$) {
j$.private.ResultsNode = jasmineRequire.ResultsNode();
+ j$.private.DomContext = jasmineRequire.DomContext(j$);
j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
j$.QueryString = jasmineRequire.QueryString();
j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter();
@@ -35,53 +36,80 @@ jasmineRequire.html = function(j$) {
jasmineRequire.HtmlReporter = function(j$) {
'use strict';
- function ResultsStateBuilder() {
- this.topResults = new j$.private.ResultsNode({}, '', null);
- this.currentParent = this.topResults;
- this.specsExecuted = 0;
- this.failureCount = 0;
- this.pendingSpecCount = 0;
+ 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
+ });
+ }
+ }
+ }
}
- ResultsStateBuilder.prototype.suiteStarted = function(result) {
- this.currentParent.addChild(result, 'suite');
- this.currentParent = this.currentParent.last();
- };
-
- ResultsStateBuilder.prototype.suiteDone = function(result) {
- this.currentParent.updateResult(result);
- if (this.currentParent !== this.topResults) {
- this.currentParent = this.currentParent.parent;
- }
-
- if (result.status === 'failed') {
- this.failureCount++;
- }
- };
-
- ResultsStateBuilder.prototype.specStarted = function(result) {};
-
- ResultsStateBuilder.prototype.specDone = function(result) {
- this.currentParent.addChild(result, 'spec');
-
- if (result.status !== 'excluded') {
- this.specsExecuted++;
- }
-
- if (result.status === 'failed') {
- this.failureCount++;
- }
-
- if (result.status == 'pending') {
- this.pendingSpecCount++;
- }
- };
-
- ResultsStateBuilder.prototype.jasmineDone = function(result) {
- if (result.failedExpectations) {
- this.failureCount += result.failedExpectations.length;
- }
- };
+ const errorBarClassName = 'jasmine-bar jasmine-errored';
+ const afterAllMessagePrefix = 'AfterAll ';
/**
* @class HtmlReporter
@@ -96,16 +124,20 @@ jasmineRequire.HtmlReporter = function(j$) {
}
const getContainer = options.getContainer;
- const createElement = options.createElement;
- const createTextNode = options.createTextNode;
+ const domContext = new j$.private.DomContext({
+ createElement: options.createElement,
+ createTextNode: options.createTextNode
+ });
const navigateWithNewParam = options.navigateWithNewParam || function() {};
const addToExistingQueryString =
options.addToExistingQueryString || defaultQueryString;
+ const urlBuilder = new UrlBuilder(addToExistingQueryString);
const filterSpecs = options.filterSpecs;
let htmlReporterMain;
+ let alerts;
let symbols;
- const deprecationWarnings = [];
- const failures = [];
+ let banner;
+ let failures;
/**
* Initializes the reporter. Should be called before {@link Env#execute}.
@@ -114,37 +146,25 @@ jasmineRequire.HtmlReporter = function(j$) {
*/
this.initialize = function() {
clearPrior();
- htmlReporterMain = createDom(
+ alerts = new AlertsView(domContext, urlBuilder);
+ symbols = new SymbolsView(domContext, config);
+ banner = new Banner(domContext, navigateWithNewParam);
+ failures = new FailuresView(domContext, urlBuilder);
+ htmlReporterMain = domContext.create(
'div',
{ className: 'jasmine_html-reporter' },
- createDom(
- 'div',
- { className: 'jasmine-banner' },
- createDom('a', {
- className: 'jasmine-title',
- href: 'http://jasmine.github.io/',
- target: '_blank'
- }),
- createDom('span', { className: 'jasmine-version' }, j$.version)
- ),
- createDom('ul', { className: 'jasmine-symbol-summary' }),
- createDom('div', { className: 'jasmine-alert' }),
- createDom(
- 'div',
- { className: 'jasmine-results' },
- createDom('div', { className: 'jasmine-failures' })
- )
+ banner.rootEl,
+ symbols.rootEl,
+ alerts.rootEl,
+ failures.rootEl
);
getContainer().appendChild(htmlReporterMain);
};
- let totalSpecsDefined;
this.jasmineStarted = function(options) {
- totalSpecsDefined = options.totalSpecsDefined || 0;
+ stateBuilder.jasmineStarted(options);
};
- const summary = createDom('div', { className: 'jasmine-summary' });
-
const stateBuilder = new ResultsStateBuilder();
this.suiteStarted = function(result) {
@@ -155,17 +175,15 @@ jasmineRequire.HtmlReporter = function(j$) {
stateBuilder.suiteDone(result);
if (result.status === 'failed') {
- failures.push(failureDom(result));
+ failures.append(result, stateBuilder.currentParent);
}
- addDeprecationWarnings(result, 'suite');
};
- this.specStarted = function(result) {
- stateBuilder.specStarted(result);
- };
+ this.specStarted = function(result) {};
this.specDone = function(result) {
stateBuilder.specDone(result);
+ symbols.append(result, config());
if (noExpectations(result)) {
const noSpecMsg = "Spec '" + result.fullName + "' has no expectations.";
@@ -178,86 +196,179 @@ jasmineRequire.HtmlReporter = function(j$) {
}
}
- if (!symbols) {
- symbols = find('.jasmine-symbol-summary');
- }
-
- symbols.appendChild(
- createDom('li', {
- className: this.displaySpecInCorrectFormat(result),
- id: 'spec_' + result.id,
- title: result.fullName
- })
- );
-
if (result.status === 'failed') {
- failures.push(failureDom(result));
+ failures.append(result, stateBuilder.currentParent);
}
-
- addDeprecationWarnings(result, 'spec');
- };
-
- this.displaySpecInCorrectFormat = function(result) {
- return noExpectations(result) && result.status === 'passed'
- ? 'jasmine-empty'
- : this.resultStatus(result.status);
- };
-
- this.resultStatus = function(status) {
- if (status === 'excluded') {
- return config().hideDisabled
- ? 'jasmine-excluded-no-display'
- : 'jasmine-excluded';
- }
- return 'jasmine-' + status;
};
this.jasmineDone = function(doneResult) {
stateBuilder.jasmineDone(doneResult);
- const banner = find('.jasmine-banner');
- const alert = find('.jasmine-alert');
- const order = doneResult && doneResult.order;
+ alerts.addDuration(doneResult.totalTime);
+ banner.showOptionsMenu(config());
- alert.appendChild(
- createDom(
- 'span',
- { className: 'jasmine-duration' },
- 'finished in ' + doneResult.totalTime / 1000 + 's'
- )
- );
-
- banner.appendChild(optionsMenu(config()));
-
- if (stateBuilder.specsExecuted < totalSpecsDefined) {
- const skippedMessage =
- 'Ran ' +
- stateBuilder.specsExecuted +
- ' of ' +
- totalSpecsDefined +
- ' specs - run all';
- // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
- const skippedLink =
- (window.location.pathname || '') +
- addToExistingQueryString('spec', '');
- alert.appendChild(
- createDom(
- 'span',
- { className: 'jasmine-bar jasmine-skipped' },
- createDom(
- 'a',
- { href: skippedLink, title: 'Run all specs' },
- skippedMessage
- )
- )
+ if (stateBuilder.specsExecuted < stateBuilder.totalSpecsDefined) {
+ alerts.addSkipped(
+ stateBuilder.specsExecuted,
+ stateBuilder.totalSpecsDefined
);
}
+
+ alerts.addSeedBar(doneResult, stateBuilder, doneResult.order);
+
+ if (doneResult.failedExpectations) {
+ for (const f of doneResult.failedExpectations) {
+ alerts.addGlobalFailure(f);
+ }
+ }
+
+ for (const dw of stateBuilder.deprecationWarnings) {
+ alerts.addDeprecationWarning(dw);
+ }
+
+ const results = find('.jasmine-results');
+ const summary = new SummaryTreeView(domContext, urlBuilder, filterSpecs);
+ summary.addResults(stateBuilder.topResults);
+ results.appendChild(summary.rootEl);
+
+ if (failures.any()) {
+ alerts.addFailureToggle(
+ function() {
+ setMenuModeTo('jasmine-failure-list');
+ },
+ function() {
+ setMenuModeTo('jasmine-spec-list');
+ }
+ );
+
+ setMenuModeTo('jasmine-failure-list');
+ failures.show();
+ }
+ };
+
+ return this;
+
+ function find(selector) {
+ return getContainer().querySelector('.jasmine_html-reporter ' + selector);
+ }
+
+ function clearPrior() {
+ const oldReporter = find('');
+
+ if (oldReporter) {
+ getContainer().removeChild(oldReporter);
+ }
+ }
+
+ function setMenuModeTo(mode) {
+ htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode);
+ }
+ }
+
+ 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 {
+ #domContext;
+ #urlBuilder;
+
+ constructor(domContext, urlBuilder) {
+ this.#domContext = domContext;
+ this.#urlBuilder = urlBuilder;
+ this.rootEl = domContext.create('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',
+ this.#domContext.create(
+ 'a',
+ { href: this.#urlBuilder.runAllHref(), title: 'Run all specs' },
+ `Ran ${numExecuted} of ${numDefined} specs - run all`
+ )
+ );
+ }
+
+ addFailureToggle(onClickFailures, onClickSpecList) {
+ const failuresLink = this.#domContext.create(
+ 'a',
+ { className: 'jasmine-failures-menu', href: '#' },
+ 'Failures'
+ );
+ let specListLink = this.#domContext.create(
+ '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', [
+ this.#domContext.create('span', {}, 'Spec List | '),
+ failuresLink
+ ]);
+ this.add('jasmine-menu jasmine-bar jasmine-failure-list', [
+ specListLink,
+ this.#domContext.create('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 (totalSpecsDefined > 0 || failed) {
+ if (stateBuilder.totalSpecsDefined > 0 || failed) {
statusBarMessage +=
pluralize('spec', stateBuilder.specsExecuted) +
', ' +
@@ -283,489 +394,89 @@ jasmineRequire.HtmlReporter = function(j$) {
let seedBar;
if (order && order.random) {
- seedBar = createDom(
+ seedBar = this.#domContext.create(
'span',
{ className: 'jasmine-seed-bar' },
', randomized with seed ',
- createDom(
+ this.#domContext.create(
'a',
{
title: 'randomized with seed ' + order.seed,
- href: seedHref(order.seed)
+ href: this.#urlBuilder.seedHref(order.seed)
},
order.seed
)
);
}
- alert.appendChild(
- createDom(
- 'span',
- { className: statusBarClassName },
- statusBarMessage,
- seedBar
- )
- );
+ this.add(statusBarClassName, [statusBarMessage, seedBar]);
+ }
- const errorBarClassName = 'jasmine-bar jasmine-errored';
- const afterAllMessagePrefix = 'AfterAll ';
+ // TODO check test coverage
+ #globalFailureMessage(failure) {
+ if (failure.globalErrorType === 'load') {
+ const prefix = 'Error during loading: ' + failure.message;
- for (let i = 0; i < globalFailures.length; i++) {
- alert.appendChild(
- createDom(
- 'span',
- { className: errorBarClassName },
- globalFailureMessage(globalFailures[i])
- )
- );
- }
-
- function 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;
+ if (failure.filename) {
+ return prefix + ' in ' + failure.filename + ' line ' + failure.lineno;
} else {
- return failure.message;
- }
- }
-
- addDeprecationWarnings(doneResult);
-
- for (let i = 0; i < deprecationWarnings.length; i++) {
- const children = [];
- let context;
-
- switch (deprecationWarnings[i].runnableType) {
- case 'spec':
- context = '(in spec: ' + deprecationWarnings[i].runnableName + ')';
- break;
- case 'suite':
- context = '(in suite: ' + deprecationWarnings[i].runnableName + ')';
- break;
- default:
- context = '';
- }
-
- deprecationWarnings[i].message.split('\n').forEach(function(line) {
- children.push(line);
- children.push(createDom('br'));
- });
-
- children[0] = 'DEPRECATION: ' + children[0];
- children.push(context);
-
- if (deprecationWarnings[i].stack) {
- children.push(createExpander(deprecationWarnings[i].stack));
- }
-
- alert.appendChild(
- createDom(
- 'span',
- { className: 'jasmine-bar jasmine-warning' },
- children
- )
- );
- }
-
- const results = find('.jasmine-results');
- results.appendChild(summary);
-
- summaryList(stateBuilder.topResults, summary);
-
- if (failures.length) {
- alert.appendChild(
- createDom(
- 'span',
- { className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
- createDom('span', {}, 'Spec List | '),
- createDom(
- 'a',
- { className: 'jasmine-failures-menu', href: '#' },
- 'Failures'
- )
- )
- );
- alert.appendChild(
- createDom(
- 'span',
- { className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
- createDom(
- 'a',
- { className: 'jasmine-spec-list-menu', href: '#' },
- 'Spec List'
- ),
- createDom('span', {}, ' | Failures ')
- )
- );
-
- find('.jasmine-failures-menu').onclick = function() {
- setMenuModeTo('jasmine-failure-list');
- return false;
- };
- find('.jasmine-spec-list-menu').onclick = function() {
- setMenuModeTo('jasmine-spec-list');
- return false;
- };
-
- setMenuModeTo('jasmine-failure-list');
-
- const failureNode = find('.jasmine-failures');
- for (let i = 0; i < failures.length; i++) {
- failureNode.appendChild(failures[i]);
- }
- }
- };
-
- return this;
-
- function failureDom(result) {
- const failure = createDom(
- 'div',
- { className: 'jasmine-spec-detail jasmine-failed' },
- failureDescription(result, stateBuilder.currentParent),
- 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(debugLogTable(result.debugLogs));
- }
-
- return failure;
- }
-
- function debugLogTable(debugLogs) {
- const tbody = createDom('tbody');
-
- debugLogs.forEach(function(entry) {
- 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
- )
- );
- }
-
- function summaryList(resultsTree, domParent) {
- let specListNode;
- for (let i = 0; i < resultsTree.children.length; i++) {
- const resultNode = resultsTree.children[i];
- if (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: specHref(resultNode.result) },
- resultNode.result.description
- )
- )
- );
-
- summaryList(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: specHref(resultNode.result) },
- specDescription
- ),
- createDom(
- 'span',
- { className: 'jasmine-spec-duration' },
- '(' + resultNode.result.duration + 'ms)'
- )
- )
- );
+ return prefix;
}
+ } else if (failure.globalErrorType === 'afterAll') {
+ return afterAllMessagePrefix + failure.message;
+ } else {
+ return failure.message;
}
}
- function 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'
- )
- )
- )
- );
+ addDeprecationWarning(dw) {
+ const children = [];
+ let context;
- const failFastCheckbox = optionsMenuDom.querySelector(
- '#jasmine-fail-fast'
- );
- failFastCheckbox.checked = config.stopOnSpecFailure;
- failFastCheckbox.onclick = function() {
- navigateWithNewParam('stopOnSpecFailure', !config.stopOnSpecFailure);
- };
-
- const throwCheckbox = optionsMenuDom.querySelector(
- '#jasmine-throw-failures'
- );
- throwCheckbox.checked = config.stopSpecOnExpectationFailure;
- throwCheckbox.onclick = function() {
- navigateWithNewParam(
- 'stopSpecOnExpectationFailure',
- !config.stopSpecOnExpectationFailure
- );
- };
-
- const randomCheckbox = optionsMenuDom.querySelector(
- '#jasmine-random-order'
- );
- randomCheckbox.checked = config.random;
- randomCheckbox.onclick = function() {
- navigateWithNewParam('random', !config.random);
- };
-
- const hideDisabled = optionsMenuDom.querySelector(
- '#jasmine-hide-disabled'
- );
- hideDisabled.checked = config.hideDisabled;
- hideDisabled.onclick = function() {
- 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;
- }
-
- function failureDescription(result, suite) {
- const wrapper = createDom(
- 'div',
- { className: 'jasmine-description' },
- createDom(
- 'a',
- { title: result.description, href: specHref(result) },
- result.description
- )
- );
- let suiteLink;
-
- while (suite && suite.parent) {
- wrapper.insertBefore(createTextNode(' > '), wrapper.firstChild);
- suiteLink = createDom(
- 'a',
- { href: suiteHref(suite) },
- suite.result.description
- );
- wrapper.insertBefore(suiteLink, wrapper.firstChild);
-
- suite = suite.parent;
+ switch (dw.runnableType) {
+ case 'spec':
+ context = '(in spec: ' + dw.runnableName + ')';
+ break;
+ case 'suite':
+ context = '(in suite: ' + dw.runnableName + ')';
+ break;
+ default:
+ context = '';
}
- return wrapper;
- }
-
- function suiteHref(suite) {
- const els = [];
-
- while (suite && suite.parent) {
- els.unshift(suite.result.description);
- suite = suite.parent;
+ for (const line of dw.message.split('\n')) {
+ children.push(line);
+ children.push(this.#domContext.create('br'));
}
- // 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('spec', els.join(' '))
+ 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(
+ this.#domContext.create('span', { className }, children)
);
}
- function addDeprecationWarnings(result, runnableType) {
- if (result && result.deprecationWarnings) {
- for (let i = 0; i < result.deprecationWarnings.length; i++) {
- const warning = result.deprecationWarnings[i].message;
- deprecationWarnings.push({
- message: warning,
- stack: result.deprecationWarnings[i].stack,
- runnableName: result.fullName,
- runnableType: runnableType
- });
- }
- }
- }
-
- function createExpander(stackTrace) {
- const expandLink = createDom('a', { href: '#' }, 'Show stack trace');
- const root = createDom(
+ #createExpander(stackTrace) {
+ const expandLink = this.#domContext.create(
+ 'a',
+ { href: '#' },
+ 'Show stack trace'
+ );
+ const root = this.#domContext.create(
'div',
{ className: 'jasmine-expander' },
expandLink,
- createDom(
+ this.#domContext.create(
'div',
{ className: 'jasmine-expander-contents jasmine-stack-trace' },
stackTrace
@@ -786,111 +497,474 @@ jasmineRequire.HtmlReporter = function(j$) {
return root;
}
+ }
- function find(selector) {
- return getContainer().querySelector('.jasmine_html-reporter ' + selector);
+ class Banner {
+ #domContext;
+ #navigateWithNewParam;
+
+ constructor(domContext, navigateWithNewParam) {
+ this.#domContext = domContext;
+ this.#navigateWithNewParam = navigateWithNewParam;
+ this.rootEl = domContext.create(
+ 'div',
+ { className: 'jasmine-banner' },
+ domContext.create('a', {
+ className: 'jasmine-title',
+ href: 'http://jasmine.github.io/',
+ target: '_blank'
+ }),
+ domContext.create('span', { className: 'jasmine-version' }, j$.version)
+ );
}
- function clearPrior() {
- const oldReporter = find('');
-
- if (oldReporter) {
- getContainer().removeChild(oldReporter);
- }
+ showOptionsMenu(config) {
+ this.rootEl.appendChild(this.#optionsMenu(config));
}
- function createDom(type, attrs, childrenArrayOrVarArgs) {
- const el = createElement(type);
- let children;
+ #optionsMenu(config) {
+ const optionsMenuDom = this.#domContext.create(
+ 'div',
+ { className: 'jasmine-run-options' },
+ this.#domContext.create(
+ 'span',
+ { className: 'jasmine-trigger' },
+ 'Options'
+ ),
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-payload' },
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-stop-on-failure' },
+ this.#domContext.create('input', {
+ className: 'jasmine-fail-fast',
+ id: 'jasmine-fail-fast',
+ type: 'checkbox'
+ }),
+ this.#domContext.create(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-fail-fast' },
+ 'stop execution on spec failure'
+ )
+ ),
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-throw-failures' },
+ this.#domContext.create('input', {
+ className: 'jasmine-throw',
+ id: 'jasmine-throw-failures',
+ type: 'checkbox'
+ }),
+ this.#domContext.create(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-throw-failures' },
+ 'stop spec on expectation failure'
+ )
+ ),
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-random-order' },
+ this.#domContext.create('input', {
+ className: 'jasmine-random',
+ id: 'jasmine-random-order',
+ type: 'checkbox'
+ }),
+ this.#domContext.create(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-random-order' },
+ 'run tests in random order'
+ )
+ ),
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-hide-disabled' },
+ this.#domContext.create('input', {
+ className: 'jasmine-disabled',
+ id: 'jasmine-hide-disabled',
+ type: 'checkbox'
+ }),
+ this.#domContext.create(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-hide-disabled' },
+ 'hide disabled tests'
+ )
+ )
+ )
+ );
- if (j$.private.isArray(childrenArrayOrVarArgs)) {
- children = childrenArrayOrVarArgs;
+ 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 {
+ #domContext;
+
+ constructor(domContext) {
+ this.#domContext = domContext;
+ this.rootEl = domContext.create('ul', {
+ className: 'jasmine-symbol-summary'
+ });
+ }
+
+ append(result, config) {
+ this.rootEl.appendChild(
+ this.#domContext.create('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 {
- children = [];
-
- for (let i = 2; i < arguments.length; i++) {
- children.push(arguments[i]);
- }
+ return 'jasmine-' + result.status;
}
+ }
+ }
- for (let i = 0; i < children.length; i++) {
- const child = children[i];
+ class SummaryTreeView {
+ #domContext;
+ #urlBuilder;
+ #filterSpecs;
- if (typeof child === 'string') {
- el.appendChild(createTextNode(child));
- } else {
- if (child) {
- el.appendChild(child);
+ constructor(domContext, urlBuilder, filterSpecs) {
+ this.#domContext = domContext;
+ this.#urlBuilder = urlBuilder;
+ this.#filterSpecs = filterSpecs;
+ this.rootEl = domContext.create('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 = this.#domContext.create(
+ 'ul',
+ { className: 'jasmine-suite', id: 'suite-' + resultNode.result.id },
+ this.#domContext.create(
+ 'li',
+ {
+ className:
+ 'jasmine-suite-detail jasmine-' + resultNode.result.status
+ },
+ this.#domContext.create(
+ '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 = this.#domContext.create('ul', {
+ className: 'jasmine-specs'
+ });
+ domParent.appendChild(specListNode);
}
- }
- }
-
- for (const attr in attrs) {
- if (attr == 'className') {
- el[attr] = attrs[attr];
- } else {
- el.setAttribute(attr, attrs[attr]);
- }
- }
-
- return el;
- }
-
- function pluralize(singular, count) {
- const word = count == 1 ? singular : singular + 's';
-
- return '' + count + ' ' + word;
- }
-
- function specHref(result) {
- // 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('spec', result.fullName)
- );
- }
-
- function seedHref(seed) {
- // 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('seed', seed)
- );
- }
-
- function defaultQueryString(key, value) {
- return '?' + key + '=' + value;
- }
-
- function setMenuModeTo(mode) {
- htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode);
- }
-
- function noExpectations(result) {
- const allExpectations =
- result.failedExpectations.length + result.passedExpectations.length;
-
- return (
- allExpectations === 0 &&
- (result.status === 'passed' || result.status === 'failed')
- );
- }
-
- 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;
+ 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(
+ this.#domContext.create(
+ 'li',
+ {
+ className: 'jasmine-' + resultNode.result.status,
+ id: 'spec-' + resultNode.result.id
+ },
+ this.#domContext.create(
+ 'a',
+ { href: this.#urlBuilder.specHref(resultNode.result) },
+ specDescription
+ ),
+ this.#domContext.create(
+ 'span',
+ { className: 'jasmine-spec-duration' },
+ '(' + resultNode.result.duration + 'ms)'
+ )
+ )
+ );
}
}
}
}
+ class FailuresView {
+ #domContext;
+ #urlBuilder;
+ #failureEls;
+
+ constructor(domContext, urlBuilder) {
+ this.#domContext = domContext;
+ this.#urlBuilder = urlBuilder;
+ this.#failureEls = [];
+ this.rootEl = domContext.create(
+ 'div',
+ { className: 'jasmine-results' },
+ domContext.create('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 = this.#domContext.create(
+ 'div',
+ { className: 'jasmine-spec-detail jasmine-failed' },
+ this.#failureDescription(result, parent),
+ this.#domContext.create('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(
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-result-message' },
+ expectation.message
+ )
+ );
+ messages.appendChild(
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-stack-trace' },
+ expectation.stack
+ )
+ );
+ }
+
+ if (result.failedExpectations.length === 0) {
+ messages.appendChild(
+ this.#domContext.create(
+ '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 = this.#domContext.create(
+ 'div',
+ { className: 'jasmine-description' },
+ this.#domContext.create(
+ 'a',
+ {
+ title: result.description,
+ href: this.#urlBuilder.specHref(result)
+ },
+ result.description
+ )
+ );
+ let suiteLink;
+
+ while (suite && suite.parent) {
+ wrapper.insertBefore(
+ this.#domContext.createTextNode(' > '),
+ wrapper.firstChild
+ );
+ suiteLink = this.#domContext.create(
+ 'a',
+ { href: this.#urlBuilder.suiteHref(suite) },
+ suite.result.description
+ );
+ wrapper.insertBefore(suiteLink, wrapper.firstChild);
+
+ suite = suite.parent;
+ }
+
+ return wrapper;
+ }
+
+ #debugLogTable(debugLogs) {
+ const tbody = this.#domContext.create('tbody');
+
+ for (const entry of debugLogs) {
+ tbody.appendChild(
+ this.#domContext.create(
+ 'tr',
+ {},
+ this.#domContext.create('td', {}, entry.timestamp.toString()),
+ this.#domContext.create(
+ 'td',
+ { className: 'jasmine-debug-log-msg' },
+ entry.message
+ )
+ )
+ );
+ }
+
+ return this.#domContext.create(
+ 'div',
+ { className: 'jasmine-debug-log' },
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-debug-log-header' },
+ 'Debug logs'
+ ),
+ this.#domContext.create(
+ 'table',
+ {},
+ this.#domContext.create(
+ 'thead',
+ {},
+ this.#domContext.create(
+ 'tr',
+ {},
+ this.#domContext.create('th', {}, 'Time (ms)'),
+ this.#domContext.create('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;
};
@@ -1030,3 +1104,58 @@ jasmineRequire.QueryString = function() {
return QueryString;
};
+
+jasmineRequire.DomContext = function(j$) {
+ 'use strict';
+
+ //TODO maybe rename
+ class DomContext {
+ #createElement;
+
+ constructor(options = {}) {
+ this.#createElement =
+ options.createElement || document.createElement.bind(document);
+ this.createTextNode =
+ options.createTextNode || document.createTextNode.bind(document);
+ }
+
+ create(type, attrs, childrenArrayOrVarArgs) {
+ const el = this.#createElement(type);
+ let children;
+
+ if (j$.private.isArray(childrenArrayOrVarArgs)) {
+ children = childrenArrayOrVarArgs;
+ } else {
+ children = [];
+
+ for (let i = 2; i < arguments.length; i++) {
+ children.push(arguments[i]);
+ }
+ }
+
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+
+ if (typeof child === 'string') {
+ el.appendChild(this.createTextNode(child));
+ } else {
+ if (child) {
+ el.appendChild(child);
+ }
+ }
+ }
+
+ for (const attr in attrs) {
+ if (attr === 'className') {
+ el[attr] = attrs[attr];
+ } else {
+ el.setAttribute(attr, attrs[attr]);
+ }
+ }
+
+ return el;
+ }
+ }
+
+ return DomContext;
+};
diff --git a/src/html/DomContext.js b/src/html/DomContext.js
new file mode 100644
index 00000000..19ea1af6
--- /dev/null
+++ b/src/html/DomContext.js
@@ -0,0 +1,54 @@
+jasmineRequire.DomContext = function(j$) {
+ 'use strict';
+
+ //TODO maybe rename
+ class DomContext {
+ #createElement;
+
+ constructor(options = {}) {
+ this.#createElement =
+ options.createElement || document.createElement.bind(document);
+ this.createTextNode =
+ options.createTextNode || document.createTextNode.bind(document);
+ }
+
+ create(type, attrs, childrenArrayOrVarArgs) {
+ const el = this.#createElement(type);
+ let children;
+
+ if (j$.private.isArray(childrenArrayOrVarArgs)) {
+ children = childrenArrayOrVarArgs;
+ } else {
+ children = [];
+
+ for (let i = 2; i < arguments.length; i++) {
+ children.push(arguments[i]);
+ }
+ }
+
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+
+ if (typeof child === 'string') {
+ el.appendChild(this.createTextNode(child));
+ } else {
+ if (child) {
+ el.appendChild(child);
+ }
+ }
+ }
+
+ for (const attr in attrs) {
+ if (attr === 'className') {
+ el[attr] = attrs[attr];
+ } else {
+ el.setAttribute(attr, attrs[attr]);
+ }
+ }
+
+ return el;
+ }
+ }
+
+ return DomContext;
+};
diff --git a/src/html/HtmlReporter.js b/src/html/HtmlReporter.js
index bb3e8948..4b4c1ec8 100644
--- a/src/html/HtmlReporter.js
+++ b/src/html/HtmlReporter.js
@@ -1,53 +1,80 @@
jasmineRequire.HtmlReporter = function(j$) {
'use strict';
- function ResultsStateBuilder() {
- this.topResults = new j$.private.ResultsNode({}, '', null);
- this.currentParent = this.topResults;
- this.specsExecuted = 0;
- this.failureCount = 0;
- this.pendingSpecCount = 0;
+ 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
+ });
+ }
+ }
+ }
}
- ResultsStateBuilder.prototype.suiteStarted = function(result) {
- this.currentParent.addChild(result, 'suite');
- this.currentParent = this.currentParent.last();
- };
-
- ResultsStateBuilder.prototype.suiteDone = function(result) {
- this.currentParent.updateResult(result);
- if (this.currentParent !== this.topResults) {
- this.currentParent = this.currentParent.parent;
- }
-
- if (result.status === 'failed') {
- this.failureCount++;
- }
- };
-
- ResultsStateBuilder.prototype.specStarted = function(result) {};
-
- ResultsStateBuilder.prototype.specDone = function(result) {
- this.currentParent.addChild(result, 'spec');
-
- if (result.status !== 'excluded') {
- this.specsExecuted++;
- }
-
- if (result.status === 'failed') {
- this.failureCount++;
- }
-
- if (result.status == 'pending') {
- this.pendingSpecCount++;
- }
- };
-
- ResultsStateBuilder.prototype.jasmineDone = function(result) {
- if (result.failedExpectations) {
- this.failureCount += result.failedExpectations.length;
- }
- };
+ const errorBarClassName = 'jasmine-bar jasmine-errored';
+ const afterAllMessagePrefix = 'AfterAll ';
/**
* @class HtmlReporter
@@ -62,16 +89,20 @@ jasmineRequire.HtmlReporter = function(j$) {
}
const getContainer = options.getContainer;
- const createElement = options.createElement;
- const createTextNode = options.createTextNode;
+ const domContext = new j$.private.DomContext({
+ createElement: options.createElement,
+ createTextNode: options.createTextNode
+ });
const navigateWithNewParam = options.navigateWithNewParam || function() {};
const addToExistingQueryString =
options.addToExistingQueryString || defaultQueryString;
+ const urlBuilder = new UrlBuilder(addToExistingQueryString);
const filterSpecs = options.filterSpecs;
let htmlReporterMain;
+ let alerts;
let symbols;
- const deprecationWarnings = [];
- const failures = [];
+ let banner;
+ let failures;
/**
* Initializes the reporter. Should be called before {@link Env#execute}.
@@ -80,37 +111,25 @@ jasmineRequire.HtmlReporter = function(j$) {
*/
this.initialize = function() {
clearPrior();
- htmlReporterMain = createDom(
+ alerts = new AlertsView(domContext, urlBuilder);
+ symbols = new SymbolsView(domContext, config);
+ banner = new Banner(domContext, navigateWithNewParam);
+ failures = new FailuresView(domContext, urlBuilder);
+ htmlReporterMain = domContext.create(
'div',
{ className: 'jasmine_html-reporter' },
- createDom(
- 'div',
- { className: 'jasmine-banner' },
- createDom('a', {
- className: 'jasmine-title',
- href: 'http://jasmine.github.io/',
- target: '_blank'
- }),
- createDom('span', { className: 'jasmine-version' }, j$.version)
- ),
- createDom('ul', { className: 'jasmine-symbol-summary' }),
- createDom('div', { className: 'jasmine-alert' }),
- createDom(
- 'div',
- { className: 'jasmine-results' },
- createDom('div', { className: 'jasmine-failures' })
- )
+ banner.rootEl,
+ symbols.rootEl,
+ alerts.rootEl,
+ failures.rootEl
);
getContainer().appendChild(htmlReporterMain);
};
- let totalSpecsDefined;
this.jasmineStarted = function(options) {
- totalSpecsDefined = options.totalSpecsDefined || 0;
+ stateBuilder.jasmineStarted(options);
};
- const summary = createDom('div', { className: 'jasmine-summary' });
-
const stateBuilder = new ResultsStateBuilder();
this.suiteStarted = function(result) {
@@ -121,17 +140,15 @@ jasmineRequire.HtmlReporter = function(j$) {
stateBuilder.suiteDone(result);
if (result.status === 'failed') {
- failures.push(failureDom(result));
+ failures.append(result, stateBuilder.currentParent);
}
- addDeprecationWarnings(result, 'suite');
};
- this.specStarted = function(result) {
- stateBuilder.specStarted(result);
- };
+ this.specStarted = function(result) {};
this.specDone = function(result) {
stateBuilder.specDone(result);
+ symbols.append(result, config());
if (noExpectations(result)) {
const noSpecMsg = "Spec '" + result.fullName + "' has no expectations.";
@@ -144,86 +161,179 @@ jasmineRequire.HtmlReporter = function(j$) {
}
}
- if (!symbols) {
- symbols = find('.jasmine-symbol-summary');
- }
-
- symbols.appendChild(
- createDom('li', {
- className: this.displaySpecInCorrectFormat(result),
- id: 'spec_' + result.id,
- title: result.fullName
- })
- );
-
if (result.status === 'failed') {
- failures.push(failureDom(result));
+ failures.append(result, stateBuilder.currentParent);
}
-
- addDeprecationWarnings(result, 'spec');
- };
-
- this.displaySpecInCorrectFormat = function(result) {
- return noExpectations(result) && result.status === 'passed'
- ? 'jasmine-empty'
- : this.resultStatus(result.status);
- };
-
- this.resultStatus = function(status) {
- if (status === 'excluded') {
- return config().hideDisabled
- ? 'jasmine-excluded-no-display'
- : 'jasmine-excluded';
- }
- return 'jasmine-' + status;
};
this.jasmineDone = function(doneResult) {
stateBuilder.jasmineDone(doneResult);
- const banner = find('.jasmine-banner');
- const alert = find('.jasmine-alert');
- const order = doneResult && doneResult.order;
+ alerts.addDuration(doneResult.totalTime);
+ banner.showOptionsMenu(config());
- alert.appendChild(
- createDom(
- 'span',
- { className: 'jasmine-duration' },
- 'finished in ' + doneResult.totalTime / 1000 + 's'
- )
- );
-
- banner.appendChild(optionsMenu(config()));
-
- if (stateBuilder.specsExecuted < totalSpecsDefined) {
- const skippedMessage =
- 'Ran ' +
- stateBuilder.specsExecuted +
- ' of ' +
- totalSpecsDefined +
- ' specs - run all';
- // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
- const skippedLink =
- (window.location.pathname || '') +
- addToExistingQueryString('spec', '');
- alert.appendChild(
- createDom(
- 'span',
- { className: 'jasmine-bar jasmine-skipped' },
- createDom(
- 'a',
- { href: skippedLink, title: 'Run all specs' },
- skippedMessage
- )
- )
+ if (stateBuilder.specsExecuted < stateBuilder.totalSpecsDefined) {
+ alerts.addSkipped(
+ stateBuilder.specsExecuted,
+ stateBuilder.totalSpecsDefined
);
}
+
+ alerts.addSeedBar(doneResult, stateBuilder, doneResult.order);
+
+ if (doneResult.failedExpectations) {
+ for (const f of doneResult.failedExpectations) {
+ alerts.addGlobalFailure(f);
+ }
+ }
+
+ for (const dw of stateBuilder.deprecationWarnings) {
+ alerts.addDeprecationWarning(dw);
+ }
+
+ const results = find('.jasmine-results');
+ const summary = new SummaryTreeView(domContext, urlBuilder, filterSpecs);
+ summary.addResults(stateBuilder.topResults);
+ results.appendChild(summary.rootEl);
+
+ if (failures.any()) {
+ alerts.addFailureToggle(
+ function() {
+ setMenuModeTo('jasmine-failure-list');
+ },
+ function() {
+ setMenuModeTo('jasmine-spec-list');
+ }
+ );
+
+ setMenuModeTo('jasmine-failure-list');
+ failures.show();
+ }
+ };
+
+ return this;
+
+ function find(selector) {
+ return getContainer().querySelector('.jasmine_html-reporter ' + selector);
+ }
+
+ function clearPrior() {
+ const oldReporter = find('');
+
+ if (oldReporter) {
+ getContainer().removeChild(oldReporter);
+ }
+ }
+
+ function setMenuModeTo(mode) {
+ htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode);
+ }
+ }
+
+ 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 {
+ #domContext;
+ #urlBuilder;
+
+ constructor(domContext, urlBuilder) {
+ this.#domContext = domContext;
+ this.#urlBuilder = urlBuilder;
+ this.rootEl = domContext.create('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',
+ this.#domContext.create(
+ 'a',
+ { href: this.#urlBuilder.runAllHref(), title: 'Run all specs' },
+ `Ran ${numExecuted} of ${numDefined} specs - run all`
+ )
+ );
+ }
+
+ addFailureToggle(onClickFailures, onClickSpecList) {
+ const failuresLink = this.#domContext.create(
+ 'a',
+ { className: 'jasmine-failures-menu', href: '#' },
+ 'Failures'
+ );
+ let specListLink = this.#domContext.create(
+ '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', [
+ this.#domContext.create('span', {}, 'Spec List | '),
+ failuresLink
+ ]);
+ this.add('jasmine-menu jasmine-bar jasmine-failure-list', [
+ specListLink,
+ this.#domContext.create('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 (totalSpecsDefined > 0 || failed) {
+ if (stateBuilder.totalSpecsDefined > 0 || failed) {
statusBarMessage +=
pluralize('spec', stateBuilder.specsExecuted) +
', ' +
@@ -249,489 +359,89 @@ jasmineRequire.HtmlReporter = function(j$) {
let seedBar;
if (order && order.random) {
- seedBar = createDom(
+ seedBar = this.#domContext.create(
'span',
{ className: 'jasmine-seed-bar' },
', randomized with seed ',
- createDom(
+ this.#domContext.create(
'a',
{
title: 'randomized with seed ' + order.seed,
- href: seedHref(order.seed)
+ href: this.#urlBuilder.seedHref(order.seed)
},
order.seed
)
);
}
- alert.appendChild(
- createDom(
- 'span',
- { className: statusBarClassName },
- statusBarMessage,
- seedBar
- )
- );
+ this.add(statusBarClassName, [statusBarMessage, seedBar]);
+ }
- const errorBarClassName = 'jasmine-bar jasmine-errored';
- const afterAllMessagePrefix = 'AfterAll ';
+ // TODO check test coverage
+ #globalFailureMessage(failure) {
+ if (failure.globalErrorType === 'load') {
+ const prefix = 'Error during loading: ' + failure.message;
- for (let i = 0; i < globalFailures.length; i++) {
- alert.appendChild(
- createDom(
- 'span',
- { className: errorBarClassName },
- globalFailureMessage(globalFailures[i])
- )
- );
- }
-
- function 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;
+ if (failure.filename) {
+ return prefix + ' in ' + failure.filename + ' line ' + failure.lineno;
} else {
- return failure.message;
- }
- }
-
- addDeprecationWarnings(doneResult);
-
- for (let i = 0; i < deprecationWarnings.length; i++) {
- const children = [];
- let context;
-
- switch (deprecationWarnings[i].runnableType) {
- case 'spec':
- context = '(in spec: ' + deprecationWarnings[i].runnableName + ')';
- break;
- case 'suite':
- context = '(in suite: ' + deprecationWarnings[i].runnableName + ')';
- break;
- default:
- context = '';
- }
-
- deprecationWarnings[i].message.split('\n').forEach(function(line) {
- children.push(line);
- children.push(createDom('br'));
- });
-
- children[0] = 'DEPRECATION: ' + children[0];
- children.push(context);
-
- if (deprecationWarnings[i].stack) {
- children.push(createExpander(deprecationWarnings[i].stack));
- }
-
- alert.appendChild(
- createDom(
- 'span',
- { className: 'jasmine-bar jasmine-warning' },
- children
- )
- );
- }
-
- const results = find('.jasmine-results');
- results.appendChild(summary);
-
- summaryList(stateBuilder.topResults, summary);
-
- if (failures.length) {
- alert.appendChild(
- createDom(
- 'span',
- { className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
- createDom('span', {}, 'Spec List | '),
- createDom(
- 'a',
- { className: 'jasmine-failures-menu', href: '#' },
- 'Failures'
- )
- )
- );
- alert.appendChild(
- createDom(
- 'span',
- { className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
- createDom(
- 'a',
- { className: 'jasmine-spec-list-menu', href: '#' },
- 'Spec List'
- ),
- createDom('span', {}, ' | Failures ')
- )
- );
-
- find('.jasmine-failures-menu').onclick = function() {
- setMenuModeTo('jasmine-failure-list');
- return false;
- };
- find('.jasmine-spec-list-menu').onclick = function() {
- setMenuModeTo('jasmine-spec-list');
- return false;
- };
-
- setMenuModeTo('jasmine-failure-list');
-
- const failureNode = find('.jasmine-failures');
- for (let i = 0; i < failures.length; i++) {
- failureNode.appendChild(failures[i]);
- }
- }
- };
-
- return this;
-
- function failureDom(result) {
- const failure = createDom(
- 'div',
- { className: 'jasmine-spec-detail jasmine-failed' },
- failureDescription(result, stateBuilder.currentParent),
- 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(debugLogTable(result.debugLogs));
- }
-
- return failure;
- }
-
- function debugLogTable(debugLogs) {
- const tbody = createDom('tbody');
-
- debugLogs.forEach(function(entry) {
- 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
- )
- );
- }
-
- function summaryList(resultsTree, domParent) {
- let specListNode;
- for (let i = 0; i < resultsTree.children.length; i++) {
- const resultNode = resultsTree.children[i];
- if (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: specHref(resultNode.result) },
- resultNode.result.description
- )
- )
- );
-
- summaryList(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: specHref(resultNode.result) },
- specDescription
- ),
- createDom(
- 'span',
- { className: 'jasmine-spec-duration' },
- '(' + resultNode.result.duration + 'ms)'
- )
- )
- );
+ return prefix;
}
+ } else if (failure.globalErrorType === 'afterAll') {
+ return afterAllMessagePrefix + failure.message;
+ } else {
+ return failure.message;
}
}
- function 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'
- )
- )
- )
- );
+ addDeprecationWarning(dw) {
+ const children = [];
+ let context;
- const failFastCheckbox = optionsMenuDom.querySelector(
- '#jasmine-fail-fast'
- );
- failFastCheckbox.checked = config.stopOnSpecFailure;
- failFastCheckbox.onclick = function() {
- navigateWithNewParam('stopOnSpecFailure', !config.stopOnSpecFailure);
- };
-
- const throwCheckbox = optionsMenuDom.querySelector(
- '#jasmine-throw-failures'
- );
- throwCheckbox.checked = config.stopSpecOnExpectationFailure;
- throwCheckbox.onclick = function() {
- navigateWithNewParam(
- 'stopSpecOnExpectationFailure',
- !config.stopSpecOnExpectationFailure
- );
- };
-
- const randomCheckbox = optionsMenuDom.querySelector(
- '#jasmine-random-order'
- );
- randomCheckbox.checked = config.random;
- randomCheckbox.onclick = function() {
- navigateWithNewParam('random', !config.random);
- };
-
- const hideDisabled = optionsMenuDom.querySelector(
- '#jasmine-hide-disabled'
- );
- hideDisabled.checked = config.hideDisabled;
- hideDisabled.onclick = function() {
- 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;
- }
-
- function failureDescription(result, suite) {
- const wrapper = createDom(
- 'div',
- { className: 'jasmine-description' },
- createDom(
- 'a',
- { title: result.description, href: specHref(result) },
- result.description
- )
- );
- let suiteLink;
-
- while (suite && suite.parent) {
- wrapper.insertBefore(createTextNode(' > '), wrapper.firstChild);
- suiteLink = createDom(
- 'a',
- { href: suiteHref(suite) },
- suite.result.description
- );
- wrapper.insertBefore(suiteLink, wrapper.firstChild);
-
- suite = suite.parent;
+ switch (dw.runnableType) {
+ case 'spec':
+ context = '(in spec: ' + dw.runnableName + ')';
+ break;
+ case 'suite':
+ context = '(in suite: ' + dw.runnableName + ')';
+ break;
+ default:
+ context = '';
}
- return wrapper;
- }
-
- function suiteHref(suite) {
- const els = [];
-
- while (suite && suite.parent) {
- els.unshift(suite.result.description);
- suite = suite.parent;
+ for (const line of dw.message.split('\n')) {
+ children.push(line);
+ children.push(this.#domContext.create('br'));
}
- // 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('spec', els.join(' '))
+ 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(
+ this.#domContext.create('span', { className }, children)
);
}
- function addDeprecationWarnings(result, runnableType) {
- if (result && result.deprecationWarnings) {
- for (let i = 0; i < result.deprecationWarnings.length; i++) {
- const warning = result.deprecationWarnings[i].message;
- deprecationWarnings.push({
- message: warning,
- stack: result.deprecationWarnings[i].stack,
- runnableName: result.fullName,
- runnableType: runnableType
- });
- }
- }
- }
-
- function createExpander(stackTrace) {
- const expandLink = createDom('a', { href: '#' }, 'Show stack trace');
- const root = createDom(
+ #createExpander(stackTrace) {
+ const expandLink = this.#domContext.create(
+ 'a',
+ { href: '#' },
+ 'Show stack trace'
+ );
+ const root = this.#domContext.create(
'div',
{ className: 'jasmine-expander' },
expandLink,
- createDom(
+ this.#domContext.create(
'div',
{ className: 'jasmine-expander-contents jasmine-stack-trace' },
stackTrace
@@ -752,110 +462,473 @@ jasmineRequire.HtmlReporter = function(j$) {
return root;
}
+ }
- function find(selector) {
- return getContainer().querySelector('.jasmine_html-reporter ' + selector);
+ class Banner {
+ #domContext;
+ #navigateWithNewParam;
+
+ constructor(domContext, navigateWithNewParam) {
+ this.#domContext = domContext;
+ this.#navigateWithNewParam = navigateWithNewParam;
+ this.rootEl = domContext.create(
+ 'div',
+ { className: 'jasmine-banner' },
+ domContext.create('a', {
+ className: 'jasmine-title',
+ href: 'http://jasmine.github.io/',
+ target: '_blank'
+ }),
+ domContext.create('span', { className: 'jasmine-version' }, j$.version)
+ );
}
- function clearPrior() {
- const oldReporter = find('');
-
- if (oldReporter) {
- getContainer().removeChild(oldReporter);
- }
+ showOptionsMenu(config) {
+ this.rootEl.appendChild(this.#optionsMenu(config));
}
- function createDom(type, attrs, childrenArrayOrVarArgs) {
- const el = createElement(type);
- let children;
+ #optionsMenu(config) {
+ const optionsMenuDom = this.#domContext.create(
+ 'div',
+ { className: 'jasmine-run-options' },
+ this.#domContext.create(
+ 'span',
+ { className: 'jasmine-trigger' },
+ 'Options'
+ ),
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-payload' },
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-stop-on-failure' },
+ this.#domContext.create('input', {
+ className: 'jasmine-fail-fast',
+ id: 'jasmine-fail-fast',
+ type: 'checkbox'
+ }),
+ this.#domContext.create(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-fail-fast' },
+ 'stop execution on spec failure'
+ )
+ ),
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-throw-failures' },
+ this.#domContext.create('input', {
+ className: 'jasmine-throw',
+ id: 'jasmine-throw-failures',
+ type: 'checkbox'
+ }),
+ this.#domContext.create(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-throw-failures' },
+ 'stop spec on expectation failure'
+ )
+ ),
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-random-order' },
+ this.#domContext.create('input', {
+ className: 'jasmine-random',
+ id: 'jasmine-random-order',
+ type: 'checkbox'
+ }),
+ this.#domContext.create(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-random-order' },
+ 'run tests in random order'
+ )
+ ),
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-hide-disabled' },
+ this.#domContext.create('input', {
+ className: 'jasmine-disabled',
+ id: 'jasmine-hide-disabled',
+ type: 'checkbox'
+ }),
+ this.#domContext.create(
+ 'label',
+ { className: 'jasmine-label', for: 'jasmine-hide-disabled' },
+ 'hide disabled tests'
+ )
+ )
+ )
+ );
- if (j$.private.isArray(childrenArrayOrVarArgs)) {
- children = childrenArrayOrVarArgs;
+ 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 {
+ #domContext;
+
+ constructor(domContext) {
+ this.#domContext = domContext;
+ this.rootEl = domContext.create('ul', {
+ className: 'jasmine-symbol-summary'
+ });
+ }
+
+ append(result, config) {
+ this.rootEl.appendChild(
+ this.#domContext.create('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 {
- children = [];
-
- for (let i = 2; i < arguments.length; i++) {
- children.push(arguments[i]);
- }
+ return 'jasmine-' + result.status;
}
+ }
+ }
- for (let i = 0; i < children.length; i++) {
- const child = children[i];
+ class SummaryTreeView {
+ #domContext;
+ #urlBuilder;
+ #filterSpecs;
- if (typeof child === 'string') {
- el.appendChild(createTextNode(child));
- } else {
- if (child) {
- el.appendChild(child);
+ constructor(domContext, urlBuilder, filterSpecs) {
+ this.#domContext = domContext;
+ this.#urlBuilder = urlBuilder;
+ this.#filterSpecs = filterSpecs;
+ this.rootEl = domContext.create('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 = this.#domContext.create(
+ 'ul',
+ { className: 'jasmine-suite', id: 'suite-' + resultNode.result.id },
+ this.#domContext.create(
+ 'li',
+ {
+ className:
+ 'jasmine-suite-detail jasmine-' + resultNode.result.status
+ },
+ this.#domContext.create(
+ '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 = this.#domContext.create('ul', {
+ className: 'jasmine-specs'
+ });
+ domParent.appendChild(specListNode);
}
- }
- }
-
- for (const attr in attrs) {
- if (attr == 'className') {
- el[attr] = attrs[attr];
- } else {
- el.setAttribute(attr, attrs[attr]);
- }
- }
-
- return el;
- }
-
- function pluralize(singular, count) {
- const word = count == 1 ? singular : singular + 's';
-
- return '' + count + ' ' + word;
- }
-
- function specHref(result) {
- // 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('spec', result.fullName)
- );
- }
-
- function seedHref(seed) {
- // 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('seed', seed)
- );
- }
-
- function defaultQueryString(key, value) {
- return '?' + key + '=' + value;
- }
-
- function setMenuModeTo(mode) {
- htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode);
- }
-
- function noExpectations(result) {
- const allExpectations =
- result.failedExpectations.length + result.passedExpectations.length;
-
- return (
- allExpectations === 0 &&
- (result.status === 'passed' || result.status === 'failed')
- );
- }
-
- 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;
+ 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(
+ this.#domContext.create(
+ 'li',
+ {
+ className: 'jasmine-' + resultNode.result.status,
+ id: 'spec-' + resultNode.result.id
+ },
+ this.#domContext.create(
+ 'a',
+ { href: this.#urlBuilder.specHref(resultNode.result) },
+ specDescription
+ ),
+ this.#domContext.create(
+ 'span',
+ { className: 'jasmine-spec-duration' },
+ '(' + resultNode.result.duration + 'ms)'
+ )
+ )
+ );
}
}
}
}
+ class FailuresView {
+ #domContext;
+ #urlBuilder;
+ #failureEls;
+
+ constructor(domContext, urlBuilder) {
+ this.#domContext = domContext;
+ this.#urlBuilder = urlBuilder;
+ this.#failureEls = [];
+ this.rootEl = domContext.create(
+ 'div',
+ { className: 'jasmine-results' },
+ domContext.create('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 = this.#domContext.create(
+ 'div',
+ { className: 'jasmine-spec-detail jasmine-failed' },
+ this.#failureDescription(result, parent),
+ this.#domContext.create('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(
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-result-message' },
+ expectation.message
+ )
+ );
+ messages.appendChild(
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-stack-trace' },
+ expectation.stack
+ )
+ );
+ }
+
+ if (result.failedExpectations.length === 0) {
+ messages.appendChild(
+ this.#domContext.create(
+ '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 = this.#domContext.create(
+ 'div',
+ { className: 'jasmine-description' },
+ this.#domContext.create(
+ 'a',
+ {
+ title: result.description,
+ href: this.#urlBuilder.specHref(result)
+ },
+ result.description
+ )
+ );
+ let suiteLink;
+
+ while (suite && suite.parent) {
+ wrapper.insertBefore(
+ this.#domContext.createTextNode(' > '),
+ wrapper.firstChild
+ );
+ suiteLink = this.#domContext.create(
+ 'a',
+ { href: this.#urlBuilder.suiteHref(suite) },
+ suite.result.description
+ );
+ wrapper.insertBefore(suiteLink, wrapper.firstChild);
+
+ suite = suite.parent;
+ }
+
+ return wrapper;
+ }
+
+ #debugLogTable(debugLogs) {
+ const tbody = this.#domContext.create('tbody');
+
+ for (const entry of debugLogs) {
+ tbody.appendChild(
+ this.#domContext.create(
+ 'tr',
+ {},
+ this.#domContext.create('td', {}, entry.timestamp.toString()),
+ this.#domContext.create(
+ 'td',
+ { className: 'jasmine-debug-log-msg' },
+ entry.message
+ )
+ )
+ );
+ }
+
+ return this.#domContext.create(
+ 'div',
+ { className: 'jasmine-debug-log' },
+ this.#domContext.create(
+ 'div',
+ { className: 'jasmine-debug-log-header' },
+ 'Debug logs'
+ ),
+ this.#domContext.create(
+ 'table',
+ {},
+ this.#domContext.create(
+ 'thead',
+ {},
+ this.#domContext.create(
+ 'tr',
+ {},
+ this.#domContext.create('th', {}, 'Time (ms)'),
+ this.#domContext.create('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;
};
diff --git a/src/html/requireHtml.js b/src/html/requireHtml.js
index f303f019..2e7a2f13 100644
--- a/src/html/requireHtml.js
+++ b/src/html/requireHtml.js
@@ -3,6 +3,7 @@ var jasmineRequire = window.jasmineRequire || require('./jasmine.js');
jasmineRequire.html = function(j$) {
j$.private.ResultsNode = jasmineRequire.ResultsNode();
+ j$.private.DomContext = jasmineRequire.DomContext(j$);
j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
j$.QueryString = jasmineRequire.QueryString();
j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter();