diff --git a/lib/jasmine-core/boot1.js b/lib/jasmine-core/boot1.js index c804eb46..6867f1e6 100644 --- a/lib/jasmine-core/boot1.js +++ b/lib/jasmine-core/boot1.js @@ -77,7 +77,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ## Reporters * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). */ - const htmlReporter = new jasmine.HtmlReporter({ + const htmlReporter = new jasmine.HtmlReporterV2({ env: env, navigateWithNewParam: function(key, value) { return queryString.navigateWithNewParam(key, value); @@ -101,7 +101,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. /** * Filter which specs will be run by matching the start of the full name against the `spec` query param. */ - const specFilter = new jasmine.HtmlSpecFilter({ + const specFilter = new jasmine.HtmlSpecFilterV2({ filterString: function() { return queryString.getParam('spec'); } diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index 194939c1..7e21e32e 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -34,10 +34,11 @@ jasmineRequire.html = function(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$.HtmlReporterV2 = jasmineRequire.HtmlReporterV2(j$); j$.QueryString = jasmineRequire.QueryString(); j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); + j$.HtmlSpecFilterV2 = jasmineRequire.HtmlSpecFilterV2(); }; jasmineRequire.HtmlReporter = function(j$) { @@ -74,7 +75,7 @@ jasmineRequire.HtmlReporter = function(j$) { this.#getContainer = options.getContainer; this.#navigateWithNewParam = options.navigateWithNewParam || function() {}; - this.#urlBuilder = new j$.private.UrlBuilder( + this.#urlBuilder = new UrlBuilder( options.addToExistingQueryString || defaultQueryString ); this.#filterSpecs = options.filterSpecs; @@ -211,6 +212,42 @@ jasmineRequire.HtmlReporter = function(j$) { } } + 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); + } + } + function defaultQueryString(key, value) { return '?' + key + '=' + value; } @@ -933,10 +970,224 @@ jasmineRequire.htmlReporterUtils = function(j$) { return { createDom, noExpectations }; }; -jasmineRequire.HtmlSpecFilter = function() { +jasmineRequire.HtmlReporterV2 = function(j$) { 'use strict'; - function HtmlSpecFilter(options) { + const { createDom, noExpectations } = j$.private.htmlReporterUtils; + + /** + * @class HtmlReporterV2 + * @classdesc Displays results and allows re-running individual specs and suites. + * @implements {Reporter} + * @param options Options object. See lib/jasmine-core/boot1.js for details. + * @since 6.0.0 + */ + class HtmlReporterV2 { + #env; + #getContainer; + #navigateWithNewParam; + #urlBuilder; + #filterSpecs; + #stateBuilder; + #config; + #htmlReporterMain; + + // Sub-views + #alerts; + #symbols; + #banner; + #failures; + + constructor(options) { + this.#env = options.env; + + this.#getContainer = options.getContainer; + this.#navigateWithNewParam = + options.navigateWithNewParam || function() {}; + this.#urlBuilder = new UrlBuilder( + options.addToExistingQueryString || defaultQueryString + ); + this.#filterSpecs = options.filterSpecs; + } + + /** + * Initializes the reporter. Should be called before {@link Env#execute}. + * @function + * @name HtmlReporter#initialize + */ + initialize() { + this.#clearPrior(); + this.#config = this.#env ? this.#env.configuration() : {}; + + this.#stateBuilder = new j$.private.ResultsStateBuilder(); + + 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, + this.#symbols.rootEl, + this.#alerts.rootEl, + this.#failures.rootEl + ); + this.#getContainer().appendChild(this.#htmlReporterMain); + } + + jasmineStarted(options) { + this.#stateBuilder.jasmineStarted(options); + } + + suiteStarted(result) { + this.#stateBuilder.suiteStarted(result); + } + + suiteDone(result) { + this.#stateBuilder.suiteDone(result); + + if (result.status === 'failed') { + this.#failures.append(result, this.#stateBuilder.currentParent); + } + } + + specStarted() {} + + specDone(result) { + this.#stateBuilder.specDone(result); + this.#symbols.append(result, this.#config); + + if (noExpectations(result)) { + const noSpecMsg = "Spec '" + result.fullName + "' has no expectations."; + if (result.status === 'failed') { + // eslint-disable-next-line no-console + console.error(noSpecMsg); + } else { + // eslint-disable-next-line no-console + console.warn(noSpecMsg); + } + } + + if (result.status === 'failed') { + this.#failures.append(result, this.#stateBuilder.currentParent); + } + } + + jasmineDone(doneResult) { + this.#stateBuilder.jasmineDone(doneResult); + this.#alerts.addDuration(doneResult.totalTime); + this.#banner.showOptionsMenu(this.#config); + + if ( + this.#stateBuilder.specsExecuted < this.#stateBuilder.totalSpecsDefined + ) { + this.#alerts.addSkipped( + this.#stateBuilder.specsExecuted, + this.#stateBuilder.totalSpecsDefined + ); + } + + this.#alerts.addSeedBar(doneResult, this.#stateBuilder, doneResult.order); + + if (doneResult.failedExpectations) { + for (const f of doneResult.failedExpectations) { + this.#alerts.addGlobalFailure(f); + } + } + + for (const dw of this.#stateBuilder.deprecationWarnings) { + this.#alerts.addDeprecationWarning(dw); + } + + const results = this.#find('.jasmine-results'); + const summary = new j$.private.SummaryTreeView( + this.#urlBuilder, + this.#filterSpecs + ); + summary.addResults(this.#stateBuilder.topResults); + results.appendChild(summary.rootEl); + + if (this.#failures.any()) { + this.#alerts.addFailureToggle( + () => this.#setMenuModeTo('jasmine-failure-list'), + () => this.#setMenuModeTo('jasmine-spec-list') + ); + + this.#setMenuModeTo('jasmine-failure-list'); + this.#failures.show(); + } + } + + #find(selector) { + return this.#getContainer().querySelector( + '.jasmine_html-reporter ' + selector + ); + } + + #clearPrior() { + const oldReporter = this.#find(''); + + if (oldReporter) { + this.#getContainer().removeChild(oldReporter); + } + } + + #setMenuModeTo(mode) { + this.#htmlReporterMain.setAttribute( + 'class', + 'jasmine_html-reporter ' + mode + ); + } + } + + 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); + } + } + + function defaultQueryString(key, value) { + return '?' + key + '=' + value; + } + + return HtmlReporterV2; +}; + +jasmineRequire.HtmlSpecFilterV2 = function() { + 'use strict'; + + function HtmlSpecFilterV2(options) { const filterString = options && options.filterString() && @@ -955,7 +1206,7 @@ jasmineRequire.HtmlSpecFilter = function() { }; } - return HtmlSpecFilter; + return HtmlSpecFilterV2; }; jasmineRequire.ResultsStateBuilder = function(j$) { @@ -1184,45 +1435,3 @@ jasmineRequire.SymbolsView = function(j$) { 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/spec/core/jasmineNamespaceSpec.js b/spec/core/jasmineNamespaceSpec.js index acde7106..ed6aa43c 100644 --- a/spec/core/jasmineNamespaceSpec.js +++ b/spec/core/jasmineNamespaceSpec.js @@ -53,7 +53,9 @@ describe('The jasmine namespace', function() { if (typeof window !== 'undefined') { // jasmine-html.js result.add('HtmlReporter'); + result.add('HtmlReporterV2'); result.add('HtmlSpecFilter'); + result.add('HtmlSpecFilterV2'); result.add('QueryString'); } diff --git a/spec/html/HtmlReporterV2Spec.js b/spec/html/HtmlReporterV2Spec.js new file mode 100644 index 00000000..c24659a9 --- /dev/null +++ b/spec/html/HtmlReporterV2Spec.js @@ -0,0 +1,1585 @@ +describe('HtmlReporterV2', function() { + let env; + + beforeEach(function() { + env = new privateUnderTest.Env(); + }); + + afterEach(function() { + env.cleanup_(); + }); + + it('builds the initial DOM elements, including the title banner', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + reporter.initialize(); + + // Main top-level elements + expect(container.querySelector('div.jasmine_html-reporter')).toBeTruthy(); + expect(container.querySelector('div.jasmine-banner')).toBeTruthy(); + expect(container.querySelector('div.jasmine-alert')).toBeTruthy(); + expect(container.querySelector('div.jasmine-results')).toBeTruthy(); + + expect(container.querySelector('ul.jasmine-symbol-summary')).toBeTruthy(); + + // title banner + const banner = container.querySelector('.jasmine-banner'); + + const title = banner.querySelector('a.jasmine-title'); + expect(title.getAttribute('href')).toEqual('http://jasmine.github.io/'); + expect(title.getAttribute('target')).toEqual('_blank'); + + const version = banner.querySelector('.jasmine-version'); + expect(version.textContent).toEqual(jasmineUnderTest.version); + }); + + it('builds a single reporter even if initialized multiple times', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + reporter.initialize(); + reporter.initialize(); + reporter.initialize(); + + expect( + container.querySelectorAll('div.jasmine_html-reporter').length + ).toEqual(1); + }); + + describe('when a spec is done', function() { + describe('and no expectations ran', function() { + let container, reporter; + + beforeEach(function() { + container = document.createElement('div'); + reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: function() { + return container; + } + }); + + spyOn(console, 'warn'); + spyOn(console, 'error'); + + reporter.initialize(); + }); + + it('should log warning to the console and print a special symbol when empty spec status is passed', function() { + reporter.specDone({ + status: 'passed', + fullName: 'Some Name', + passedExpectations: [], + failedExpectations: [] + }); + /* eslint-disable-next-line no-console */ + expect(console.warn).toHaveBeenCalledWith( + "Spec 'Some Name' has no expectations." + ); + const specEl = container.querySelector('.jasmine-symbol-summary li'); + expect(specEl.getAttribute('class')).toEqual('jasmine-empty'); + }); + + it('should log error to the console and print a failure symbol when empty spec status is failed', function() { + reporter.specDone({ + status: 'failed', + fullName: 'Some Name', + passedExpectations: [], + failedExpectations: [] + }); + /* eslint-disable-next-line no-console */ + expect(console.error).toHaveBeenCalledWith( + "Spec 'Some Name' has no expectations." + ); + const specEl = container.querySelector('.jasmine-symbol-summary li'); + expect(specEl.getAttribute('class')).toEqual('jasmine-failed'); + }); + }); + + it('reports the status symbol of a excluded spec', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + reporter.initialize(); + reporter.specDone({ + id: 789, + status: 'excluded', + fullName: 'symbols should have titles', + passedExpectations: [], + failedExpectations: [] + }); + + const specEl = container.querySelector('.jasmine-symbol-summary li'); + expect(specEl.getAttribute('class')).toEqual('jasmine-excluded'); + expect(specEl.getAttribute('id')).toEqual('spec_789'); + expect(specEl.getAttribute('title')).toEqual( + 'symbols should have titles' + ); + }); + + it('reports the status symbol of a pending spec', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + reporter.initialize(); + + reporter.specDone({ + id: 789, + status: 'pending', + passedExpectations: [], + failedExpectations: [] + }); + + const specEl = container.querySelector('.jasmine-symbol-summary li'); + expect(specEl.getAttribute('class')).toEqual('jasmine-pending'); + expect(specEl.getAttribute('id')).toEqual('spec_789'); + }); + + it('reports the status symbol of a passing spec', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + reporter.initialize(); + + reporter.specDone({ + id: 123, + status: 'passed', + passedExpectations: [{ passed: true }], + failedExpectations: [] + }); + + const statuses = container.querySelector('.jasmine-symbol-summary'); + const specEl = statuses.querySelector('li'); + expect(specEl.getAttribute('class')).toEqual('jasmine-passed'); + expect(specEl.getAttribute('id')).toEqual('spec_123'); + }); + + it('reports the status symbol of a failing spec', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.specDone({ + id: 345, + status: 'failed', + failedExpectations: [], + passedExpectations: [] + }); + + const specEl = container.querySelector('.jasmine-symbol-summary li'); + expect(specEl.getAttribute('class')).toEqual('jasmine-failed'); + expect(specEl.getAttribute('id')).toEqual('spec_345'); + }); + }); + + describe('when there are deprecation warnings', function() { + it('displays the messages in their own alert bars', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.specDone({ + status: 'passed', + fullName: 'a spec with a deprecation', + deprecationWarnings: [{ message: 'spec deprecation' }], + failedExpectations: [], + passedExpectations: [] + }); + reporter.suiteDone({ + status: 'passed', + fullName: 'a suite with a deprecation', + deprecationWarnings: [{ message: 'suite deprecation' }], + failedExpectations: [] + }); + reporter.jasmineDone({ + deprecationWarnings: [{ message: 'global deprecation' }], + failedExpectations: [] + }); + + const alertBars = container.querySelectorAll( + '.jasmine-alert .jasmine-bar' + ); + + expect(alertBars.length).toEqual(4); + expect(alertBars[1].innerHTML).toMatch( + /spec deprecation.*\(in spec: a spec with a deprecation\)/ + ); + expect(alertBars[1].getAttribute('class')).toEqual( + 'jasmine-bar jasmine-warning' + ); + expect(alertBars[2].innerHTML).toMatch( + /suite deprecation.*\(in suite: a suite with a deprecation\)/ + ); + expect(alertBars[3].innerHTML).toMatch(/global deprecation/); + expect(alertBars[3].innerHTML).not.toMatch(/in /); + }); + + it('displays expandable stack traces', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + deprecationWarnings: [ + { + message: 'a deprecation', + stack: 'a stack trace' + } + ], + failedExpectations: [] + }); + + const expander = container.querySelector( + '.jasmine-alert .jasmine-bar .jasmine-expander' + ); + const expanderContents = expander.querySelector( + '.jasmine-expander-contents' + ); + expect(expanderContents.textContent).toMatch(/a stack trace/); + + const expanderLink = expander.querySelector('a'); + expect(expander).not.toHaveClass('jasmine-expanded'); + expect(expanderLink.textContent).toMatch(/Show stack trace/); + + expanderLink.click(); + expect(expander).toHaveClass('jasmine-expanded'); + expect(expanderLink.textContent).toMatch(/Hide stack trace/); + expanderLink.click(); + + expect(expander).not.toHaveClass('jasmine-expanded'); + expect(expanderLink.textContent).toMatch(/Show stack trace/); + }); + + it('omits the expander when there is no stack trace', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + deprecationWarnings: [ + { + message: 'a deprecation', + stack: '' + } + ], + failedExpectations: [] + }); + + const warningBar = container.querySelector('.jasmine-warning'); + expect(warningBar.querySelector('.jasmine-expander')).toBeFalsy(); + }); + + it('nicely formats the verboseDeprecations note', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + deprecationWarnings: [ + { + message: + 'a deprecation\nNote: This message will be shown only once. Set config.verboseDeprecations to true to see every occurrence.' + } + ], + failedExpectations: [] + }); + + const alertBar = container.querySelector('.jasmine-warning'); + + expect(alertBar.innerHTML).toMatch( + /a deprecation
Note: This message will be shown only once/ + ); + }); + }); + + describe('when Jasmine is done', function() { + it('adds a warning to the link title of specs that have no expectations', function() { + if (!window.console) { + window.console = { error: function() {} }; + } + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + spyOn(console, 'error'); + + reporter.initialize(); + reporter.jasmineStarted({}); + reporter.suiteStarted({ id: 1 }); + reporter.specStarted({ + id: 1, + passedExpectations: [], + failedExpectations: [] + }); + reporter.specDone({ + id: 1, + status: 'passed', + description: 'Spec Description', + passedExpectations: [], + failedExpectations: [] + }); + reporter.suiteDone({ id: 1 }); + reporter.jasmineDone({}); + + const summary = container.querySelector('.jasmine-summary'); + const suite = summary.childNodes[0]; + const specs = suite.childNodes[1]; + const spec = specs.childNodes[0]; + const specLink = spec.childNodes[0]; + expect(specLink.innerHTML).toMatch(/SPEC HAS NO EXPECTATIONS/); + }); + + it('reports the run time', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + + reporter.jasmineDone({ totalTime: 100 }); + + const duration = container.querySelector( + '.jasmine-alert .jasmine-duration' + ); + expect(duration.innerHTML).toMatch(/finished in 0.1s/); + }); + + it('reports the suite names with status, and spec names with status and duration', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer, + addToExistingQueryString: function(key, value) { + return '?foo=bar&' + key + '=' + value; + } + }); + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.suiteStarted({ + id: 1, + description: 'A Suite', + fullName: 'A Suite' + }); + + let specResult = { + id: 123, + description: 'with a spec', + fullName: 'A Suite with a spec', + status: 'passed', + failedExpectations: [], + passedExpectations: [{ passed: true }], + duration: 1230 + }; + reporter.specStarted(specResult); + reporter.specDone(specResult); + + reporter.suiteStarted({ + id: 2, + description: 'inner suite', + fullName: 'A Suite inner suite' + }); + + specResult = { + id: 124, + description: 'with another spec', + fullName: 'A Suite inner suite with another spec', + status: 'passed', + failedExpectations: [], + passedExpectations: [{ passed: true }], + duration: 1240 + }; + reporter.specStarted(specResult); + reporter.specDone(specResult); + + reporter.suiteDone({ + id: 2, + status: 'things', + description: 'inner suite', + fullName: 'A Suite inner suite' + }); + + specResult = { + id: 209, + description: 'with a failing spec', + fullName: 'A Suite inner with a failing spec', + status: 'failed', + failedExpectations: [{}], + passedExpectations: [], + duration: 2090 + }; + reporter.specStarted(specResult); + reporter.specDone(specResult); + + reporter.suiteDone({ + id: 1, + status: 'things', + description: 'A Suite', + fullName: 'A Suite' + }); + + reporter.jasmineDone({}); + const summary = container.querySelector('.jasmine-summary'); + + expect(summary.childNodes.length).toEqual(1); + + const outerSuite = summary.childNodes[0]; + expect(outerSuite.childNodes.length).toEqual(4); + + const classes = []; + for (let i = 0; i < outerSuite.childNodes.length; i++) { + const node = outerSuite.childNodes[i]; + classes.push(node.getAttribute('class')); + } + expect(classes).toEqual([ + 'jasmine-suite-detail jasmine-things', + 'jasmine-specs', + 'jasmine-suite', + 'jasmine-specs' + ]); + + const suiteDetail = outerSuite.childNodes[0]; + const suiteLink = suiteDetail.childNodes[0]; + expect(suiteLink.innerHTML).toEqual('A Suite'); + expect(suiteLink.getAttribute('href')).toEqual('/?foo=bar&spec=A Suite'); + + const specs = outerSuite.childNodes[1]; + const spec = specs.childNodes[0]; + expect(spec.getAttribute('class')).toEqual('jasmine-passed'); + expect(spec.getAttribute('id')).toEqual('spec-123'); + + const specLink = spec.childNodes[0]; + expect(specLink.innerHTML).toEqual('with a spec'); + expect(specLink.getAttribute('href')).toEqual( + '/?foo=bar&spec=A Suite with a spec' + ); + + const specDuration = spec.childNodes[1]; + expect(specDuration.innerHTML).toEqual('(1230ms)'); + }); + + it('has an options menu', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + reporter.jasmineDone({}); + + const trigger = container.querySelector( + '.jasmine-run-options .jasmine-trigger' + ), + payload = container.querySelector( + '.jasmine-run-options .jasmine-payload' + ); + + expect(payload).not.toHaveClass('jasmine-open'); + + trigger.onclick(); + + expect(payload).toHaveClass('jasmine-open'); + + trigger.onclick(); + + expect(payload).not.toHaveClass('jasmine-open'); + }); + + describe('when there are global errors', function() { + it('displays the exceptions in their own alert bars', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + failedExpectations: [ + { + message: 'Global After All Failure', + globalErrorType: 'afterAll' + }, + { message: 'Your JS is borken', globalErrorType: 'load' } + ] + }); + + const alertBars = container.querySelectorAll( + '.jasmine-alert .jasmine-bar' + ); + + expect(alertBars.length).toEqual(3); + expect(alertBars[1].getAttribute('class')).toEqual( + 'jasmine-bar jasmine-errored' + ); + expect(alertBars[1].innerHTML).toMatch( + /AfterAll Global After All Failure/ + ); + expect(alertBars[2].innerHTML).toMatch( + /Error during loading: Your JS is borken/ + ); + expect(alertBars[2].innerHTML).not.toMatch(/line/); + }); + + it('does not display the "AfterAll" prefix for other error types', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + failedExpectations: [ + { message: 'load error', globalErrorType: 'load' }, + { + message: 'lateExpectation error', + globalErrorType: 'lateExpectation' + }, + { message: 'lateError error', globalErrorType: 'lateError' } + ] + }); + + const alertBars = container.querySelectorAll( + '.jasmine-alert .jasmine-bar' + ); + + expect(alertBars.length).toEqual(4); + expect(alertBars[1].textContent).toContain('load error'); + expect(alertBars[2].textContent).toContain('lateExpectation error'); + expect(alertBars[3].textContent).toContain('lateError error'); + + for (let bar of alertBars) { + expect(bar.textContent).not.toContain('AfterAll'); + } + }); + + it('displays file and line information if available', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + failedExpectations: [ + { + message: 'Your JS is borken', + globalErrorType: 'load', + filename: 'some/file.js', + lineno: 42 + } + ] + }); + + const alertBars = container.querySelectorAll( + '.jasmine-alert .jasmine-bar' + ); + + expect(alertBars.length).toEqual(2); + expect(alertBars[1].innerHTML).toMatch( + /Error during loading: Your JS is borken in some\/file.js line 42/ + ); + }); + }); + + describe('UI for stop on spec failure', function() { + it('should be unchecked for full execution', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + reporter.jasmineDone({}); + + const stopOnFailureUI = container.querySelector('.jasmine-fail-fast'); + expect(stopOnFailureUI.checked).toBe(false); + }); + + it('should be checked if stopping short', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + env.configure({ stopOnSpecFailure: true }); + + reporter.initialize(); + reporter.jasmineDone({}); + + const stopOnFailureUI = container.querySelector('.jasmine-fail-fast'); + expect(stopOnFailureUI.checked).toBe(true); + }); + + it('should navigate and turn the setting on', function() { + const container = document.createElement('div'); + const navigationHandler = jasmine.createSpy('navigate'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + navigateWithNewParam: navigationHandler, + getContainer: getContainer + }); + + reporter.initialize(); + reporter.jasmineDone({}); + + const stopOnFailureUI = container.querySelector('.jasmine-fail-fast'); + stopOnFailureUI.click(); + + expect(navigationHandler).toHaveBeenCalledWith( + 'stopOnSpecFailure', + true + ); + }); + + it('should navigate and turn the setting off', function() { + const container = document.createElement('div'); + const navigationHandler = jasmine.createSpy('navigate'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + navigateWithNewParam: navigationHandler, + getContainer: getContainer + }); + + env.configure({ stopOnSpecFailure: true }); + + reporter.initialize(); + reporter.jasmineDone({}); + + const stopOnFailureUI = container.querySelector('.jasmine-fail-fast'); + stopOnFailureUI.click(); + + expect(navigationHandler).toHaveBeenCalledWith( + 'stopOnSpecFailure', + false + ); + }); + }); + + describe('UI for throwing errors on expectation failures', function() { + it('should be unchecked if not throwing', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + reporter.jasmineDone({}); + + const throwingExpectationsUI = container.querySelector( + '.jasmine-throw' + ); + expect(throwingExpectationsUI.checked).toBe(false); + }); + + it('should be checked if throwing', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + env.configure({ stopSpecOnExpectationFailure: true }); + + reporter.initialize(); + reporter.jasmineDone({}); + + const throwingExpectationsUI = container.querySelector( + '.jasmine-throw' + ); + expect(throwingExpectationsUI.checked).toBe(true); + }); + + it('should navigate and change the setting to on', function() { + const container = document.createElement('div'); + const navigateHandler = jasmine.createSpy('navigate'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer, + navigateWithNewParam: navigateHandler + }); + + reporter.initialize(); + reporter.jasmineDone({}); + + const throwingExpectationsUI = container.querySelector( + '.jasmine-throw' + ); + throwingExpectationsUI.click(); + + expect(navigateHandler).toHaveBeenCalledWith( + 'stopSpecOnExpectationFailure', + true + ); + }); + + it('should navigate and change the setting to off', function() { + const container = document.createElement('div'); + const navigateHandler = jasmine.createSpy('navigate'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer, + navigateWithNewParam: navigateHandler + }); + + env.configure({ stopSpecOnExpectationFailure: true }); + + reporter.initialize(); + reporter.jasmineDone({}); + + const throwingExpectationsUI = container.querySelector( + '.jasmine-throw' + ); + throwingExpectationsUI.click(); + + expect(navigateHandler).toHaveBeenCalledWith( + 'stopSpecOnExpectationFailure', + false + ); + }); + }); + + describe('UI for hiding disabled specs', function() { + it('should be unchecked if not hiding disabled specs', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + env.configure({ hideDisabled: false }); + reporter.initialize(); + reporter.jasmineDone({}); + + const disabledUI = container.querySelector('.jasmine-disabled'); + expect(disabledUI.checked).toBe(false); + }); + + it('should be checked if hiding disabled', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + env.configure({ hideDisabled: true }); + reporter.initialize(); + reporter.jasmineDone({}); + + const disabledUI = container.querySelector('.jasmine-disabled'); + expect(disabledUI.checked).toBe(true); + }); + + it('should not display specs that have been disabled', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + env.configure({ hideDisabled: true }); + reporter.initialize(); + reporter.specDone({ + id: 789, + status: 'excluded', + fullName: 'symbols should have titles', + passedExpectations: [], + failedExpectations: [] + }); + + const specEl = container.querySelector('.jasmine-symbol-summary li'); + expect(specEl.getAttribute('class')).toEqual( + 'jasmine-excluded-no-display' + ); + }); + }); + + describe('UI for running tests in random order', function() { + it('should be unchecked if not randomizing', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + env.configure({ random: false }); + reporter.initialize(); + reporter.jasmineDone({}); + + const randomUI = container.querySelector('.jasmine-random'); + expect(randomUI.checked).toBe(false); + }); + + it('should be checked if randomizing', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + env.configure({ random: true }); + reporter.initialize(); + reporter.jasmineDone({}); + + const randomUI = container.querySelector('.jasmine-random'); + expect(randomUI.checked).toBe(true); + }); + + it('should navigate and change the setting to on', function() { + const container = document.createElement('div'); + const navigateHandler = jasmine.createSpy('navigate'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer, + navigateWithNewParam: navigateHandler + }); + + env.configure({ random: false }); + reporter.initialize(); + reporter.jasmineDone({}); + + const randomUI = container.querySelector('.jasmine-random'); + randomUI.click(); + + expect(navigateHandler).toHaveBeenCalledWith('random', true); + }); + + it('should navigate and change the setting to off', function() { + const container = document.createElement('div'); + const navigateHandler = jasmine.createSpy('navigate'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer, + navigateWithNewParam: navigateHandler + }); + + env.configure({ random: true }); + reporter.initialize(); + reporter.jasmineDone({}); + + const randomUI = container.querySelector('.jasmine-random'); + randomUI.click(); + + expect(navigateHandler).toHaveBeenCalledWith('random', false); + }); + + it('should show the seed bar if randomizing', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + reporter.jasmineDone({ + order: { + random: true, + seed: '424242' + } + }); + + const seedBar = container.querySelector('.jasmine-seed-bar'); + expect(seedBar.textContent).toBe(', randomized with seed 424242'); + const seedLink = container.querySelector('.jasmine-seed-bar a'); + expect(seedLink.getAttribute('href')).toBe('/?seed=424242'); + }); + + it('should not show the current seed bar if not randomizing', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + reporter.jasmineDone({}); + + const seedBar = container.querySelector('.jasmine-seed-bar'); + expect(seedBar).toBeNull(); + }); + + it('should include non-spec query params in the jasmine-skipped link when present', function() { + const container = document.createElement('div'); + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: function() { + return container; + }, + addToExistingQueryString: function(key, value) { + return '?foo=bar&' + key + '=' + value; + } + }); + + reporter.initialize(); + reporter.jasmineStarted({ totalSpecsDefined: 1 }); + reporter.jasmineDone({ order: { random: true } }); + + const skippedLink = container.querySelector('.jasmine-skipped a'); + expect(skippedLink.getAttribute('href')).toEqual('/?foo=bar&spec='); + }); + }); + + describe('and all specs pass', function() { + let container; + + beforeEach(function() { + container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + reporter.initialize(); + + reporter.jasmineStarted({ totalSpecsDefined: 2 }); + reporter.specDone({ + id: 123, + description: 'with a spec', + fullName: 'A Suite with a spec', + status: 'passed', + passedExpectations: [{ passed: true }], + failedExpectations: [] + }); + reporter.specDone({ + id: 124, + description: 'with another spec', + fullName: 'A Suite inner suite with another spec', + status: 'passed', + passedExpectations: [{ passed: true }], + failedExpectations: [] + }); + reporter.jasmineDone({}); + }); + + it('reports the specs counts', function() { + const alertBars = container.querySelectorAll( + '.jasmine-alert .jasmine-bar' + ); + + expect(alertBars.length).toEqual(1); + expect(alertBars[0].innerHTML).toMatch(/2 specs, 0 failures/); + }); + + it('reports no failure details', function() { + const specFailure = container.querySelector('.jasmine-failures'); + + expect(specFailure.childNodes.length).toEqual(0); + }); + + it('reports no pending specs', function() { + const alertBar = container.querySelector('.jasmine-alert .jasmine-bar'); + + expect(alertBar.innerHTML).not.toMatch(/pending spec[s]/); + }); + }); + + describe('and there are excluded specs', function() { + let container, reporter, reporterConfig, specStatus; + + beforeEach(function() { + container = document.createElement('div'); + reporterConfig = { + env: env, + getContainer: function() { + return container; + } + }; + specStatus = { + id: 123, + description: 'with a excluded spec', + fullName: 'A Suite with a excluded spec', + status: 'excluded', + passedExpectations: [], + failedExpectations: [] + }; + }); + + describe('when the specs are not filtered', function() { + beforeEach(function() { + reporterConfig.filterSpecs = false; + reporter = new jasmineUnderTest.HtmlReporterV2(reporterConfig); + reporter.initialize(); + reporter.jasmineStarted({ totalSpecsDefined: 1 }); + reporter.specStarted(specStatus); + reporter.specDone(specStatus); + reporter.jasmineDone({}); + }); + + it('shows the excluded spec in the spec list', function() { + const specList = container.querySelector('.jasmine-summary'); + + expect(specList.innerHTML).toContain('with a excluded spec'); + }); + }); + + describe('when the specs are filtered', function() { + beforeEach(function() { + reporterConfig.filterSpecs = true; + reporter = new jasmineUnderTest.HtmlReporterV2(reporterConfig); + reporter.initialize(); + reporter.jasmineStarted({ totalSpecsDefined: 1 }); + reporter.specStarted(specStatus); + reporter.specDone(specStatus); + reporter.jasmineDone({}); + }); + + it("doesn't show the excluded spec in the spec list", function() { + const specList = container.querySelector('.jasmine-summary'); + + expect(specList.innerHTML).toEqual(''); + }); + }); + }); + + describe('and there are pending specs', function() { + let container, reporter; + + function pendingSpecStatus() { + return { + id: 123, + description: 'with a spec', + fullName: 'A Suite with a spec', + status: 'pending', + passedExpectations: [], + failedExpectations: [] + }; + } + + function reportWithSpecStatus(specStatus) { + reporter.specStarted(specStatus); + reporter.specDone(specStatus); + reporter.jasmineDone({}); + } + + beforeEach(function() { + container = document.createElement('div'); + const getContainer = function() { + return container; + }; + reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + reporter.initialize(); + + reporter.jasmineStarted({ totalSpecsDefined: 1 }); + }); + + it('reports the pending specs count', function() { + reportWithSpecStatus(pendingSpecStatus()); + const alertBar = container.querySelector('.jasmine-alert .jasmine-bar'); + + expect(alertBar.innerHTML).toMatch( + /1 spec, 0 failures, 1 pending spec/ + ); + }); + + it('reports no failure details', function() { + reportWithSpecStatus(pendingSpecStatus()); + const specFailure = container.querySelector('.jasmine-failures'); + + expect(specFailure.childNodes.length).toEqual(0); + }); + + it('displays the custom pending reason', function() { + reportWithSpecStatus({ + ...pendingSpecStatus(), + pendingReason: 'my custom pending reason' + }); + const pendingDetails = container.querySelector( + '.jasmine-summary .jasmine-pending' + ); + + expect(pendingDetails.innerHTML).toContain( + 'PENDING WITH MESSAGE: my custom pending reason' + ); + }); + + it('indicates that the spec is pending even if there is no reason', function() { + reportWithSpecStatus({ + ...pendingSpecStatus(), + pendingReason: '' + }); + const pendingDetails = container.querySelector( + '.jasmine-summary .jasmine-pending' + ); + + expect(pendingDetails.innerHTML).toContain('PENDING'); + }); + }); + + describe('and some tests fail', function() { + let container, reporter; + + beforeEach(function() { + container = document.createElement('div'); + const getContainer = function() { + return container; + }; + reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer, + addToExistingQueryString: function(key, value) { + return '?foo=bar&' + key + '=' + value; + } + }); + reporter.initialize(); + + reporter.jasmineStarted({ totalSpecsDefined: 1 }); + reporter.suiteStarted({ + id: 1, + description: 'A suite' + }); + reporter.suiteStarted({ + id: 2, + description: 'inner suite' + }); + + const passingSpecResult = { + id: 123, + status: 'passed', + passedExpectations: [{ passed: true }], + failedExpectations: [] + }; + reporter.specStarted(passingSpecResult); + reporter.specDone(passingSpecResult); + + const failingSpecResult = { + id: 124, + status: 'failed', + description: 'a failing spec', + fullName: 'a suite inner suite a failing spec', + passedExpectations: [], + failedExpectations: [ + { + message: 'a failure message', + stack: 'a stack trace' + } + ] + }; + const failingSpecResultWithDebugLogs = { + id: 567, + status: 'failed', + description: 'a failing spec', + fullName: 'a suite inner suite a failing spec', + passedExpectations: [], + failedExpectations: [ + { + message: 'a failure message', + stack: 'a stack trace' + } + ], + debugLogs: [ + { timestamp: 123, message: 'msg 1' }, + { timestamp: 456, message: 'msg 1' } + ] + }; + + const passingSuiteResult = { + id: 1, + description: 'A suite' + }; + const failingSuiteResult = { + id: 2, + description: 'a suite', + fullName: 'a suite', + status: 'failed', + failedExpectations: [{ message: 'My After All Exception' }] + }; + reporter.specStarted(failingSpecResult); + reporter.specDone(failingSpecResult); + reporter.suiteDone(passingSuiteResult); + reporter.suiteDone(failingSuiteResult); + reporter.suiteDone(passingSuiteResult); + reporter.specStarted(failingSpecResultWithDebugLogs); + reporter.specDone(failingSpecResultWithDebugLogs); + reporter.jasmineDone({}); + }); + + it('reports the specs counts', function() { + const alertBar = container.querySelector('.jasmine-alert .jasmine-bar'); + expect(alertBar.innerHTML).toMatch(/3 specs, 3 failures/); + }); + + it('reports failure messages and stack traces', function() { + const specFailures = container.querySelector('.jasmine-failures'); + + expect(specFailures.childNodes.length).toEqual(3); + + const specFailure = specFailures.childNodes[0]; + expect(specFailure.getAttribute('class')).toMatch(/jasmine-failed/); + expect(specFailure.getAttribute('class')).toMatch( + /jasmine-spec-detail/ + ); + + const specDiv = specFailure.childNodes[0]; + expect(specDiv.getAttribute('class')).toEqual('jasmine-description'); + + const message = specFailure.childNodes[1].childNodes[0]; + expect(message.getAttribute('class')).toEqual('jasmine-result-message'); + expect(message.innerHTML).toEqual('a failure message'); + + const stackTrace = specFailure.childNodes[1].childNodes[1]; + expect(stackTrace.getAttribute('class')).toEqual('jasmine-stack-trace'); + expect(stackTrace.innerHTML).toEqual('a stack trace'); + + const suiteFailure = specFailures.childNodes[0]; + expect(suiteFailure.getAttribute('class')).toMatch(/jasmine-failed/); + expect(suiteFailure.getAttribute('class')).toMatch( + /jasmine-spec-detail/ + ); + + const suiteDiv = suiteFailure.childNodes[0]; + expect(suiteDiv.getAttribute('class')).toEqual('jasmine-description'); + + const suiteMessage = suiteFailure.childNodes[1].childNodes[0]; + expect(suiteMessage.getAttribute('class')).toEqual( + 'jasmine-result-message' + ); + expect(suiteMessage.innerHTML).toEqual('a failure message'); + + const suiteStackTrace = suiteFailure.childNodes[1].childNodes[1]; + expect(suiteStackTrace.getAttribute('class')).toEqual( + 'jasmine-stack-trace' + ); + expect(suiteStackTrace.innerHTML).toEqual('a stack trace'); + }); + + it('reports traces when present', function() { + const specFailure = container.querySelectorAll( + '.jasmine-spec-detail.jasmine-failed' + )[2], + debugLogs = specFailure.querySelector('.jasmine-debug-log table'); + + expect(debugLogs).toBeTruthy(); + const rows = debugLogs.querySelectorAll('tbody tr'); + expect(rows.length).toEqual(2); + }); + + it('provides links to focus on a failure and each containing suite', function() { + const description = container.querySelector( + '.jasmine-failures .jasmine-description' + ); + const links = description.querySelectorAll('a'); + + expect(description.textContent).toEqual( + 'A suite > inner suite > a failing spec' + ); + + expect(links.length).toEqual(3); + expect(links[0].textContent).toEqual('A suite'); + + expect(links[0].getAttribute('href')).toMatch(/\?foo=bar&spec=A suite/); + + expect(links[1].textContent).toEqual('inner suite'); + expect(links[1].getAttribute('href')).toMatch( + /\?foo=bar&spec=A suite inner suite/ + ); + + expect(links[2].textContent).toEqual('a failing spec'); + expect(links[2].getAttribute('href')).toMatch( + /\?foo=bar&spec=a suite inner suite a failing spec/ + ); + }); + + it('allows switching between failure details and the spec summary', function() { + const menuBar = container.querySelectorAll('.jasmine-bar')[1]; + + expect(menuBar.getAttribute('class')).not.toMatch(/hidden/); + + const link = menuBar.querySelector('a'); + expect(link.innerHTML).toEqual('Failures'); + expect(link.getAttribute('href')).toEqual('#'); + }); + + it("sets the reporter to 'Failures List' mode", function() { + const reporterNode = container.querySelector('.jasmine_html-reporter'); + expect(reporterNode.getAttribute('class')).toMatch( + 'jasmine-failure-list' + ); + }); + }); + + it('counts failures that are reported in the jasmineDone event', function() { + const container = document.createElement('div'); + function getContainer() { + return container; + } + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer, + addToExistingQueryString: function(key, value) { + return '?' + key + '=' + value; + } + }); + reporter.initialize(); + + reporter.jasmineStarted({ totalSpecsDefined: 1 }); + + const failingSpecResult = { + id: 124, + status: 'failed', + description: 'a failing spec', + fullName: 'a suite inner suite a failing spec', + passedExpectations: [], + failedExpectations: [ + { + message: 'a failure message', + stack: 'a stack trace' + } + ] + }; + + reporter.specStarted(failingSpecResult); + reporter.specDone(failingSpecResult); + reporter.jasmineDone({ + failedExpectations: [ + { + message: 'a failure message', + stack: 'a stack trace' + }, + { + message: 'a failure message', + stack: 'a stack trace' + } + ] + }); + + const alertBar = container.querySelector('.jasmine-alert .jasmine-bar'); + expect(alertBar.innerHTML).toMatch(/1 spec, 3 failures/); + }); + }); + + describe('The overall result bar', function() { + describe("When the jasmineDone event's overallStatus is 'passed'", function() { + it('has class jasmine-passed', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + overallStatus: 'passed', + failedExpectations: [] + }); + + const alertBar = container.querySelector('.jasmine-overall-result'); + expect(alertBar).toHaveClass('jasmine-passed'); + }); + }); + + describe("When the jasmineDone event's overallStatus is 'failed'", function() { + it('has class jasmine-failed', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + overallStatus: 'failed', + failedExpectations: [] + }); + + const alertBar = container.querySelector('.jasmine-overall-result'); + expect(alertBar).toHaveClass('jasmine-failed'); + }); + }); + + describe("When the jasmineDone event's overallStatus is 'incomplete'", function() { + it('has class jasmine-incomplete', function() { + const container = document.createElement('div'); + const getContainer = function() { + return container; + }; + const reporter = new jasmineUnderTest.HtmlReporterV2({ + env: env, + getContainer: getContainer + }); + + reporter.initialize(); + + reporter.jasmineStarted({}); + reporter.jasmineDone({ + overallStatus: 'incomplete', + incompleteReason: 'because nope', + failedExpectations: [] + }); + + const alertBar = container.querySelector('.jasmine-overall-result'); + expect(alertBar).toHaveClass('jasmine-incomplete'); + expect(alertBar.textContent).toContain('Incomplete: because nope'); + }); + }); + }); +}); diff --git a/spec/html/HtmlSpecFilterSpec.js b/spec/html/HtmlSpecFilterSpec.js index 8fa9a05d..758851fb 100644 --- a/spec/html/HtmlSpecFilterSpec.js +++ b/spec/html/HtmlSpecFilterSpec.js @@ -1,4 +1,4 @@ -describe('jasmineUnderTest.HtmlSpecFilter', function() { +describe('HtmlSpecFilter', function() { it('should match when no string is provided', function() { const specFilter = new jasmineUnderTest.HtmlSpecFilter(); diff --git a/spec/html/HtmlSpecFilterV2Spec.js b/spec/html/HtmlSpecFilterV2Spec.js new file mode 100644 index 00000000..61b339d5 --- /dev/null +++ b/spec/html/HtmlSpecFilterV2Spec.js @@ -0,0 +1,19 @@ +describe('HtmlSpecFilterV2', function() { + it('should match when no string is provided', function() { + const specFilter = new jasmineUnderTest.HtmlSpecFilterV2(); + + expect(specFilter.matches('foo')).toBe(true); + expect(specFilter.matches('*bar')).toBe(true); + }); + + it('should only match the provided string', function() { + const specFilter = new jasmineUnderTest.HtmlSpecFilterV2({ + filterString: function() { + return 'foo'; + } + }); + + expect(specFilter.matches('foo')).toBe(true); + expect(specFilter.matches('bar')).toBe(false); + }); +}); diff --git a/src/boot/boot1.js b/src/boot/boot1.js index 8a5e2ae0..5b8e2d91 100644 --- a/src/boot/boot1.js +++ b/src/boot/boot1.js @@ -53,7 +53,7 @@ * ## Reporters * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). */ - const htmlReporter = new jasmine.HtmlReporter({ + const htmlReporter = new jasmine.HtmlReporterV2({ env: env, navigateWithNewParam: function(key, value) { return queryString.navigateWithNewParam(key, value); @@ -77,7 +77,7 @@ /** * Filter which specs will be run by matching the start of the full name against the `spec` query param. */ - const specFilter = new jasmine.HtmlSpecFilter({ + const specFilter = new jasmine.HtmlSpecFilterV2({ filterString: function() { return queryString.getParam('spec'); } diff --git a/src/html/HtmlReporter.js b/src/html/HtmlReporter.js index 856448ec..45bdf071 100644 --- a/src/html/HtmlReporter.js +++ b/src/html/HtmlReporter.js @@ -32,7 +32,7 @@ jasmineRequire.HtmlReporter = function(j$) { this.#getContainer = options.getContainer; this.#navigateWithNewParam = options.navigateWithNewParam || function() {}; - this.#urlBuilder = new j$.private.UrlBuilder( + this.#urlBuilder = new UrlBuilder( options.addToExistingQueryString || defaultQueryString ); this.#filterSpecs = options.filterSpecs; @@ -169,6 +169,42 @@ jasmineRequire.HtmlReporter = function(j$) { } } + 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); + } + } + function defaultQueryString(key, value) { return '?' + key + '=' + value; } diff --git a/src/html/HtmlReporterV2.js b/src/html/HtmlReporterV2.js new file mode 100644 index 00000000..18fdc0ab --- /dev/null +++ b/src/html/HtmlReporterV2.js @@ -0,0 +1,213 @@ +jasmineRequire.HtmlReporterV2 = function(j$) { + 'use strict'; + + const { createDom, noExpectations } = j$.private.htmlReporterUtils; + + /** + * @class HtmlReporterV2 + * @classdesc Displays results and allows re-running individual specs and suites. + * @implements {Reporter} + * @param options Options object. See lib/jasmine-core/boot1.js for details. + * @since 6.0.0 + */ + class HtmlReporterV2 { + #env; + #getContainer; + #navigateWithNewParam; + #urlBuilder; + #filterSpecs; + #stateBuilder; + #config; + #htmlReporterMain; + + // Sub-views + #alerts; + #symbols; + #banner; + #failures; + + constructor(options) { + this.#env = options.env; + + this.#getContainer = options.getContainer; + this.#navigateWithNewParam = + options.navigateWithNewParam || function() {}; + this.#urlBuilder = new UrlBuilder( + options.addToExistingQueryString || defaultQueryString + ); + this.#filterSpecs = options.filterSpecs; + } + + /** + * Initializes the reporter. Should be called before {@link Env#execute}. + * @function + * @name HtmlReporter#initialize + */ + initialize() { + this.#clearPrior(); + this.#config = this.#env ? this.#env.configuration() : {}; + + this.#stateBuilder = new j$.private.ResultsStateBuilder(); + + 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, + this.#symbols.rootEl, + this.#alerts.rootEl, + this.#failures.rootEl + ); + this.#getContainer().appendChild(this.#htmlReporterMain); + } + + jasmineStarted(options) { + this.#stateBuilder.jasmineStarted(options); + } + + suiteStarted(result) { + this.#stateBuilder.suiteStarted(result); + } + + suiteDone(result) { + this.#stateBuilder.suiteDone(result); + + if (result.status === 'failed') { + this.#failures.append(result, this.#stateBuilder.currentParent); + } + } + + specStarted() {} + + specDone(result) { + this.#stateBuilder.specDone(result); + this.#symbols.append(result, this.#config); + + if (noExpectations(result)) { + const noSpecMsg = "Spec '" + result.fullName + "' has no expectations."; + if (result.status === 'failed') { + // eslint-disable-next-line no-console + console.error(noSpecMsg); + } else { + // eslint-disable-next-line no-console + console.warn(noSpecMsg); + } + } + + if (result.status === 'failed') { + this.#failures.append(result, this.#stateBuilder.currentParent); + } + } + + jasmineDone(doneResult) { + this.#stateBuilder.jasmineDone(doneResult); + this.#alerts.addDuration(doneResult.totalTime); + this.#banner.showOptionsMenu(this.#config); + + if ( + this.#stateBuilder.specsExecuted < this.#stateBuilder.totalSpecsDefined + ) { + this.#alerts.addSkipped( + this.#stateBuilder.specsExecuted, + this.#stateBuilder.totalSpecsDefined + ); + } + + this.#alerts.addSeedBar(doneResult, this.#stateBuilder, doneResult.order); + + if (doneResult.failedExpectations) { + for (const f of doneResult.failedExpectations) { + this.#alerts.addGlobalFailure(f); + } + } + + for (const dw of this.#stateBuilder.deprecationWarnings) { + this.#alerts.addDeprecationWarning(dw); + } + + const results = this.#find('.jasmine-results'); + const summary = new j$.private.SummaryTreeView( + this.#urlBuilder, + this.#filterSpecs + ); + summary.addResults(this.#stateBuilder.topResults); + results.appendChild(summary.rootEl); + + if (this.#failures.any()) { + this.#alerts.addFailureToggle( + () => this.#setMenuModeTo('jasmine-failure-list'), + () => this.#setMenuModeTo('jasmine-spec-list') + ); + + this.#setMenuModeTo('jasmine-failure-list'); + this.#failures.show(); + } + } + + #find(selector) { + return this.#getContainer().querySelector( + '.jasmine_html-reporter ' + selector + ); + } + + #clearPrior() { + const oldReporter = this.#find(''); + + if (oldReporter) { + this.#getContainer().removeChild(oldReporter); + } + } + + #setMenuModeTo(mode) { + this.#htmlReporterMain.setAttribute( + 'class', + 'jasmine_html-reporter ' + mode + ); + } + } + + 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); + } + } + + function defaultQueryString(key, value) { + return '?' + key + '=' + value; + } + + return HtmlReporterV2; +}; diff --git a/src/html/HtmlSpecFilterv2.js b/src/html/HtmlSpecFilterv2.js new file mode 100644 index 00000000..8ad66d68 --- /dev/null +++ b/src/html/HtmlSpecFilterv2.js @@ -0,0 +1,24 @@ +jasmineRequire.HtmlSpecFilterV2 = function() { + 'use strict'; + + function HtmlSpecFilterV2(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 HtmlSpecFilterV2; +}; diff --git a/src/html/UrlBuilder.js b/src/html/UrlBuilder.js deleted file mode 100644 index 68ae35f6..00000000 --- a/src/html/UrlBuilder.js +++ /dev/null @@ -1,41 +0,0 @@ -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/requireHtml.js b/src/html/requireHtml.js index 783e7230..c64ac71a 100644 --- a/src/html/requireHtml.js +++ b/src/html/requireHtml.js @@ -10,8 +10,9 @@ jasmineRequire.html = function(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$.HtmlReporterV2 = jasmineRequire.HtmlReporterV2(j$); j$.QueryString = jasmineRequire.QueryString(); j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); + j$.HtmlSpecFilterV2 = jasmineRequire.HtmlSpecFilterV2(); };