Introduce a tab bar

This will make it easier to add a third tab to HtmlReporterV2.
This commit is contained in:
Steve Gravrock
2025-10-26 12:50:42 -07:00
parent 85322d1877
commit d31d33aeb3
10 changed files with 646 additions and 89 deletions

View File

@@ -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() {

97
spec/html/TabBarSpec.js Normal file
View File

@@ -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();
});
});
});