diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index c1637e7b..a8963132 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.TabBar = jasmineRequire.TabBar(j$); j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); j$.HtmlReporterV2Urls = jasmineRequire.HtmlReporterV2Urls(j$); j$.HtmlReporterV2 = jasmineRequire.HtmlReporterV2(j$); @@ -187,16 +188,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 +471,7 @@ jasmineRequire.AlertsView = function(j$) { ); } + // TODO: remove this once HtmlReporterV2 doesn't use it addFailureToggle(onClickFailures, onClickSpecList) { const failuresLink = createDom( 'a', @@ -456,14 +494,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 +990,9 @@ jasmineRequire.HtmlReporterV2 = function(j$) { const { createDom, noExpectations } = j$.private.htmlReporterUtils; + const specListTabId = 'jasmine-specListTab'; + const failuresTabId = 'jasmine-failuresTab'; + /** * @class HtmlReporterV2 * @classdesc Displays results and allows re-running individual specs and suites. @@ -974,6 +1021,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) { // Sub-views #alerts; #statusBar; + #tabBar; #progress; #banner; #failures; @@ -1011,6 +1059,22 @@ 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' } + ], + tabId => { + if (tabId === specListTabId) { + this.#setMenuModeTo('jasmine-spec-list'); + } else { + this.#setMenuModeTo('jasmine-failure-list'); + } + } + ); + this.#alerts.addBar(this.#tabBar.rootEl); + this.#progress = new ProgressView(); this.#banner = new j$.private.Banner( this.#queryString.navigateWithNewParam.bind(this.#queryString), @@ -1103,13 +1167,11 @@ jasmineRequire.HtmlReporterV2 = function(j$) { results.appendChild(summary.rootEl); 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(specListTabId); + this.#tabBar.showTab(failuresTabId); + this.#tabBar.selectTab(failuresTabId); + } else { + this.#tabBar.selectTab(specListTabId); } } @@ -1148,7 +1210,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'); } } @@ -1665,3 +1726,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..e0596114 100644 --- a/lib/jasmine-core/jasmine.css +++ b/lib/jasmine-core/jasmine.css @@ -323,4 +323,12 @@ body { } .jasmine_html-reporter .jasmine-debug-log .jasmine-debug-log-msg { white-space: pre; +} + +.jasmine-hidden { + display: none; +} + +.jasmine-tab + .jasmine-tab:before { + content: " | "; } \ No newline at end of file diff --git a/spec/html/HtmlReporterV2Spec.js b/spec/html/HtmlReporterV2Spec.js index e1a1c6f3..cafebe4a 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,193 @@ 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(2); + expect(tabs[0].textContent).toEqual('Spec List'); + expect(tabs[1].textContent).toEqual('Failures'); + checkHidden(tabs, [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]); + }); + }); + + describe('when Jasmine is done', function() { + function hasSpecOrSuiteFailureBehavior(reportEvents) { + let reporter; + + beforeEach(function() { + reporter = setup(); + reporter.initialize(); + reportEvents(reporter); + }); + + it('shows the Spec List and Failures tabs', function() { + const tabs = container.querySelectorAll('.jasmine-tab'); + checkHidden(tabs, [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('does not show any tabs', function() { + const tabs = container.querySelectorAll('.jasmine-tab'); + checkHidden(tabs, [true, true]); + }); + + 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: [{}] + }); + }); + }); + }); + }); + 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 +636,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 +666,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 +696,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 +942,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 +1256,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/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..e1d1205d 100644 --- a/src/html/HtmlReporterV2.js +++ b/src/html/HtmlReporterV2.js @@ -3,6 +3,9 @@ jasmineRequire.HtmlReporterV2 = function(j$) { const { createDom, noExpectations } = j$.private.htmlReporterUtils; + const specListTabId = 'jasmine-specListTab'; + const failuresTabId = 'jasmine-failuresTab'; + /** * @class HtmlReporterV2 * @classdesc Displays results and allows re-running individual specs and suites. @@ -31,6 +34,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) { // Sub-views #alerts; #statusBar; + #tabBar; #progress; #banner; #failures; @@ -68,6 +72,22 @@ 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' } + ], + tabId => { + if (tabId === specListTabId) { + this.#setMenuModeTo('jasmine-spec-list'); + } else { + this.#setMenuModeTo('jasmine-failure-list'); + } + } + ); + this.#alerts.addBar(this.#tabBar.rootEl); + this.#progress = new ProgressView(); this.#banner = new j$.private.Banner( this.#queryString.navigateWithNewParam.bind(this.#queryString), @@ -160,13 +180,11 @@ jasmineRequire.HtmlReporterV2 = function(j$) { results.appendChild(summary.rootEl); 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(specListTabId); + this.#tabBar.showTab(failuresTabId); + this.#tabBar.selectTab(failuresTabId); + } else { + this.#tabBar.selectTab(specListTabId); } } @@ -205,7 +223,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/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..9dc752d9 100644 --- a/src/html/_HTMLReporter.scss +++ b/src/html/_HTMLReporter.scss @@ -464,3 +464,11 @@ body { } } } + +.jasmine-hidden { + display: none; +} + +.jasmine-tab + .jasmine-tab:before { + content: ' | '; +} diff --git a/src/html/requireHtml.js b/src/html/requireHtml.js index df9d85bc..05b1800f 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.TabBar = jasmineRequire.TabBar(j$); j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); j$.HtmlReporterV2Urls = jasmineRequire.HtmlReporterV2Urls(j$); j$.HtmlReporterV2 = jasmineRequire.HtmlReporterV2(j$);