Skip to content

Commit 854470d

Browse files
gbudjeakpKrinkle
authored andcommitted
Core: Add QUnit.config.testFilter to programmatically filter tests
Ref #1814. Closes #1815.
1 parent 1fdc1a3 commit 854470d

File tree

5 files changed

+160
-7
lines changed

5 files changed

+160
-7
lines changed

docs/api/config/testFilter.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
layout: page-api
3+
title: QUnit.config.testFilter
4+
excerpt: Programmatically filter which tests to run.
5+
groups:
6+
- config
7+
version_added: "2.25.0"
8+
---
9+
10+
Programmatically filter which tests to run.
11+
12+
<table>
13+
<tr>
14+
<th>type</th>
15+
<td markdown="span">`function` or `null`</td>
16+
</tr>
17+
<tr>
18+
<th>default</th>
19+
<td markdown="span">`null`</td>
20+
</tr>
21+
</table>
22+
23+
The `testFilter` property allows you to implement custom logic for filtering which tests to run at runtime. This is useful for advanced scenarios such as:
24+
25+
* Quarantining flaky tests in CI environments
26+
* Distributing tests across parallel workers
27+
* Loading filter criteria from external sources (APIs, files, etc.)
28+
29+
The callback receives a `testInfo` object and must return a boolean:
30+
* Return `true` to run the test
31+
* Return `false` to skip the test
32+
33+
If the callback throws an error, the error is logged as a warning and the test is skipped.
34+
35+
## Parameters
36+
37+
### `testInfo` (object)
38+
39+
| Property | Type | Description |
40+
|----------|------|-------------|
41+
| `testId` | string | Internal hash identifier (used by "Rerun" links)
42+
| `testName` | string | Name of the test
43+
| `module` | string | Name of the parent module
44+
| `skip` | boolean | Whether test was already marked to skip
45+
46+
## See also
47+
48+
* [QUnit.config.filter](./filter.md)
49+
* [QUnit.config.module](./module.md)
50+
* [test.if()](../QUnit/test.if.md)
51+
52+
## Examples
53+
54+
### Quarantine flaky tests
55+
56+
Use an external quarantine list to skip unstable tests in CI without modifying test code.
57+
58+
```js
59+
const quarantineList = ['flaky network test', 'timing-dependent test'];
60+
61+
QUnit.config.testFilter = function (testInfo) {
62+
if (process.env.CI === 'true') {
63+
const isQuarantined = quarantineList.some(function (pattern) {
64+
return testInfo.testName.indexOf(pattern) !== -1;
65+
});
66+
if (isQuarantined) {
67+
console.log('[QUARANTINE] Skipping: ' + testInfo.testName);
68+
return false;
69+
}
70+
}
71+
return true;
72+
};
73+
```
74+
75+
### Parallel test sharding
76+
77+
Distribute tests across multiple workers using deterministic hash-based assignment.
78+
79+
```js
80+
const WORKER_ID = parseInt(process.env.WORKER_ID, 10);
81+
const TOTAL_WORKERS = parseInt(process.env.TOTAL_WORKERS, 10);
82+
83+
QUnit.config.testFilter = function (testInfo) {
84+
let hash = 0;
85+
for (let i = 0; i < testInfo.testId.length; i++) {
86+
const char = testInfo.testId.charCodeAt(i);
87+
hash = ((hash << 5) - hash) + char;
88+
hash = hash & hash;
89+
}
90+
hash = Math.abs(hash);
91+
92+
// Assign test to worker
93+
const assignedWorker = hash % TOTAL_WORKERS;
94+
return assignedWorker === WORKER_ID;
95+
};
96+
```

src/core/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const config = {
2525
// Select by pattern or case-insensitive substring match against "moduleName: testName"
2626
filter: '',
2727

28+
testFilter: null,
29+
2830
fixture: undefined,
2931

3032
// HTML Reporter: Hide results of passed tests.

src/core/test.js

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -810,15 +810,34 @@ Test.prototype = {
810810
}
811811

812812
const filter = config.filter;
813-
if (!filter) {
814-
return true;
813+
if (filter) {
814+
const regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec(filter);
815+
const fullName = (this.module.name + ': ' + this.testName);
816+
if (regexFilter) {
817+
if (!this.regexFilter(!!regexFilter[1], regexFilter[2], regexFilter[3], fullName)) {
818+
return false;
819+
}
820+
} else if (!this.stringFilter(filter, fullName)) {
821+
return false;
822+
}
823+
}
824+
825+
if (typeof config.testFilter === 'function') {
826+
const testInfo = {
827+
testId: this.testId,
828+
testName: this.testName,
829+
module: this.module.name,
830+
skip: !!this.skip
831+
};
832+
try {
833+
return !!config.testFilter(testInfo);
834+
} catch (error) {
835+
Logger.warn('Error in QUnit.config.testFilter callback: ', error);
836+
return false;
837+
}
815838
}
816839

817-
const regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec(filter);
818-
const fullName = (this.module.name + ': ' + this.testName);
819-
return regexFilter
820-
? this.regexFilter(!!regexFilter[1], regexFilter[2], regexFilter[3], fullName)
821-
: this.stringFilter(filter, fullName);
840+
return true;
822841
},
823842

824843
regexFilter: function (exclude, pattern, flags, fullName) {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const quarantineList = ['flaky test', 'broken test'];
2+
3+
QUnit.config.testFilter = function (testInfo) {
4+
return !quarantineList.some(function (pattern) {
5+
return testInfo.testName.indexOf(pattern) !== -1;
6+
});
7+
};
8+
9+
QUnit.module('testFilter demo');
10+
11+
QUnit.test('stable test', function (assert) {
12+
assert.true(true, 'this test should run');
13+
});
14+
15+
QUnit.test('flaky test', function (assert) {
16+
assert.true(false, 'this test should be filtered out');
17+
});
18+
19+
QUnit.test('broken test', function (assert) {
20+
assert.true(false, 'this test should also be filtered out');
21+
});
22+
23+
QUnit.test('another stable test', function (assert) {
24+
assert.true(true, 'this test should also run');
25+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# name: config.testFilter
2+
# command: ["qunit", "config-testFilter.js"]
3+
4+
TAP version 13
5+
ok 1 testFilter demo > stable test
6+
ok 2 testFilter demo > another stable test
7+
1..2
8+
# pass 2
9+
# skip 0
10+
# todo 0
11+
# fail 0

0 commit comments

Comments
 (0)