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();