diff --git a/lib/jasmine-core/boot1.js b/lib/jasmine-core/boot1.js index 8c12f8c7..302fceec 100644 --- a/lib/jasmine-core/boot1.js +++ b/lib/jasmine-core/boot1.js @@ -105,14 +105,14 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. /** * Filter which specs will be run by matching the start of the full name against the `spec` query param. */ - const specFilter = new jasmine.HtmlSpecFilter({ + const specFilter = new jasmine.HtmlExactSpecFilter({ filterString: function() { return queryString.getParam('spec'); } }); config.specFilter = function(spec) { - return specFilter.matches(spec.getFullName()); + return specFilter.matches(spec); }; env.configure(config); diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index e90c362e..7de406bf 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -30,6 +30,7 @@ jasmineRequire.html = function(j$) { j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); j$.QueryString = jasmineRequire.QueryString(); j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); + j$.HtmlExactSpecFilter = jasmineRequire.HtmlExactSpecFilter(); }; jasmineRequire.HtmlReporter = function(j$) { @@ -39,11 +40,13 @@ jasmineRequire.HtmlReporter = function(j$) { this.specsExecuted = 0; this.failureCount = 0; this.pendingSpecCount = 0; + this.suitesById = []; } ResultsStateBuilder.prototype.suiteStarted = function(result) { this.currentParent.addChild(result, 'suite'); this.currentParent = this.currentParent.last(); + this.suitesById[result.id] = this.currentParent; }; ResultsStateBuilder.prototype.suiteDone = function(result) { @@ -716,21 +719,6 @@ jasmineRequire.HtmlReporter = function(j$) { return wrapper; } - function suiteHref(suite) { - const els = []; - - while (suite && suite.parent) { - els.unshift(suite.result.description); - suite = suite.parent; - } - - // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906 - return ( - (window.location.pathname || '') + - addToExistingQueryString('spec', els.join(' ')) - ); - } - function addDeprecationWarnings(result, runnableType) { if (result && result.deprecationWarnings) { for (let i = 0; i < result.deprecationWarnings.length; i++) { @@ -828,11 +816,33 @@ jasmineRequire.HtmlReporter = function(j$) { return '' + count + ' ' + word; } + function suitePath(suite) { + const els = []; + + while (suite && suite.parent) { + els.unshift(suite.result.description); + suite = suite.parent; + } + + return els; + } + + function suiteHref(suite) { + return pathHref(suitePath(suite)); + } + function specHref(result) { + const suite = stateBuilder.suitesById[result.parentSuiteId]; + const path = suitePath(suite); + path.push(result.description); + return pathHref(path); + } + + function pathHref(path) { // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906 return ( (window.location.pathname || '') + - addToExistingQueryString('spec', result.fullName) + addToExistingQueryString('spec', JSON.stringify(path)) ); } @@ -881,11 +891,18 @@ jasmineRequire.HtmlReporter = function(j$) { }; jasmineRequire.HtmlSpecFilter = function() { + // Legacy HTML spec filter, preserved for backward compatibility with + // boot files that predate HtmlExactSpecFilterV2 function HtmlSpecFilter(options) { - const filterString = - options && - options.filterString() && - options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + let filterString = (options && options.filterString()) || ''; + + if (filterString.startsWith('[')) { + // Convert an HtmlExactSpecFilterV2 string into something we can use + filterString = JSON.parse(filterString).join(' '); + } + + filterString = filterString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + const filterPattern = new RegExp(filterString); this.matches = function(specName) { @@ -974,3 +991,42 @@ jasmineRequire.QueryString = function() { return QueryString; }; + +jasmineRequire.HtmlExactSpecFilter = function() { + class HtmlExactSpecFilter { + #getFilterString; + + constructor(options) { + if (typeof options?.filterString !== 'function') { + throw new Error('options.filterString must be a function'); + } + + this.#getFilterString = options.filterString; + } + + matches(spec) { + const filterString = this.#getFilterString(); + + if (!filterString) { + return true; + } + + const filterPath = JSON.parse(this.#getFilterString()); + const specPath = spec.getPath(); + + if (filterPath.length > specPath.length) { + return false; + } + + for (let i = 0; i < filterPath.length; i++) { + if (specPath[i] !== filterPath[i]) { + return false; + } + } + + return true; + } + } + + return HtmlExactSpecFilter; +}; diff --git a/spec/html/HtmlExactSpecFilterSpec.js b/spec/html/HtmlExactSpecFilterSpec.js new file mode 100644 index 00000000..9fcef7d3 --- /dev/null +++ b/spec/html/HtmlExactSpecFilterSpec.js @@ -0,0 +1,49 @@ +describe('HtmlExactSpecFilter', function() { + it('matches everything when no string is provided', function() { + const specFilter = new jasmineUnderTest.HtmlExactSpecFilter({ + filterString() { + return ''; + } + }); + + expect(specFilter.matches({})).toBeTrue(); + }); + + it('matches a spec with the exact same path', function() { + const specFilter = new jasmineUnderTest.HtmlExactSpecFilter({ + filterString() { + return '["a","b","c"]'; + } + }); + + expect(specFilter.matches(stubSpec(['a', 'b', 'c']))).toBeTrue(); + }); + + it('matches a spec whose path has the filter path as a prefix', function() { + const specFilter = new jasmineUnderTest.HtmlExactSpecFilter({ + filterString() { + return '["a","b"]'; + } + }); + + expect(specFilter.matches(stubSpec(['a', 'b', 'c']))).toBeTrue(); + }); + + it('does not match a spec with a different path', function() { + const specFilter = new jasmineUnderTest.HtmlExactSpecFilter({ + filterString() { + return '["a","b","c"]'; + } + }); + + expect(specFilter.matches(stubSpec(['a', 'd', 'c']))).toBeFalse(); + }); + + function stubSpec(path) { + return { + getPath() { + return path; + } + }; + } +}); diff --git a/spec/html/HtmlReporterSpec.js b/spec/html/HtmlReporterSpec.js index 9e552f8d..fc06c560 100644 --- a/spec/html/HtmlReporterSpec.js +++ b/spec/html/HtmlReporterSpec.js @@ -528,6 +528,7 @@ describe('HtmlReporter', function() { let specResult = { id: 123, + parentSuiteId: 1, description: 'with a spec', fullName: 'A Suite with a spec', status: 'passed', @@ -605,7 +606,9 @@ describe('HtmlReporter', function() { const suiteDetail = outerSuite.childNodes[0]; const suiteLink = suiteDetail.childNodes[0]; expect(suiteLink.innerHTML).toEqual('A Suite'); - expect(suiteLink.getAttribute('href')).toEqual('/?foo=bar&spec=A Suite'); + expect(suiteLink.getAttribute('href')).toEqual( + '/?foo=bar&spec=["A Suite"]' + ); const specs = outerSuite.childNodes[1]; const spec = specs.childNodes[0]; @@ -615,7 +618,7 @@ describe('HtmlReporter', function() { const specLink = spec.childNodes[0]; expect(specLink.innerHTML).toEqual('with a spec'); expect(specLink.getAttribute('href')).toEqual( - '/?foo=bar&spec=A Suite with a spec' + '/?foo=bar&spec=["A Suite","with a spec"]' ); const specDuration = spec.childNodes[1]; @@ -1541,6 +1544,7 @@ describe('HtmlReporter', function() { const failingSpecResult = { id: 124, + parentSuiteId: 2, status: 'failed', description: 'a failing spec', fullName: 'a suite inner suite a failing spec', @@ -1664,16 +1668,18 @@ describe('HtmlReporter', function() { expect(links.length).toEqual(3); expect(links[0].textContent).toEqual('A suite'); - expect(links[0].getAttribute('href')).toMatch(/\?foo=bar&spec=A suite/); + expect(links[0].getAttribute('href')).toEqual( + '/?foo=bar&spec=["A suite"]' + ); expect(links[1].textContent).toEqual('inner suite'); - expect(links[1].getAttribute('href')).toMatch( - /\?foo=bar&spec=A suite inner suite/ + expect(links[1].getAttribute('href')).toEqual( + '/?foo=bar&spec=["A suite","inner suite"]' ); expect(links[2].textContent).toEqual('a failing spec'); - expect(links[2].getAttribute('href')).toMatch( - /\?foo=bar&spec=a suite inner suite a failing spec/ + expect(links[2].getAttribute('href')).toEqual( + '/?foo=bar&spec=["A suite","inner suite","a failing spec"]' ); }); diff --git a/spec/html/HtmlSpecFilterSpec.js b/spec/html/HtmlSpecFilterSpec.js index 8fa9a05d..ccfdc11d 100644 --- a/spec/html/HtmlSpecFilterSpec.js +++ b/spec/html/HtmlSpecFilterSpec.js @@ -1,4 +1,4 @@ -describe('jasmineUnderTest.HtmlSpecFilter', function() { +describe('HtmlSpecFilter', function() { it('should match when no string is provided', function() { const specFilter = new jasmineUnderTest.HtmlSpecFilter(); @@ -16,4 +16,17 @@ describe('jasmineUnderTest.HtmlSpecFilter', function() { expect(specFilter.matches('foo')).toBe(true); expect(specFilter.matches('bar')).toBe(false); }); + + it('copes with HtmlExactSpecFilterV2 filter strings', function() { + const specFilter = new jasmineUnderTest.HtmlSpecFilter({ + filterString: function() { + return '["foo","bar"]'; + } + }); + + expect(specFilter.matches('foo bar')).toBe(true); + expect(specFilter.matches('baz foo bar qux')).toBe(true); + expect(specFilter.matches('foo')).toBe(false); + expect(specFilter.matches('bar')).toBe(false); + }); }); diff --git a/src/boot/boot1.js b/src/boot/boot1.js index 55d7e800..39327ca7 100644 --- a/src/boot/boot1.js +++ b/src/boot/boot1.js @@ -81,14 +81,14 @@ /** * Filter which specs will be run by matching the start of the full name against the `spec` query param. */ - const specFilter = new jasmine.HtmlSpecFilter({ + const specFilter = new jasmine.HtmlExactSpecFilter({ filterString: function() { return queryString.getParam('spec'); } }); config.specFilter = function(spec) { - return specFilter.matches(spec.getFullName()); + return specFilter.matches(spec); }; env.configure(config); diff --git a/src/html/HtmlExactSpecFilter.js b/src/html/HtmlExactSpecFilter.js new file mode 100644 index 00000000..01c32ae0 --- /dev/null +++ b/src/html/HtmlExactSpecFilter.js @@ -0,0 +1,38 @@ +jasmineRequire.HtmlExactSpecFilter = function() { + class HtmlExactSpecFilter { + #getFilterString; + + constructor(options) { + if (typeof options?.filterString !== 'function') { + throw new Error('options.filterString must be a function'); + } + + this.#getFilterString = options.filterString; + } + + matches(spec) { + const filterString = this.#getFilterString(); + + if (!filterString) { + return true; + } + + const filterPath = JSON.parse(this.#getFilterString()); + const specPath = spec.getPath(); + + if (filterPath.length > specPath.length) { + return false; + } + + for (let i = 0; i < filterPath.length; i++) { + if (specPath[i] !== filterPath[i]) { + return false; + } + } + + return true; + } + } + + return HtmlExactSpecFilter; +}; diff --git a/src/html/HtmlReporter.js b/src/html/HtmlReporter.js index a96599cd..ff3ee80d 100644 --- a/src/html/HtmlReporter.js +++ b/src/html/HtmlReporter.js @@ -5,11 +5,13 @@ jasmineRequire.HtmlReporter = function(j$) { this.specsExecuted = 0; this.failureCount = 0; this.pendingSpecCount = 0; + this.suitesById = []; } ResultsStateBuilder.prototype.suiteStarted = function(result) { this.currentParent.addChild(result, 'suite'); this.currentParent = this.currentParent.last(); + this.suitesById[result.id] = this.currentParent; }; ResultsStateBuilder.prototype.suiteDone = function(result) { @@ -682,21 +684,6 @@ jasmineRequire.HtmlReporter = function(j$) { return wrapper; } - function suiteHref(suite) { - const els = []; - - while (suite && suite.parent) { - els.unshift(suite.result.description); - suite = suite.parent; - } - - // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906 - return ( - (window.location.pathname || '') + - addToExistingQueryString('spec', els.join(' ')) - ); - } - function addDeprecationWarnings(result, runnableType) { if (result && result.deprecationWarnings) { for (let i = 0; i < result.deprecationWarnings.length; i++) { @@ -794,11 +781,33 @@ jasmineRequire.HtmlReporter = function(j$) { return '' + count + ' ' + word; } + function suitePath(suite) { + const els = []; + + while (suite && suite.parent) { + els.unshift(suite.result.description); + suite = suite.parent; + } + + return els; + } + + function suiteHref(suite) { + return pathHref(suitePath(suite)); + } + function specHref(result) { + const suite = stateBuilder.suitesById[result.parentSuiteId]; + const path = suitePath(suite); + path.push(result.description); + return pathHref(path); + } + + function pathHref(path) { // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906 return ( (window.location.pathname || '') + - addToExistingQueryString('spec', result.fullName) + addToExistingQueryString('spec', JSON.stringify(path)) ); } diff --git a/src/html/HtmlSpecFilter.js b/src/html/HtmlSpecFilter.js index e5593aae..320d2f61 100644 --- a/src/html/HtmlSpecFilter.js +++ b/src/html/HtmlSpecFilter.js @@ -1,9 +1,16 @@ jasmineRequire.HtmlSpecFilter = function() { + // Legacy HTML spec filter, preserved for backward compatibility with + // boot files that predate HtmlExactSpecFilterV2 function HtmlSpecFilter(options) { - const filterString = - options && - options.filterString() && - options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + let filterString = (options && options.filterString()) || ''; + + if (filterString.startsWith('[')) { + // Convert an HtmlExactSpecFilterV2 string into something we can use + filterString = JSON.parse(filterString).join(' '); + } + + filterString = filterString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + const filterPattern = new RegExp(filterString); this.matches = function(specName) { diff --git a/src/html/requireHtml.js b/src/html/requireHtml.js index 9df50b08..522db07f 100644 --- a/src/html/requireHtml.js +++ b/src/html/requireHtml.js @@ -6,4 +6,5 @@ jasmineRequire.html = function(j$) { j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); j$.QueryString = jasmineRequire.QueryString(); j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); + j$.HtmlExactSpecFilter = jasmineRequire.HtmlExactSpecFilter(); };