diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index a8963132..104e3d0f 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -35,6 +35,7 @@ jasmineRequire.html = function(j$) { j$.private.SymbolsView = jasmineRequire.SymbolsView(j$); j$.private.SummaryTreeView = jasmineRequire.SummaryTreeView(j$); j$.private.FailuresView = jasmineRequire.FailuresView(j$); + j$.private.PerformanceView = jasmineRequire.PerformanceView(j$); j$.private.TabBar = jasmineRequire.TabBar(j$); j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); j$.HtmlReporterV2Urls = jasmineRequire.HtmlReporterV2Urls(j$); @@ -992,6 +993,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) { const specListTabId = 'jasmine-specListTab'; const failuresTabId = 'jasmine-failuresTab'; + const perfTabId = 'jasmine-perfTab'; /** * @class HtmlReporterV2 @@ -1063,13 +1065,16 @@ jasmineRequire.HtmlReporterV2 = function(j$) { this.#tabBar = new j$.private.TabBar( [ { id: specListTabId, label: 'Spec List' }, - { id: failuresTabId, label: 'Failures' } + { id: failuresTabId, label: 'Failures' }, + { id: perfTabId, label: 'Performance' } ], tabId => { if (tabId === specListTabId) { this.#setMenuModeTo('jasmine-spec-list'); - } else { + } else if (tabId === failuresTabId) { this.#setMenuModeTo('jasmine-failure-list'); + } else { + this.#setMenuModeTo('jasmine-performance'); } } ); @@ -1165,9 +1170,13 @@ jasmineRequire.HtmlReporterV2 = function(j$) { ); summary.addResults(this.#stateBuilder.topResults); results.appendChild(summary.rootEl); + const perf = new j$.private.PerformanceView(); + perf.addResults(this.#stateBuilder.topResults); + results.appendChild(perf.rootEl); + this.#tabBar.showTab(specListTabId); + this.#tabBar.showTab(perfTabId); if (this.#stateBuilder.anyNonTopSuiteFailures) { - this.#tabBar.showTab(specListTabId); this.#tabBar.showTab(failuresTabId); this.#tabBar.selectTab(failuresTabId); } else { @@ -1495,6 +1504,79 @@ jasmineRequire.OverallStatusBar = function(j$) { return OverallStatusBar; }; +jasmineRequire.PerformanceView = function(j$) { + const createDom = j$.private.htmlReporterUtils.createDom; + const MAX_SLOW_SPECS = 20; + + class PerformanceView { + #tbody; + + constructor() { + this.#tbody = document.createElement('tbody'); + this.rootEl = createDom( + 'div', + { className: 'jasmine-performance-view' }, + createDom('h2', {}, 'Performance'), + createDom('h3', {}, 'Slowest Specs'), + createDom( + 'table', + {}, + createDom( + 'thead', + {}, + createDom( + 'tr', + {}, + createDom('th', {}, 'Duration'), + createDom('th', {}, 'Spec Name') + ) + ), + this.#tbody + ) + ); + } + + addResults(resultsTree) { + let specResults = []; + getSpecResults(resultsTree, specResults); + + specResults.sort(function(a, b) { + if (a.duration < b.duration) { + return 1; + } else if (a.duration > b.duration) { + return -1; + } else { + return 0; + } + }); + specResults = specResults.slice(0, MAX_SLOW_SPECS); + + for (const r of specResults) { + this.#tbody.appendChild( + createDom( + 'tr', + {}, + createDom('td', {}, `${r.duration}ms`), + createDom('td', {}, r.fullName) + ) + ); + } + } + } + + function getSpecResults(resultsTree, dest) { + for (const node of resultsTree.children) { + if (node.type === 'suite') { + getSpecResults(node, dest); + } else if (node.result.status !== 'excluded') { + dest.push(node.result); + } + } + } + + return PerformanceView; +}; + jasmineRequire.ResultsStateBuilder = function(j$) { 'use strict'; diff --git a/lib/jasmine-core/jasmine.css b/lib/jasmine-core/jasmine.css index e0596114..167639d0 100644 --- a/lib/jasmine-core/jasmine.css +++ b/lib/jasmine-core/jasmine.css @@ -198,11 +198,17 @@ body { color: white; } .jasmine_html-reporter.jasmine-spec-list .jasmine-bar.jasmine-menu.jasmine-failure-list, -.jasmine_html-reporter.jasmine-spec-list .jasmine-results .jasmine-failures { +.jasmine_html-reporter.jasmine-spec-list .jasmine-results .jasmine-failures, +.jasmine_html-reporter.jasmine-spec-list .jasmine-performance-view { display: none; } .jasmine_html-reporter.jasmine-failure-list .jasmine-bar.jasmine-menu.jasmine-spec-list, -.jasmine_html-reporter.jasmine-failure-list .jasmine-summary { +.jasmine_html-reporter.jasmine-failure-list .jasmine-summary, +.jasmine_html-reporter.jasmine-failure-list .jasmine-performance-view { + display: none; +} +.jasmine_html-reporter.jasmine-performance .jasmine-results .jasmine-failures, +.jasmine_html-reporter.jasmine-performance .jasmine-summary { display: none; } .jasmine_html-reporter .jasmine-results { diff --git a/spec/html/HtmlReporterV2Spec.js b/spec/html/HtmlReporterV2Spec.js index cafebe4a..d422e817 100644 --- a/spec/html/HtmlReporterV2Spec.js +++ b/spec/html/HtmlReporterV2Spec.js @@ -275,10 +275,11 @@ describe('HtmlReporterV2', function() { reporter.initialize(); reporter.jasmineStarted({ totalSpecsDefined: 0 }); const tabs = container.querySelectorAll('.jasmine-tab'); - expect(tabs.length).toEqual(2); + expect(tabs.length).toEqual(3); expect(tabs[0].textContent).toEqual('Spec List'); expect(tabs[1].textContent).toEqual('Failures'); - checkHidden(tabs, [true, true]); + expect(tabs[2].textContent).toEqual('Performance'); + checkHidden(tabs, [true, true, true]); // Results, even failures, should not show any tabs reporter.specDone({ @@ -289,7 +290,7 @@ describe('HtmlReporterV2', function() { failedExpectations: [{}], passedExpectations: [] }); - checkHidden(tabs, [true, true]); + checkHidden(tabs, [true, true, true]); }); }); @@ -303,9 +304,9 @@ describe('HtmlReporterV2', function() { reportEvents(reporter); }); - it('shows the Spec List and Failures tabs', function() { + it('shows all three tabs', function() { const tabs = container.querySelectorAll('.jasmine-tab'); - checkHidden(tabs, [false, false]); + checkHidden(tabs, [false, false, false]); }); it('selects the Failures tab', function() { @@ -357,9 +358,9 @@ describe('HtmlReporterV2', function() { reportEvents(reporter); }); - it('does not show any tabs', function() { + it('shows the Spec List and Performance tabs', function() { const tabs = container.querySelectorAll('.jasmine-tab'); - checkHidden(tabs, [true, true]); + checkHidden(tabs, [false, true, false]); }); it('shows the spec list view', function() { @@ -443,6 +444,26 @@ describe('HtmlReporterV2', function() { }); }); }); + + it('shows the slow spec view when the Performance tab is clicked', function() { + const reporter = setup(); + reporter.initialize(); + reporter.jasmineStarted({ totalSpecsDefined: 0 }); + reporter.specDone({ + duration: 1.2, + failedExpectations: [], + passedExpectations: [] + }); + reporter.jasmineDone({}); + const tabs = container.querySelectorAll('.jasmine-tab'); + let perfLink = tabs[2].querySelector('a'); + const reporterNode = container.querySelector('.jasmine_html-reporter'); + expect(perfLink.textContent).toEqual('Performance'); + perfLink.click(); + expect(reporterNode).toHaveClass('jasmine-performance'); + expect(reporterNode.innerHTML).toContain('

Performance

'); + expect(reporterNode.innerHTML).toContain('1.2ms'); + }); }); }); diff --git a/spec/html/PerformanceViewSpec.js b/spec/html/PerformanceViewSpec.js new file mode 100644 index 00000000..3da3e359 --- /dev/null +++ b/spec/html/PerformanceViewSpec.js @@ -0,0 +1,69 @@ +'use strict'; + +describe('PerformanceView', function() { + it('shows specs ordered by execution time', function() { + const stateBuilder = new privateUnderTest.ResultsStateBuilder(); + stateBuilder.suiteStarted({}); + stateBuilder.specDone({ + fullName: 'spec A', + duration: 2 + }); + stateBuilder.suiteDone({}); + stateBuilder.specDone({ + fullName: 'spec B', + duration: 1 + }); + stateBuilder.specDone({ + fullName: 'spec C', + duration: 3 + }); + const subject = new privateUnderTest.PerformanceView(); + subject.addResults(stateBuilder.topResults); + + const rows = Array.from(subject.rootEl.querySelectorAll('tbody tr')); + const durations = rows.map(r => r.querySelectorAll('td')[0].textContent); + const names = rows.map(r => r.querySelectorAll('td')[1].textContent); + expect(names).toEqual(['spec C', 'spec A', 'spec B']); + expect(durations).toEqual(['3ms', '2ms', '1ms']); + }); + + it('shows at most 20 specs', function() { + const stateBuilder = new privateUnderTest.ResultsStateBuilder(); + const subject = new privateUnderTest.PerformanceView(); + + for (let i = 0; i < 21; i++) { + stateBuilder.specDone({ + fullName: `spec ${i}`, + duration: i + }); + } + + subject.addResults(stateBuilder.topResults); + + expect(subject.rootEl.querySelectorAll('tbody tr').length).toEqual(20); + expect(subject.textContent).not.toContain('spec 0'); + }); + + it('filters out excluded specs', function() { + const stateBuilder = new privateUnderTest.ResultsStateBuilder(); + stateBuilder.specDone({ + fullName: 'spec A', + duration: 2 + }); + stateBuilder.specDone({ + fullName: 'spec B', + duration: 1, + status: 'excluded' + }); + stateBuilder.specDone({ + fullName: 'spec C', + duration: 3 + }); + const subject = new privateUnderTest.PerformanceView(); + subject.addResults(stateBuilder.topResults); + + const rows = Array.from(subject.rootEl.querySelectorAll('tbody tr')); + const names = rows.map(r => r.querySelectorAll('td')[1].textContent); + expect(names).toEqual(['spec C', 'spec A']); + }); +}); diff --git a/src/html/HtmlReporterV2.js b/src/html/HtmlReporterV2.js index e1d1205d..cb703beb 100644 --- a/src/html/HtmlReporterV2.js +++ b/src/html/HtmlReporterV2.js @@ -5,6 +5,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) { const specListTabId = 'jasmine-specListTab'; const failuresTabId = 'jasmine-failuresTab'; + const perfTabId = 'jasmine-perfTab'; /** * @class HtmlReporterV2 @@ -76,13 +77,16 @@ jasmineRequire.HtmlReporterV2 = function(j$) { this.#tabBar = new j$.private.TabBar( [ { id: specListTabId, label: 'Spec List' }, - { id: failuresTabId, label: 'Failures' } + { id: failuresTabId, label: 'Failures' }, + { id: perfTabId, label: 'Performance' } ], tabId => { if (tabId === specListTabId) { this.#setMenuModeTo('jasmine-spec-list'); - } else { + } else if (tabId === failuresTabId) { this.#setMenuModeTo('jasmine-failure-list'); + } else { + this.#setMenuModeTo('jasmine-performance'); } } ); @@ -178,9 +182,13 @@ jasmineRequire.HtmlReporterV2 = function(j$) { ); summary.addResults(this.#stateBuilder.topResults); results.appendChild(summary.rootEl); + const perf = new j$.private.PerformanceView(); + perf.addResults(this.#stateBuilder.topResults); + results.appendChild(perf.rootEl); + this.#tabBar.showTab(specListTabId); + this.#tabBar.showTab(perfTabId); if (this.#stateBuilder.anyNonTopSuiteFailures) { - this.#tabBar.showTab(specListTabId); this.#tabBar.showTab(failuresTabId); this.#tabBar.selectTab(failuresTabId); } else { diff --git a/src/html/PerformanceView.js b/src/html/PerformanceView.js new file mode 100644 index 00000000..fddc5eff --- /dev/null +++ b/src/html/PerformanceView.js @@ -0,0 +1,72 @@ +jasmineRequire.PerformanceView = function(j$) { + const createDom = j$.private.htmlReporterUtils.createDom; + const MAX_SLOW_SPECS = 20; + + class PerformanceView { + #tbody; + + constructor() { + this.#tbody = document.createElement('tbody'); + this.rootEl = createDom( + 'div', + { className: 'jasmine-performance-view' }, + createDom('h2', {}, 'Performance'), + createDom('h3', {}, 'Slowest Specs'), + createDom( + 'table', + {}, + createDom( + 'thead', + {}, + createDom( + 'tr', + {}, + createDom('th', {}, 'Duration'), + createDom('th', {}, 'Spec Name') + ) + ), + this.#tbody + ) + ); + } + + addResults(resultsTree) { + let specResults = []; + getSpecResults(resultsTree, specResults); + + specResults.sort(function(a, b) { + if (a.duration < b.duration) { + return 1; + } else if (a.duration > b.duration) { + return -1; + } else { + return 0; + } + }); + specResults = specResults.slice(0, MAX_SLOW_SPECS); + + for (const r of specResults) { + this.#tbody.appendChild( + createDom( + 'tr', + {}, + createDom('td', {}, `${r.duration}ms`), + createDom('td', {}, r.fullName) + ) + ); + } + } + } + + function getSpecResults(resultsTree, dest) { + for (const node of resultsTree.children) { + if (node.type === 'suite') { + getSpecResults(node, dest); + } else if (node.result.status !== 'excluded') { + dest.push(node.result); + } + } + } + + return PerformanceView; +}; diff --git a/src/html/_HTMLReporter.scss b/src/html/_HTMLReporter.scss index 9dc752d9..d6a285fb 100644 --- a/src/html/_HTMLReporter.scss +++ b/src/html/_HTMLReporter.scss @@ -280,15 +280,25 @@ body { } // simplify toggle control between the two menu bars + // TODO: clean this up once HtmlReporter is removed &.jasmine-spec-list { .jasmine-bar.jasmine-menu.jasmine-failure-list, - .jasmine-results .jasmine-failures { + .jasmine-results .jasmine-failures, + .jasmine-performance-view { display: none; } } &.jasmine-failure-list { .jasmine-bar.jasmine-menu.jasmine-spec-list, + .jasmine-summary, + .jasmine-performance-view { + display: none; + } + } + + &.jasmine-performance { + .jasmine-results .jasmine-failures, .jasmine-summary { display: none; } diff --git a/src/html/requireHtml.js b/src/html/requireHtml.js index 05b1800f..17fd9420 100644 --- a/src/html/requireHtml.js +++ b/src/html/requireHtml.js @@ -11,6 +11,7 @@ jasmineRequire.html = function(j$) { j$.private.SymbolsView = jasmineRequire.SymbolsView(j$); j$.private.SummaryTreeView = jasmineRequire.SummaryTreeView(j$); j$.private.FailuresView = jasmineRequire.FailuresView(j$); + j$.private.PerformanceView = jasmineRequire.PerformanceView(j$); j$.private.TabBar = jasmineRequire.TabBar(j$); j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); j$.HtmlReporterV2Urls = jasmineRequire.HtmlReporterV2Urls(j$);