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.
This commit is contained in:
Steve Gravrock
2025-09-16 21:03:16 -07:00
parent 4ccc7bf3ac
commit 8309416cb2
10 changed files with 231 additions and 52 deletions

View File

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

View File

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

View File

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

View File

@@ -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"]'
);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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