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