diff --git a/lib/jasmine-core/jasmine-html.js b/lib/jasmine-core/jasmine-html.js index 5c341d38..22c5d65a 100644 --- a/lib/jasmine-core/jasmine-html.js +++ b/lib/jasmine-core/jasmine-html.js @@ -1010,7 +1010,10 @@ jasmineRequire.HtmlReporterV2 = function(j$) { return window.location; } }); - this.#urlBuilder = new UrlBuilder(this.#queryString); + this.#urlBuilder = new UrlBuilder({ + queryString: this.#queryString, + getSuiteById: id => this.#stateBuilder.suitesById[id] + }); this.#filterSpecs = options.urls.filteringSpecs(); } @@ -1147,34 +1150,48 @@ jasmineRequire.HtmlReporterV2 = function(j$) { class UrlBuilder { #queryString; + #getSuiteById; - constructor(queryString) { - this.#queryString = queryString; + constructor(options) { + this.#queryString = options.queryString; + this.#getSuiteById = options.getSuiteById; } suiteHref(suite) { - const els = []; - - while (suite && suite.parent) { - els.unshift(suite.result.description); - suite = suite.parent; - } - - return this.#addToExistingQueryString('spec', els.join(' ')); + const path = this.#suitePath(suite); + return this.#specPathHref(path); } - specHref(result) { - return this.#addToExistingQueryString('spec', result.fullName); + specHref(specResult) { + const suite = this.#getSuiteById(specResult.parentSuiteId); + const path = this.#suitePath(suite); + path.push(specResult.description); + return this.#specPathHref(path); } runAllHref() { - return this.#addToExistingQueryString('spec', ''); + return this.#addToExistingQueryString('path', ''); } seedHref(seed) { return this.#addToExistingQueryString('seed', seed); } + #suitePath(suite) { + const path = []; + + while (suite && suite.parent) { + path.unshift(suite.result.description); + suite = suite.parent; + } + + return path; + } + + #specPathHref(specPath) { + return this.#addToExistingQueryString('path', JSON.stringify(specPath)); + } + #addToExistingQueryString(k, v) { // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906 return ( @@ -1190,6 +1207,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) { jasmineRequire.HtmlReporterV2Urls = function(j$) { 'use strict'; + // TODO unify with V2 UrlBuilder? // TODO jsdoc class HtmlReporterV2Urls { constructor(options = {}) { @@ -1227,19 +1245,19 @@ jasmineRequire.HtmlReporterV2Urls = function(j$) { const specFilter = new j$.private.HtmlSpecFilterV2({ filterString: () => { - return this.queryString.getParam('spec'); + return this.queryString.getParam('path'); } }); config.specFilter = function(spec) { - return specFilter.matches(spec.getFullName()); + return specFilter.matches(spec); }; return config; } filteringSpecs() { - return !!this.queryString.getParam('spec'); + return !!this.queryString.getParam('path'); } } @@ -1247,25 +1265,42 @@ jasmineRequire.HtmlReporterV2Urls = function(j$) { }; jasmineRequire.HtmlSpecFilterV2 = function() { - 'use strict'; + class HtmlSpecFilterV2 { + #getFilterString; - function HtmlSpecFilterV2(options) { - const filterString = - options && - options.filterString() && - options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); - const filterPattern = new RegExp(filterString); + constructor(options) { + this.#getFilterString = options.filterString; + } /** * Determines whether the spec with the specified name should be executed. - * @name HtmlSpecFilter#matches + * @name HtmlSpecFilterV2#matches * @function - * @param {string} specName The full name of the spec + * @param {Spec} spec * @returns {boolean} */ - this.matches = function(specName) { - return filterPattern.test(specName); - }; + 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 HtmlSpecFilterV2; @@ -1278,6 +1313,7 @@ jasmineRequire.ResultsStateBuilder = function(j$) { constructor() { this.topResults = new j$.private.ResultsNode({}, '', null); this.currentParent = this.topResults; + this.suitesById = {}; this.totalSpecsDefined = 0; this.specsExecuted = 0; this.failureCount = 0; @@ -1288,6 +1324,7 @@ jasmineRequire.ResultsStateBuilder = function(j$) { suiteStarted(result) { this.currentParent.addChild(result, 'suite'); this.currentParent = this.currentParent.last(); + this.suitesById[result.id] = this.currentParent; } suiteDone(result) { diff --git a/spec/html/HtmlReporterV2Spec.js b/spec/html/HtmlReporterV2Spec.js index 05d1b4af..63be22da 100644 --- a/spec/html/HtmlReporterV2Spec.js +++ b/spec/html/HtmlReporterV2Spec.js @@ -349,6 +349,7 @@ describe('HtmlReporterV2', function() { let specResult = { id: 123, + parentSuiteId: 1, description: 'with a spec', fullName: 'A Suite with a spec', status: 'passed', @@ -423,7 +424,9 @@ describe('HtmlReporterV2', function() { const suiteDetail = outerSuite.childNodes[0]; const suiteLink = suiteDetail.childNodes[0]; expect(suiteLink.innerHTML).toEqual('A Suite'); - expect(suiteLink.getAttribute('href')).toEqual('/?spec=A%20Suite'); + expect(suiteLink.getAttribute('href')).toEqual( + `/?path=${encodeURIComponent('["A Suite"]')}` + ); const specs = outerSuite.childNodes[1]; const spec = specs.childNodes[0]; @@ -433,7 +436,7 @@ describe('HtmlReporterV2', function() { const specLink = spec.childNodes[0]; expect(specLink.innerHTML).toEqual('with a spec'); expect(specLink.getAttribute('href')).toEqual( - '/?spec=A%20Suite%20with%20a%20spec' + `/?path=${encodeURIComponent('["A Suite","with a spec"]')}` ); const specDuration = spec.childNodes[1]; @@ -779,7 +782,7 @@ describe('HtmlReporterV2', function() { reporter.jasmineDone({ order: { random: true } }); const skippedLink = container.querySelector('.jasmine-skipped a'); - expect(skippedLink.getAttribute('href')).toEqual('/?spec='); + expect(skippedLink.getAttribute('href')).toEqual('/?path='); }); }); @@ -985,6 +988,7 @@ describe('HtmlReporterV2', function() { const failingSpecResult = { id: 124, + parentSuiteId: 2, status: 'failed', description: 'a failing spec', fullName: 'a suite inner suite a failing spec', @@ -1106,16 +1110,20 @@ describe('HtmlReporterV2', function() { expect(links.length).toEqual(3); expect(links[0].textContent).toEqual('A suite'); - expect(links[0].getAttribute('href')).toMatch(/\?spec=A%20suite/); + expect(links[0].getAttribute('href')).toEqual( + `/?path=${encodeURIComponent('["A suite"]')}` + ); expect(links[1].textContent).toEqual('inner suite'); - expect(links[1].getAttribute('href')).toMatch( - /\?spec=A%20suite%20inner%20suite/ + expect(links[1].getAttribute('href')).toEqual( + `/?path=${encodeURIComponent('["A suite","inner suite"]')}` ); expect(links[2].textContent).toEqual('a failing spec'); - expect(links[2].getAttribute('href')).toMatch( - /\?spec=a%20suite%20inner%20suite%20a%20failing%20spec/ + expect(links[2].getAttribute('href')).toEqual( + `/?path=${encodeURIComponent( + '["A suite","inner suite","a failing spec"]' + )}` ); }); diff --git a/spec/html/HtmlReporterV2UrlsSpec.js b/spec/html/HtmlReporterV2UrlsSpec.js index fc462fa2..145cec60 100644 --- a/spec/html/HtmlReporterV2UrlsSpec.js +++ b/spec/html/HtmlReporterV2UrlsSpec.js @@ -10,17 +10,17 @@ describe('HtmlReporterV2Urls', function() { it('configures a matching spec filter', function() { const queryString = mockQueryString(); - queryString.getParam.withArgs('spec').and.returnValue('foo'); + queryString.getParam.withArgs('path').and.returnValue('["foo","bar"]'); const subject = new jasmineUnderTest.HtmlReporterV2Urls({ queryString }); const config = subject.configFromCurrentUrl(); const matching = { - getFullName() { - return 'foobar'; + getPath() { + return ['foo', 'bar']; } }; const nonMatching = { - getFullName() { - return 'baz'; + getPath() { + return ['foobar']; } }; expect(config.specFilter(matching)).toEqual(true); diff --git a/spec/html/HtmlSpecFilterV2Spec.js b/spec/html/HtmlSpecFilterV2Spec.js index bc3dd19f..16c175d7 100644 --- a/spec/html/HtmlSpecFilterV2Spec.js +++ b/spec/html/HtmlSpecFilterV2Spec.js @@ -1,19 +1,49 @@ describe('HtmlSpecFilterV2', function() { - it('should match when no string is provided', function() { - const specFilter = new privateUnderTest.HtmlSpecFilterV2(); - - expect(specFilter.matches('foo')).toBe(true); - expect(specFilter.matches('*bar')).toBe(true); - }); - - it('should only match the provided string', function() { + it('matches everything when no string is provided', function() { const specFilter = new privateUnderTest.HtmlSpecFilterV2({ - filterString: function() { - return 'foo'; + filterString() { + return ''; } }); - expect(specFilter.matches('foo')).toBe(true); - expect(specFilter.matches('bar')).toBe(false); + expect(specFilter.matches({})).toBeTrue(); }); + + it('matches a spec with the exact same path', function() { + const specFilter = new privateUnderTest.HtmlSpecFilterV2({ + 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 privateUnderTest.HtmlSpecFilterV2({ + 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 privateUnderTest.HtmlSpecFilterV2({ + filterString() { + return '["a","b","c"]'; + } + }); + + expect(specFilter.matches(stubSpec(['a', 'd', 'c']))).toBeFalse(); + }); + + function stubSpec(path) { + return { + getPath() { + return path; + } + }; + } }); diff --git a/src/html/HtmlReporterV2.js b/src/html/HtmlReporterV2.js index 111a31a0..0afc7cec 100644 --- a/src/html/HtmlReporterV2.js +++ b/src/html/HtmlReporterV2.js @@ -37,7 +37,10 @@ jasmineRequire.HtmlReporterV2 = function(j$) { return window.location; } }); - this.#urlBuilder = new UrlBuilder(this.#queryString); + this.#urlBuilder = new UrlBuilder({ + queryString: this.#queryString, + getSuiteById: id => this.#stateBuilder.suitesById[id] + }); this.#filterSpecs = options.urls.filteringSpecs(); } @@ -174,34 +177,48 @@ jasmineRequire.HtmlReporterV2 = function(j$) { class UrlBuilder { #queryString; + #getSuiteById; - constructor(queryString) { - this.#queryString = queryString; + constructor(options) { + this.#queryString = options.queryString; + this.#getSuiteById = options.getSuiteById; } suiteHref(suite) { - const els = []; - - while (suite && suite.parent) { - els.unshift(suite.result.description); - suite = suite.parent; - } - - return this.#addToExistingQueryString('spec', els.join(' ')); + const path = this.#suitePath(suite); + return this.#specPathHref(path); } - specHref(result) { - return this.#addToExistingQueryString('spec', result.fullName); + specHref(specResult) { + const suite = this.#getSuiteById(specResult.parentSuiteId); + const path = this.#suitePath(suite); + path.push(specResult.description); + return this.#specPathHref(path); } runAllHref() { - return this.#addToExistingQueryString('spec', ''); + return this.#addToExistingQueryString('path', ''); } seedHref(seed) { return this.#addToExistingQueryString('seed', seed); } + #suitePath(suite) { + const path = []; + + while (suite && suite.parent) { + path.unshift(suite.result.description); + suite = suite.parent; + } + + return path; + } + + #specPathHref(specPath) { + return this.#addToExistingQueryString('path', JSON.stringify(specPath)); + } + #addToExistingQueryString(k, v) { // include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906 return ( diff --git a/src/html/HtmlReporterV2Urls.js b/src/html/HtmlReporterV2Urls.js index 5b490d55..9d5a4961 100644 --- a/src/html/HtmlReporterV2Urls.js +++ b/src/html/HtmlReporterV2Urls.js @@ -1,6 +1,7 @@ jasmineRequire.HtmlReporterV2Urls = function(j$) { 'use strict'; + // TODO unify with V2 UrlBuilder? // TODO jsdoc class HtmlReporterV2Urls { constructor(options = {}) { @@ -38,19 +39,19 @@ jasmineRequire.HtmlReporterV2Urls = function(j$) { const specFilter = new j$.private.HtmlSpecFilterV2({ filterString: () => { - return this.queryString.getParam('spec'); + return this.queryString.getParam('path'); } }); config.specFilter = function(spec) { - return specFilter.matches(spec.getFullName()); + return specFilter.matches(spec); }; return config; } filteringSpecs() { - return !!this.queryString.getParam('spec'); + return !!this.queryString.getParam('path'); } } diff --git a/src/html/HtmlSpecFilterv2.js b/src/html/HtmlSpecFilterv2.js index 8ad66d68..a875b132 100644 --- a/src/html/HtmlSpecFilterv2.js +++ b/src/html/HtmlSpecFilterv2.js @@ -1,23 +1,40 @@ jasmineRequire.HtmlSpecFilterV2 = function() { - 'use strict'; + class HtmlSpecFilterV2 { + #getFilterString; - function HtmlSpecFilterV2(options) { - const filterString = - options && - options.filterString() && - options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); - const filterPattern = new RegExp(filterString); + constructor(options) { + this.#getFilterString = options.filterString; + } /** * Determines whether the spec with the specified name should be executed. - * @name HtmlSpecFilter#matches + * @name HtmlSpecFilterV2#matches * @function - * @param {string} specName The full name of the spec + * @param {Spec} spec * @returns {boolean} */ - this.matches = function(specName) { - return filterPattern.test(specName); - }; + 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 HtmlSpecFilterV2; diff --git a/src/html/ResultsStateBuilder.js b/src/html/ResultsStateBuilder.js index ab1390c9..a5cc0f76 100644 --- a/src/html/ResultsStateBuilder.js +++ b/src/html/ResultsStateBuilder.js @@ -5,6 +5,7 @@ jasmineRequire.ResultsStateBuilder = function(j$) { constructor() { this.topResults = new j$.private.ResultsNode({}, '', null); this.currentParent = this.topResults; + this.suitesById = {}; this.totalSpecsDefined = 0; this.specsExecuted = 0; this.failureCount = 0; @@ -15,6 +16,7 @@ jasmineRequire.ResultsStateBuilder = function(j$) { suiteStarted(result) { this.currentParent.addChild(result, 'suite'); this.currentParent = this.currentParent.last(); + this.suitesById[result.id] = this.currentParent; } suiteDone(result) {