From 8309416cb2ab2d343f2ce83a8f7323913ce60062 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Tue, 16 Sep 2025 21:03:16 -0700 Subject: [PATCH] Clicking a link in the HTML reporter does exact filtering This feature requires an update to boot1.js, as shown in this commit. Users with an older boot1.js will get the older inexact filtering. --- lib/jasmine-core/boot1.js | 4 +- lib/jasmine-core/jasmine-html.js | 96 ++++++++++++++++++++++------ spec/html/HtmlExactSpecFilterSpec.js | 49 ++++++++++++++ spec/html/HtmlReporterSpec.js | 20 ++++-- spec/html/HtmlSpecFilterSpec.js | 15 ++++- src/boot/boot1.js | 4 +- src/html/HtmlExactSpecFilter.js | 38 +++++++++++ src/html/HtmlReporter.js | 41 +++++++----- src/html/HtmlSpecFilter.js | 15 +++-- src/html/requireHtml.js | 1 + 10 files changed, 231 insertions(+), 52 deletions(-) create mode 100644 spec/html/HtmlExactSpecFilterSpec.js create mode 100644 src/html/HtmlExactSpecFilter.js 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(); };