diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js
index c1637e7b..b7029468 100644
--- a/lib/jasmine-core/jasmine-html.js
+++ b/lib/jasmine-core/jasmine-html.js
@@ -35,6 +35,8 @@ 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$);
j$.HtmlReporterV2 = jasmineRequire.HtmlReporterV2(j$);
@@ -187,16 +189,52 @@ jasmineRequire.HtmlReporter = function(j$) {
results.appendChild(summary.rootEl);
if (this.#stateBuilder.anyNonTopSuiteFailures) {
- this.#alerts.addFailureToggle(
- () => this.#setMenuModeTo('jasmine-failure-list'),
- () => this.#setMenuModeTo('jasmine-spec-list')
- );
-
+ this.#addFailureToggle();
this.#setMenuModeTo('jasmine-failure-list');
this.#failures.show();
}
}
+ #addFailureToggle() {
+ const onClickFailures = () => this.#setMenuModeTo('jasmine-failure-list');
+ const onClickSpecList = () => this.#setMenuModeTo('jasmine-spec-list');
+ 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.#alerts.addBar(
+ createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
+ [createDom('span', {}, 'Spec List | '), failuresLink]
+ )
+ );
+ this.#alerts.addBar(
+ createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
+ [specListLink, createDom('span', {}, ' | Failures ')]
+ )
+ );
+ }
+
#find(selector) {
return this.#getContainer().querySelector(
'.jasmine_html-reporter ' + selector
@@ -434,6 +472,7 @@ jasmineRequire.AlertsView = function(j$) {
);
}
+ // TODO: remove this once HtmlReporterV2 doesn't use it
addFailureToggle(onClickFailures, onClickSpecList) {
const failuresLink = createDom(
'a',
@@ -456,14 +495,20 @@ jasmineRequire.AlertsView = function(j$) {
return false;
};
- this.#createAndAdd('jasmine-menu jasmine-bar jasmine-spec-list', [
- createDom('span', {}, 'Spec List | '),
- failuresLink
- ]);
- this.#createAndAdd('jasmine-menu jasmine-bar jasmine-failure-list', [
- specListLink,
- createDom('span', {}, ' | Failures ')
- ]);
+ this.rootEl.appendChild(
+ createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
+ [createDom('span', {}, 'Spec List | '), failuresLink]
+ )
+ );
+ this.rootEl.appendChild(
+ createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
+ [specListLink, createDom('span', {}, ' | Failures ')]
+ )
+ );
}
addGlobalFailure(failure) {
@@ -946,6 +991,10 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
const { createDom, noExpectations } = j$.private.htmlReporterUtils;
+ const specListTabId = 'jasmine-specListTab';
+ const failuresTabId = 'jasmine-failuresTab';
+ const perfTabId = 'jasmine-perfTab';
+
/**
* @class HtmlReporterV2
* @classdesc Displays results and allows re-running individual specs and suites.
@@ -974,6 +1023,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
// Sub-views
#alerts;
#statusBar;
+ #tabBar;
#progress;
#banner;
#failures;
@@ -1011,6 +1061,25 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
this.#statusBar = new j$.private.OverallStatusBar(this.#urlBuilder);
this.#statusBar.showRunning();
this.#alerts.addBar(this.#statusBar.rootEl);
+
+ this.#tabBar = new j$.private.TabBar(
+ [
+ { id: specListTabId, label: 'Spec List' },
+ { id: failuresTabId, label: 'Failures' },
+ { id: perfTabId, label: 'Performance' }
+ ],
+ tabId => {
+ if (tabId === specListTabId) {
+ this.#setMenuModeTo('jasmine-spec-list');
+ } else if (tabId === failuresTabId) {
+ this.#setMenuModeTo('jasmine-failure-list');
+ } else {
+ this.#setMenuModeTo('jasmine-performance');
+ }
+ }
+ );
+ this.#alerts.addBar(this.#tabBar.rootEl);
+
this.#progress = new ProgressView();
this.#banner = new j$.private.Banner(
this.#queryString.navigateWithNewParam.bind(this.#queryString),
@@ -1101,15 +1170,17 @@ 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.#alerts.addFailureToggle(
- () => this.#setMenuModeTo('jasmine-failure-list'),
- () => this.#setMenuModeTo('jasmine-spec-list')
- );
-
- this.#setMenuModeTo('jasmine-failure-list');
- this.#failures.show();
+ this.#tabBar.showTab(failuresTabId);
+ this.#tabBar.selectTab(failuresTabId);
+ } else {
+ this.#tabBar.selectTab(specListTabId);
}
}
@@ -1148,7 +1219,6 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
this.rootEl.value = this.rootEl.value + 1;
if (result.status === 'failed') {
- // TODO: also a non-color indicator
this.rootEl.classList.add('failed');
}
}
@@ -1434,6 +1504,105 @@ jasmineRequire.OverallStatusBar = function(j$) {
return OverallStatusBar;
};
+jasmineRequire.PerformanceView = function(j$) {
+ const createDom = j$.private.htmlReporterUtils.createDom;
+ const MAX_SLOW_SPECS = 20;
+
+ class PerformanceView {
+ #summary;
+ #tbody;
+
+ constructor() {
+ this.#tbody = document.createElement('tbody');
+ this.#summary = document.createElement('div');
+ this.rootEl = createDom(
+ 'div',
+ { className: 'jasmine-performance-view' },
+ createDom('h2', {}, 'Performance'),
+ this.#summary,
+ createDom('h3', {}, 'Slowest Specs'),
+ createDom(
+ 'table',
+ {},
+ createDom(
+ 'thead',
+ {},
+ createDom(
+ 'tr',
+ {},
+ createDom('th', {}, 'Duration'),
+ createDom('th', {}, 'Spec Name')
+ )
+ ),
+ this.#tbody
+ )
+ );
+ }
+
+ addResults(resultsTree) {
+ const specResults = [];
+ getSpecResults(resultsTree, specResults);
+
+ if (specResults.length === 0) {
+ return;
+ }
+
+ specResults.sort(function(a, b) {
+ if (a.duration < b.duration) {
+ return 1;
+ } else if (a.duration > b.duration) {
+ return -1;
+ } else {
+ return 0;
+ }
+ });
+
+ this.#populateSumary(specResults);
+ this.#populateTable(specResults);
+ }
+
+ #populateSumary(specResults) {
+ const total = specResults.map(r => r.duration).reduce((a, b) => a + b, 0);
+ const mean = total / specResults.length;
+ const median = specResults[Math.floor(specResults.length / 2)].duration;
+ this.#summary.appendChild(
+ document.createTextNode(`Mean spec run time: ${mean.toFixed(0)}ms`)
+ );
+ this.#summary.appendChild(document.createElement('br'));
+ this.#summary.appendChild(
+ document.createTextNode(`Median spec run time: ${median}ms`)
+ );
+ }
+
+ #populateTable(specResults) {
+ 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';
@@ -1665,3 +1834,81 @@ jasmineRequire.SymbolsView = function(j$) {
return SymbolsView;
};
+
+jasmineRequire.TabBar = function(j$) {
+ const createDom = j$.private.htmlReporterUtils.createDom;
+
+ class TabBar {
+ #tabs;
+ #onSelectTab;
+
+ // tabSpecs should be an array of {id, label}.
+ // All tabs are initially not visible and not selected.
+ constructor(tabSpecs, onSelectTab) {
+ this.#onSelectTab = onSelectTab;
+ this.#tabs = [];
+ this.#tabs = tabSpecs.map(ts => new Tab(ts, () => this.selectTab(ts.id)));
+
+ this.rootEl = createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar' },
+ this.#tabs.map(t => t.rootEl)
+ );
+ }
+
+ showTab(id) {
+ for (const tab of this.#tabs) {
+ if (tab.rootEl.id === id) {
+ tab.setVisibility(true);
+ }
+ }
+ }
+
+ selectTab(id) {
+ for (const tab of this.#tabs) {
+ tab.setSelected(tab.rootEl.id === id);
+ }
+
+ this.#onSelectTab(id);
+ }
+ }
+
+ class Tab {
+ #spec;
+ #onClick;
+
+ constructor(spec, onClick) {
+ this.#spec = spec;
+ this.#onClick = onClick;
+ this.rootEl = createDom(
+ 'span',
+ { id: spec.id, className: 'jasmine-tab jasmine-hidden' },
+ this.#createLink()
+ );
+ }
+
+ setVisibility(visible) {
+ this.rootEl.classList.toggle('jasmine-hidden', !visible);
+ }
+
+ setSelected(selected) {
+ if (selected) {
+ this.rootEl.textContent = this.#spec.label;
+ } else {
+ this.rootEl.textContent = '';
+ this.rootEl.appendChild(this.#createLink());
+ }
+ }
+
+ #createLink() {
+ const link = createDom('a', { href: '#' }, this.#spec.label);
+ link.addEventListener('click', e => {
+ e.preventDefault();
+ this.#onClick();
+ });
+ return link;
+ }
+ }
+
+ return TabBar;
+};
diff --git a/lib/jasmine-core/jasmine.css b/lib/jasmine-core/jasmine.css
index 71f6fc17..24cf47dd 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 {
@@ -323,4 +329,23 @@ body {
}
.jasmine_html-reporter .jasmine-debug-log .jasmine-debug-log-msg {
white-space: pre;
+}
+
+.jasmine-hidden {
+ display: none;
+}
+
+.jasmine-tab + .jasmine-tab:before {
+ content: " | ";
+}
+
+.jasmine-performance-view h2, .jasmine-performance-view h3 {
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+.jasmine-performance-view table {
+ border-spacing: 5px;
+}
+.jasmine-performance-view th, .jasmine-performance-view td {
+ text-align: left;
}
\ No newline at end of file
diff --git a/spec/html/HtmlReporterV2Spec.js b/spec/html/HtmlReporterV2Spec.js
index e1a1c6f3..d422e817 100644
--- a/spec/html/HtmlReporterV2Spec.js
+++ b/spec/html/HtmlReporterV2Spec.js
@@ -167,18 +167,18 @@ describe('HtmlReporterV2', function() {
'.jasmine-alert .jasmine-bar'
);
- expect(alertBars.length).toEqual(4);
- expect(alertBars[1].innerHTML).toMatch(
+ expect(alertBars.length).toEqual(5);
+ expect(alertBars[2].innerHTML).toMatch(
/spec deprecation.*\(in spec: a spec with a deprecation\)/
);
- expect(alertBars[1].getAttribute('class')).toEqual(
+ expect(alertBars[2].getAttribute('class')).toEqual(
'jasmine-bar jasmine-warning'
);
- expect(alertBars[2].innerHTML).toMatch(
+ expect(alertBars[3].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 /);
+ expect(alertBars[4].innerHTML).toMatch(/global deprecation/);
+ expect(alertBars[4].innerHTML).not.toMatch(/in /);
});
it('displays expandable stack traces', function() {
@@ -259,6 +259,214 @@ describe('HtmlReporterV2', function() {
});
});
+ describe('The tab bar', function() {
+ function checkHidden(tabs, expected) {
+ const actual = Array.from(tabs).map(t =>
+ t.classList.contains('jasmine-hidden')
+ );
+ expect(actual)
+ .withContext('tab hiddenness')
+ .toEqual(expected);
+ }
+
+ describe('while Jasmine is running', function() {
+ it('hides all tabs', function() {
+ const reporter = setup();
+ reporter.initialize();
+ reporter.jasmineStarted({ totalSpecsDefined: 0 });
+ const tabs = container.querySelectorAll('.jasmine-tab');
+ expect(tabs.length).toEqual(3);
+ expect(tabs[0].textContent).toEqual('Spec List');
+ expect(tabs[1].textContent).toEqual('Failures');
+ expect(tabs[2].textContent).toEqual('Performance');
+ checkHidden(tabs, [true, true, true]);
+
+ // Results, even failures, should not show any tabs
+ reporter.specDone({
+ id: 1,
+ description: 'a failing spec',
+ fullName: 'a failing spec',
+ status: 'failed',
+ failedExpectations: [{}],
+ passedExpectations: []
+ });
+ checkHidden(tabs, [true, true, true]);
+ });
+ });
+
+ describe('when Jasmine is done', function() {
+ function hasSpecOrSuiteFailureBehavior(reportEvents) {
+ let reporter;
+
+ beforeEach(function() {
+ reporter = setup();
+ reporter.initialize();
+ reportEvents(reporter);
+ });
+
+ it('shows all three tabs', function() {
+ const tabs = container.querySelectorAll('.jasmine-tab');
+ checkHidden(tabs, [false, false, false]);
+ });
+
+ it('selects the Failures tab', function() {
+ const reporterNode = container.querySelector(
+ '.jasmine_html-reporter'
+ );
+ expect(reporterNode).toHaveClass('jasmine-failure-list');
+ });
+
+ it('switches between failure details and the spec summary', function() {
+ const tabs = container.querySelectorAll('.jasmine-tab');
+ let specListLink = () => tabs[0].querySelector('a');
+ let failuresLink = () => tabs[1].querySelector('a');
+ const reporterNode = container.querySelector(
+ '.jasmine_html-reporter'
+ );
+ expect(specListLink().textContent).toEqual('Spec List');
+ expect(failuresLink())
+ .withContext('failures link')
+ .toBeFalsy();
+
+ specListLink().click();
+ expect(reporterNode).toHaveClass('jasmine-spec-list');
+ expect(reporterNode).not.toHaveClass('jasmine-failure-list');
+ expect(specListLink())
+ .withContext('spec list link')
+ .toBeFalsy();
+ expect(failuresLink().textContent).toEqual('Failures');
+
+ failuresLink().click();
+ expect(reporterNode.getAttribute('class')).toMatch(
+ 'jasmine-failure-list'
+ );
+ expect(failuresLink())
+ .withContext('failures link')
+ .toBeFalsy();
+ expect(specListLink().textContent).toEqual('Spec List');
+ expect(reporterNode).toHaveClass('jasmine-failure-list');
+ expect(reporterNode).not.toHaveClass('jasmine-spec-list');
+ });
+ }
+
+ function hasSpecAndSuiteSuccessBehavior(reportEvents) {
+ let reporter;
+
+ beforeEach(function() {
+ reporter = setup();
+ reporter.initialize();
+ reportEvents(reporter);
+ });
+
+ it('shows the Spec List and Performance tabs', function() {
+ const tabs = container.querySelectorAll('.jasmine-tab');
+ checkHidden(tabs, [false, true, false]);
+ });
+
+ it('shows the spec list view', function() {
+ const reporterNode = container.querySelector(
+ '.jasmine_html-reporter'
+ );
+ expect(reporterNode).toHaveClass('jasmine-spec-list');
+ expect(reporterNode).not.toHaveClass('jasmine-failure-list');
+ });
+ }
+
+ describe('with spec failures', function() {
+ hasSpecOrSuiteFailureBehavior(function(reporter) {
+ reporter.jasmineStarted({ totalSpecsDefined: 0 });
+ reporter.specDone({
+ id: 1,
+ description: 'a failing spec',
+ fullName: 'a failing spec',
+ status: 'failed',
+ failedExpectations: [{}],
+ passedExpectations: []
+ });
+ reporter.specDone({
+ id: 2,
+ description: 'a passing spec',
+ fullName: 'a passing spec',
+ status: 'passed',
+ failedExpectations: [],
+ passedExpectations: []
+ });
+ reporter.jasmineDone({});
+ });
+ });
+
+ describe('with suite failures', function() {
+ hasSpecOrSuiteFailureBehavior(function(reporter) {
+ reporter.jasmineStarted({ totalSpecsDefined: 0 });
+ reporter.specDone({
+ id: 1,
+ description: 'a failing spec',
+ fullName: 'a failing spec',
+ status: 'failed',
+ failedExpectations: [{}],
+ passedExpectations: []
+ });
+ reporter.specDone({
+ id: 2,
+ description: 'a passing spec',
+ fullName: 'a passing spec',
+ status: 'passed',
+ failedExpectations: [],
+ passedExpectations: []
+ });
+ reporter.jasmineDone({});
+ });
+ });
+
+ describe('without any failures', function() {
+ hasSpecAndSuiteSuccessBehavior(function(reporter) {
+ reporter.jasmineStarted({ totalSpecsDefined: 0 });
+ reporter.specDone({
+ id: 1,
+ description: 'a passing spec',
+ fullName: 'a passing spec',
+ status: 'passed',
+ failedExpectations: [],
+ passedExpectations: []
+ });
+ reporter.suiteDone({ id: 1 });
+ reporter.jasmineDone({});
+ });
+ });
+
+ describe('with only top suite failures', function() {
+ // Top suite failures are displayed in their own alert bars, so they
+ // don't cause the failures tab to be shown.
+ hasSpecAndSuiteSuccessBehavior(function(reporter) {
+ reporter.jasmineStarted({ totalSpecsDefined: 0 });
+ reporter.jasmineDone({
+ failedExpectations: [{}]
+ });
+ });
+ });
+
+ 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 | ');
+ });
+ });
+ });
+
describe('when Jasmine is done', function() {
it('adds a warning to the link title of specs that have no expectations', function() {
const reporter = setup();
@@ -449,21 +657,18 @@ describe('HtmlReporterV2', function() {
]
});
- const alertBars = container.querySelectorAll(
- '.jasmine-alert .jasmine-bar'
+ const errorBars = container.querySelectorAll(
+ '.jasmine-alert .jasmine-bar.jasmine-errored'
);
- expect(alertBars.length).toEqual(3);
- expect(alertBars[1].getAttribute('class')).toEqual(
- 'jasmine-bar jasmine-errored'
- );
- expect(alertBars[1].innerHTML).toMatch(
+ expect(errorBars.length).toEqual(2);
+ expect(errorBars[0].innerHTML).toMatch(
/AfterAll Global After All Failure/
);
- expect(alertBars[2].innerHTML).toMatch(
+ expect(errorBars[1].innerHTML).toMatch(
/Error during loading: Your JS is borken/
);
- expect(alertBars[2].innerHTML).not.toMatch(/line/);
+ expect(errorBars[1].innerHTML).not.toMatch(/line/);
});
it('does not display the "AfterAll" prefix for other error types', function() {
@@ -482,16 +687,16 @@ describe('HtmlReporterV2', function() {
]
});
- const alertBars = container.querySelectorAll(
- '.jasmine-alert .jasmine-bar'
+ const errorBars = container.querySelectorAll(
+ '.jasmine-alert .jasmine-bar.jasmine-errored'
);
- 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');
+ expect(errorBars.length).toEqual(3);
+ expect(errorBars[0].textContent).toContain('load error');
+ expect(errorBars[1].textContent).toContain('lateExpectation error');
+ expect(errorBars[2].textContent).toContain('lateError error');
- for (let bar of alertBars) {
+ for (let bar of errorBars) {
expect(bar.textContent).not.toContain('AfterAll');
}
});
@@ -512,12 +717,12 @@ describe('HtmlReporterV2', function() {
]
});
- const alertBars = container.querySelectorAll(
- '.jasmine-alert .jasmine-bar'
+ const alertBar = container.querySelector(
+ '.jasmine-alert .jasmine-bar.jasmine-errored'
);
- expect(alertBars.length).toEqual(2);
- expect(alertBars[1].innerHTML).toMatch(
+ expect(alertBar).toBeTruthy();
+ expect(alertBar.innerHTML).toMatch(
/Error during loading: Your JS is borken in some\/file.js line 42/
);
});
@@ -758,12 +963,12 @@ describe('HtmlReporterV2', function() {
});
it('reports the specs counts', function() {
- const alertBars = container.querySelectorAll(
- '.jasmine-alert .jasmine-bar'
+ const resultBar = container.querySelector(
+ '.jasmine-alert .jasmine-bar.jasmine-overall-result'
);
- expect(alertBars.length).toEqual(1);
- expect(alertBars[0].innerHTML).toMatch(/2 specs, 0 failures/);
+ expect(resultBar).toBeTruthy();
+ expect(resultBar.innerHTML).toMatch(/2 specs, 0 failures/);
});
it('reports no failure details', function() {
@@ -1072,23 +1277,6 @@ describe('HtmlReporterV2', function() {
)}`
);
});
-
- 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() {
diff --git a/spec/html/PerformanceViewSpec.js b/spec/html/PerformanceViewSpec.js
new file mode 100644
index 00000000..63e5f873
--- /dev/null
+++ b/spec/html/PerformanceViewSpec.js
@@ -0,0 +1,107 @@
+'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('shows mean and median run times for an odd number of specs', function() {
+ const stateBuilder = new privateUnderTest.ResultsStateBuilder();
+ const subject = new privateUnderTest.PerformanceView();
+
+ stateBuilder.specDone({ duration: 1 });
+ stateBuilder.specDone({ duration: 2 });
+ stateBuilder.specDone({ duration: 5 });
+ subject.addResults(stateBuilder.topResults);
+
+ expect(subject.rootEl.textContent).toContain('Mean spec run time: 3ms');
+ expect(subject.rootEl.textContent).toContain('Median spec run time: 2ms');
+ });
+
+ it('shows mean and median run times for an even number of specs', function() {
+ const stateBuilder = new privateUnderTest.ResultsStateBuilder();
+ const subject = new privateUnderTest.PerformanceView();
+
+ stateBuilder.specDone({ duration: 1 });
+ stateBuilder.specDone({ duration: 3 });
+ stateBuilder.specDone({ duration: 10 });
+ stateBuilder.specDone({ duration: 2 });
+ subject.addResults(stateBuilder.topResults);
+
+ expect(subject.rootEl.textContent).toContain('Mean spec run time: 4ms');
+ expect(subject.rootEl.textContent).toContain('Median spec run time: 2ms');
+ });
+
+ it('copes with 0 specs', function() {
+ const stateBuilder = new privateUnderTest.ResultsStateBuilder();
+ const subject = new privateUnderTest.PerformanceView();
+
+ expect(function() {
+ subject.addResults(stateBuilder.topResults);
+ }).not.toThrow();
+ });
+
+ 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']);
+ expect(subject.rootEl.textContent).toContain('Mean spec run time: 3ms');
+ expect(subject.rootEl.textContent).toContain('Median spec run time: 2ms');
+ });
+});
diff --git a/spec/html/TabBarSpec.js b/spec/html/TabBarSpec.js
new file mode 100644
index 00000000..b7849d5d
--- /dev/null
+++ b/spec/html/TabBarSpec.js
@@ -0,0 +1,97 @@
+describe('TabBar', function() {
+ it('initially renders but hides the tabs', function() {
+ const subject = new privateUnderTest.TabBar([
+ { id: 'tab1', label: 'tab 1' }
+ ]);
+ const tabs = subject.rootEl.querySelectorAll('.jasmine-tab');
+ expect(tabs.length).toEqual(1);
+ expect(tabs[0].id).toEqual('tab1');
+ expect(tabs[0]).toHaveClass('jasmine-hidden');
+ const link = tabs[0].querySelector('a');
+ expect(link).toBeTruthy();
+ expect(link.textContent).toEqual('tab 1');
+ });
+
+ it('does not initially call the onSelect callback', function() {
+ const onSelect = jasmine.createSpy('onSelect');
+ new privateUnderTest.TabBar([{ id: 'tab1', label: '' }], onSelect);
+ expect(onSelect).not.toHaveBeenCalled();
+ });
+
+ describe('#showTab', function() {
+ it('shows the specified tab', function() {
+ const subject = new privateUnderTest.TabBar([
+ { id: 'tab1' },
+ { id: 'tab2' }
+ ]);
+
+ subject.showTab('tab2');
+
+ const tabs = subject.rootEl.querySelectorAll('.jasmine-tab');
+ expect(tabs[0]).toHaveClass('jasmine-hidden');
+ expect(tabs[1]).not.toHaveClass('jasmine-hidden');
+ });
+
+ it('does not hide previously shown tabs', function() {
+ const subject = new privateUnderTest.TabBar([
+ { id: 'tab1' },
+ { id: 'tab2' }
+ ]);
+
+ subject.showTab('tab1');
+ subject.showTab('tab2');
+
+ const tabs = subject.rootEl.querySelectorAll('.jasmine-tab');
+ expect(tabs[0]).not.toHaveClass('jasmine-hidden');
+ });
+ });
+
+ describe("When a tab's link is clicked", function() {
+ it("calls the onSelect callback with the tab's id", function() {
+ const onSelect = jasmine.createSpy('onSelect');
+ const subject = new privateUnderTest.TabBar(
+ [{ id: 'tab1', label: '' }],
+ onSelect
+ );
+
+ subject.rootEl.querySelector('.jasmine-tab a').click();
+
+ expect(onSelect).toHaveBeenCalledWith('tab1');
+ });
+
+ it('shows links on all non-selected tabs only', function() {
+ const subject = new privateUnderTest.TabBar(
+ [
+ { id: 'tab1', label: 'tab 1' },
+ { id: 'tab2', label: 'tab 2' },
+ { id: 'tab3', label: 'tab 3' }
+ ],
+ () => {}
+ );
+
+ subject.rootEl.querySelectorAll('.jasmine-tab a')[1].click();
+ let tabs = subject.rootEl.querySelectorAll('.jasmine-tab');
+ expect(tabs[0].querySelector('a'))
+ .withContext('tab 1')
+ .toBeTruthy();
+ expect(tabs[1].querySelector('a'))
+ .withContext('tab 1')
+ .toBeFalsy();
+ expect(tabs[2].querySelector('a'))
+ .withContext('tab 1')
+ .toBeTruthy();
+
+ subject.rootEl.querySelectorAll('.jasmine-tab a')[0].click();
+ tabs = subject.rootEl.querySelectorAll('.jasmine-tab');
+ expect(tabs[0].querySelector('a'))
+ .withContext('tab 1')
+ .toBeFalsy();
+ expect(tabs[1].querySelector('a'))
+ .withContext('tab 1')
+ .toBeTruthy();
+ expect(tabs[2].querySelector('a'))
+ .withContext('tab 1')
+ .toBeTruthy();
+ });
+ });
+});
diff --git a/src/html/AlertsView.js b/src/html/AlertsView.js
index 321da750..1f41ce52 100644
--- a/src/html/AlertsView.js
+++ b/src/html/AlertsView.js
@@ -24,6 +24,7 @@ jasmineRequire.AlertsView = function(j$) {
);
}
+ // TODO: remove this once HtmlReporterV2 doesn't use it
addFailureToggle(onClickFailures, onClickSpecList) {
const failuresLink = createDom(
'a',
@@ -46,14 +47,20 @@ jasmineRequire.AlertsView = function(j$) {
return false;
};
- this.#createAndAdd('jasmine-menu jasmine-bar jasmine-spec-list', [
- createDom('span', {}, 'Spec List | '),
- failuresLink
- ]);
- this.#createAndAdd('jasmine-menu jasmine-bar jasmine-failure-list', [
- specListLink,
- createDom('span', {}, ' | Failures ')
- ]);
+ this.rootEl.appendChild(
+ createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
+ [createDom('span', {}, 'Spec List | '), failuresLink]
+ )
+ );
+ this.rootEl.appendChild(
+ createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
+ [specListLink, createDom('span', {}, ' | Failures ')]
+ )
+ );
}
addGlobalFailure(failure) {
diff --git a/src/html/HtmlReporter.js b/src/html/HtmlReporter.js
index a410986e..4bfc8c91 100644
--- a/src/html/HtmlReporter.js
+++ b/src/html/HtmlReporter.js
@@ -142,16 +142,52 @@ jasmineRequire.HtmlReporter = function(j$) {
results.appendChild(summary.rootEl);
if (this.#stateBuilder.anyNonTopSuiteFailures) {
- this.#alerts.addFailureToggle(
- () => this.#setMenuModeTo('jasmine-failure-list'),
- () => this.#setMenuModeTo('jasmine-spec-list')
- );
-
+ this.#addFailureToggle();
this.#setMenuModeTo('jasmine-failure-list');
this.#failures.show();
}
}
+ #addFailureToggle() {
+ const onClickFailures = () => this.#setMenuModeTo('jasmine-failure-list');
+ const onClickSpecList = () => this.#setMenuModeTo('jasmine-spec-list');
+ 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.#alerts.addBar(
+ createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
+ [createDom('span', {}, 'Spec List | '), failuresLink]
+ )
+ );
+ this.#alerts.addBar(
+ createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
+ [specListLink, createDom('span', {}, ' | Failures ')]
+ )
+ );
+ }
+
#find(selector) {
return this.#getContainer().querySelector(
'.jasmine_html-reporter ' + selector
diff --git a/src/html/HtmlReporterV2.js b/src/html/HtmlReporterV2.js
index 6a114105..cb703beb 100644
--- a/src/html/HtmlReporterV2.js
+++ b/src/html/HtmlReporterV2.js
@@ -3,6 +3,10 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
const { createDom, noExpectations } = j$.private.htmlReporterUtils;
+ const specListTabId = 'jasmine-specListTab';
+ const failuresTabId = 'jasmine-failuresTab';
+ const perfTabId = 'jasmine-perfTab';
+
/**
* @class HtmlReporterV2
* @classdesc Displays results and allows re-running individual specs and suites.
@@ -31,6 +35,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
// Sub-views
#alerts;
#statusBar;
+ #tabBar;
#progress;
#banner;
#failures;
@@ -68,6 +73,25 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
this.#statusBar = new j$.private.OverallStatusBar(this.#urlBuilder);
this.#statusBar.showRunning();
this.#alerts.addBar(this.#statusBar.rootEl);
+
+ this.#tabBar = new j$.private.TabBar(
+ [
+ { id: specListTabId, label: 'Spec List' },
+ { id: failuresTabId, label: 'Failures' },
+ { id: perfTabId, label: 'Performance' }
+ ],
+ tabId => {
+ if (tabId === specListTabId) {
+ this.#setMenuModeTo('jasmine-spec-list');
+ } else if (tabId === failuresTabId) {
+ this.#setMenuModeTo('jasmine-failure-list');
+ } else {
+ this.#setMenuModeTo('jasmine-performance');
+ }
+ }
+ );
+ this.#alerts.addBar(this.#tabBar.rootEl);
+
this.#progress = new ProgressView();
this.#banner = new j$.private.Banner(
this.#queryString.navigateWithNewParam.bind(this.#queryString),
@@ -158,15 +182,17 @@ 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.#alerts.addFailureToggle(
- () => this.#setMenuModeTo('jasmine-failure-list'),
- () => this.#setMenuModeTo('jasmine-spec-list')
- );
-
- this.#setMenuModeTo('jasmine-failure-list');
- this.#failures.show();
+ this.#tabBar.showTab(failuresTabId);
+ this.#tabBar.selectTab(failuresTabId);
+ } else {
+ this.#tabBar.selectTab(specListTabId);
}
}
@@ -205,7 +231,6 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
this.rootEl.value = this.rootEl.value + 1;
if (result.status === 'failed') {
- // TODO: also a non-color indicator
this.rootEl.classList.add('failed');
}
}
diff --git a/src/html/PerformanceView.js b/src/html/PerformanceView.js
new file mode 100644
index 00000000..5f1f2948
--- /dev/null
+++ b/src/html/PerformanceView.js
@@ -0,0 +1,98 @@
+jasmineRequire.PerformanceView = function(j$) {
+ const createDom = j$.private.htmlReporterUtils.createDom;
+ const MAX_SLOW_SPECS = 20;
+
+ class PerformanceView {
+ #summary;
+ #tbody;
+
+ constructor() {
+ this.#tbody = document.createElement('tbody');
+ this.#summary = document.createElement('div');
+ this.rootEl = createDom(
+ 'div',
+ { className: 'jasmine-performance-view' },
+ createDom('h2', {}, 'Performance'),
+ this.#summary,
+ createDom('h3', {}, 'Slowest Specs'),
+ createDom(
+ 'table',
+ {},
+ createDom(
+ 'thead',
+ {},
+ createDom(
+ 'tr',
+ {},
+ createDom('th', {}, 'Duration'),
+ createDom('th', {}, 'Spec Name')
+ )
+ ),
+ this.#tbody
+ )
+ );
+ }
+
+ addResults(resultsTree) {
+ const specResults = [];
+ getSpecResults(resultsTree, specResults);
+
+ if (specResults.length === 0) {
+ return;
+ }
+
+ specResults.sort(function(a, b) {
+ if (a.duration < b.duration) {
+ return 1;
+ } else if (a.duration > b.duration) {
+ return -1;
+ } else {
+ return 0;
+ }
+ });
+
+ this.#populateSumary(specResults);
+ this.#populateTable(specResults);
+ }
+
+ #populateSumary(specResults) {
+ const total = specResults.map(r => r.duration).reduce((a, b) => a + b, 0);
+ const mean = total / specResults.length;
+ const median = specResults[Math.floor(specResults.length / 2)].duration;
+ this.#summary.appendChild(
+ document.createTextNode(`Mean spec run time: ${mean.toFixed(0)}ms`)
+ );
+ this.#summary.appendChild(document.createElement('br'));
+ this.#summary.appendChild(
+ document.createTextNode(`Median spec run time: ${median}ms`)
+ );
+ }
+
+ #populateTable(specResults) {
+ 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/TabBar.js b/src/html/TabBar.js
new file mode 100644
index 00000000..0f4742ff
--- /dev/null
+++ b/src/html/TabBar.js
@@ -0,0 +1,77 @@
+jasmineRequire.TabBar = function(j$) {
+ const createDom = j$.private.htmlReporterUtils.createDom;
+
+ class TabBar {
+ #tabs;
+ #onSelectTab;
+
+ // tabSpecs should be an array of {id, label}.
+ // All tabs are initially not visible and not selected.
+ constructor(tabSpecs, onSelectTab) {
+ this.#onSelectTab = onSelectTab;
+ this.#tabs = [];
+ this.#tabs = tabSpecs.map(ts => new Tab(ts, () => this.selectTab(ts.id)));
+
+ this.rootEl = createDom(
+ 'span',
+ { className: 'jasmine-menu jasmine-bar' },
+ this.#tabs.map(t => t.rootEl)
+ );
+ }
+
+ showTab(id) {
+ for (const tab of this.#tabs) {
+ if (tab.rootEl.id === id) {
+ tab.setVisibility(true);
+ }
+ }
+ }
+
+ selectTab(id) {
+ for (const tab of this.#tabs) {
+ tab.setSelected(tab.rootEl.id === id);
+ }
+
+ this.#onSelectTab(id);
+ }
+ }
+
+ class Tab {
+ #spec;
+ #onClick;
+
+ constructor(spec, onClick) {
+ this.#spec = spec;
+ this.#onClick = onClick;
+ this.rootEl = createDom(
+ 'span',
+ { id: spec.id, className: 'jasmine-tab jasmine-hidden' },
+ this.#createLink()
+ );
+ }
+
+ setVisibility(visible) {
+ this.rootEl.classList.toggle('jasmine-hidden', !visible);
+ }
+
+ setSelected(selected) {
+ if (selected) {
+ this.rootEl.textContent = this.#spec.label;
+ } else {
+ this.rootEl.textContent = '';
+ this.rootEl.appendChild(this.#createLink());
+ }
+ }
+
+ #createLink() {
+ const link = createDom('a', { href: '#' }, this.#spec.label);
+ link.addEventListener('click', e => {
+ e.preventDefault();
+ this.#onClick();
+ });
+ return link;
+ }
+ }
+
+ return TabBar;
+};
diff --git a/src/html/_HTMLReporter.scss b/src/html/_HTMLReporter.scss
index 18535761..e804397f 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;
}
@@ -464,3 +474,26 @@ body {
}
}
}
+
+.jasmine-hidden {
+ display: none;
+}
+
+.jasmine-tab + .jasmine-tab:before {
+ content: ' | ';
+}
+
+.jasmine-performance-view {
+ h2, h3 {
+ margin-top: 1em;
+ margin-bottom: 1em;
+ }
+
+ table {
+ border-spacing: 5px;
+ }
+
+ th, td {
+ text-align: left;
+ }
+}
diff --git a/src/html/requireHtml.js b/src/html/requireHtml.js
index df9d85bc..17fd9420 100644
--- a/src/html/requireHtml.js
+++ b/src/html/requireHtml.js
@@ -11,6 +11,8 @@ 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$);
j$.HtmlReporterV2 = jasmineRequire.HtmlReporterV2(j$);