Skip to content

Commit 3d440fd

Browse files
committed
Core: Add QUnit.config.testFilter
Add programmatic callback to filter tests at runtime. Useful for quarantining flaky tests, parallel test sharding, and runtime capability detection. The callback receives testInfo { testId, testName, module, skip } and returns true to run the test or false to skip it.
1 parent a8e1436 commit 3d440fd

File tree

6 files changed

+523
-7
lines changed

6 files changed

+523
-7
lines changed

demos/testFilter.js

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
/**
2+
* Demo: QUnit.config.testFilter
3+
*
4+
* The testFilter callback allows you to programmatically filter which tests run
5+
* at runtime. This is useful for CI workflows, flaky test quarantine, dynamic
6+
* test selection, and other advanced scenarios.
7+
*/
8+
9+
// ============================================================================
10+
// Example: Combined testFilter implementation
11+
// ============================================================================
12+
13+
// Quarantine list - in production, load from JSON file or API
14+
const quarantineList = ['flaky network test', 'timing-dependent test'];
15+
16+
QUnit.config.testFilter = function (testInfo) {
17+
// testInfo contains: { testId, testName, module, skip }
18+
19+
// 1. Skip quarantined tests in CI
20+
if (process.env.CI === 'true') {
21+
const isQuarantined = quarantineList.some(function (pattern) {
22+
return testInfo.testName.indexOf(pattern) !== -1;
23+
});
24+
if (isQuarantined) {
25+
console.log('[QUARANTINE] Skipping: ' + testInfo.module + ' > ' + testInfo.testName);
26+
return false;
27+
}
28+
}
29+
30+
// 2. Skip slow tests in quick mode
31+
if (process.env.QUICK_RUN === 'true') {
32+
if (testInfo.testName.toLowerCase().indexOf('slow') !== -1) {
33+
console.log('[QUICK_RUN] Skipping slow test: ' + testInfo.testName);
34+
return false;
35+
}
36+
}
37+
38+
// 3. Filter by module name if specified
39+
if (process.env.TEST_MODULE) {
40+
if (testInfo.module.indexOf(process.env.TEST_MODULE) === -1) {
41+
return false;
42+
}
43+
}
44+
45+
// 4. Parallel sharding - distribute tests across workers
46+
if (process.env.WORKER_ID !== undefined) {
47+
const WORKER_ID = parseInt(process.env.WORKER_ID, 10);
48+
const TOTAL_WORKERS = parseInt(process.env.TOTAL_WORKERS || '1', 10);
49+
50+
let hash = 0;
51+
for (let i = 0; i < testInfo.testId.length; i++) {
52+
const char = testInfo.testId.charCodeAt(i);
53+
hash = ((hash << 5) - hash) + char;
54+
hash = hash & hash;
55+
}
56+
hash = Math.abs(hash);
57+
58+
const assignedWorker = hash % TOTAL_WORKERS;
59+
if (assignedWorker !== WORKER_ID) {
60+
return false;
61+
}
62+
console.log(' [Worker ' + WORKER_ID + '] Running: ' + testInfo.module + ' > ' + testInfo.testName);
63+
}
64+
65+
// 5. Feature detection - skip tests for unavailable features
66+
// Tests tagged with [feature] in name are checked against runtime capabilities
67+
const features = {
68+
webgl: typeof WebGLRenderingContext !== 'undefined',
69+
webrtc: typeof RTCPeerConnection !== 'undefined',
70+
serviceWorker: typeof navigator !== 'undefined' && 'serviceWorker' in navigator,
71+
indexedDB: typeof indexedDB !== 'undefined'
72+
};
73+
74+
for (const feature in features) {
75+
const tag = '[' + feature + ']';
76+
if (testInfo.testName.indexOf(tag) !== -1 && !features[feature]) {
77+
console.log('[FEATURE] Skipping ' + feature + ' test: ' + testInfo.testName);
78+
return false;
79+
}
80+
}
81+
82+
return true;
83+
};
84+
85+
// ============================================================================
86+
// Example 2: Quarantine flaky tests in CI
87+
// ============================================================================
88+
89+
if (process.env.CI === 'true') {
90+
// In production, this list might come from a JSON file or API
91+
const quarantineList = [
92+
'flaky network test',
93+
'timing-dependent test',
94+
'unreliable integration test'
95+
];
96+
97+
QUnit.config.testFilter = function (testInfo) {
98+
const isQuarantined = quarantineList.some(pattern =>
99+
testInfo.testName.includes(pattern)
100+
);
101+
102+
if (isQuarantined) {
103+
console.log(`[QUARANTINE] Skipping: ${testInfo.module} > ${testInfo.testName}`);
104+
return false;
105+
}
106+
107+
return true;
108+
};
109+
}
110+
111+
// ============================================================================
112+
// Example 3: Filter by module name
113+
// ============================================================================
114+
115+
// Run only tests from specific modules
116+
if (process.env.TEST_MODULE) {
117+
const targetModule = process.env.TEST_MODULE;
118+
119+
QUnit.config.testFilter = function (testInfo) {
120+
const moduleMatches = testInfo.module.includes(targetModule);
121+
122+
if (!moduleMatches) {
123+
console.log(`[MODULE_FILTER] Skipping: ${testInfo.module}`);
124+
}
125+
126+
return moduleMatches;
127+
};
128+
}
129+
130+
// ============================================================================
131+
// Example 4: Feature flags - Run tests based on runtime capabilities
132+
// ============================================================================
133+
134+
// Check if certain features are available at runtime
135+
const features = {
136+
webgl: typeof WebGLRenderingContext !== 'undefined',
137+
webrtc: typeof RTCPeerConnection !== 'undefined',
138+
serviceWorker: typeof navigator !== 'undefined' && 'serviceWorker' in navigator,
139+
indexedDB: typeof indexedDB !== 'undefined'
140+
};
141+
142+
QUnit.config.testFilter = function (testInfo) {
143+
for (const [feature, available] of Object.entries(features)) {
144+
if (testInfo.testName.includes(`[${feature}]`) && !available) {
145+
console.log(`[FEATURE_FLAG] Skipping ${feature} test: ${testInfo.testName}`);
146+
return false;
147+
}
148+
}
149+
150+
return true;
151+
};
152+
153+
// ============================================================================
154+
// Example 5: Parallel test sharding across workers
155+
// ============================================================================
156+
157+
if (process.env.WORKER_ID !== undefined) {
158+
const WORKER_ID = parseInt(process.env.WORKER_ID, 10);
159+
const TOTAL_WORKERS = parseInt(process.env.TOTAL_WORKERS || '1', 10);
160+
161+
console.log(`Worker ${WORKER_ID + 1} of ${TOTAL_WORKERS}`);
162+
163+
function hashTestId (testId) {
164+
let hash = 0;
165+
for (let i = 0; i < testId.length; i++) {
166+
const char = testId.charCodeAt(i);
167+
hash = ((hash << 5) - hash) + char;
168+
hash = hash & hash;
169+
}
170+
return Math.abs(hash);
171+
}
172+
173+
QUnit.config.testFilter = function (testInfo) {
174+
const hash = hashTestId(testInfo.testId);
175+
const assignedWorker = hash % TOTAL_WORKERS;
176+
const shouldRun = assignedWorker === WORKER_ID;
177+
178+
if (shouldRun) {
179+
console.log(` [Worker ${WORKER_ID}] Running: ${testInfo.module} > ${testInfo.testName}`);
180+
}
181+
182+
return shouldRun;
183+
};
184+
}
185+
186+
// ============================================================================
187+
// Example 6: Combine multiple filter conditions
188+
// ============================================================================
189+
190+
QUnit.config.testFilter = function (testInfo) {
191+
// Skip quarantined tests
192+
const quarantined = ['flaky test'];
193+
if (quarantined.some(pattern => testInfo.testName.includes(pattern))) {
194+
return false;
195+
}
196+
197+
// Skip slow tests in quick mode
198+
if (process.env.QUICK_RUN && testInfo.testName.includes('slow')) {
199+
return false;
200+
}
201+
202+
// Only run tests from specific module if specified
203+
if (process.env.TEST_MODULE && !testInfo.module.includes(process.env.TEST_MODULE)) {
204+
return false;
205+
}
206+
207+
return true;
208+
};
209+
210+
// ============================================================================
211+
// Example tests
212+
// ============================================================================
213+
214+
QUnit.module('Fast tests', function () {
215+
QUnit.test('quick calculation', function (assert) {
216+
assert.equal(2 + 2, 4);
217+
});
218+
219+
QUnit.test('string operation', function (assert) {
220+
assert.equal('test'.toUpperCase(), 'TEST');
221+
});
222+
});
223+
224+
QUnit.module('Slow tests', function () {
225+
QUnit.test('slow database query', function (assert) {
226+
assert.ok(true);
227+
});
228+
229+
QUnit.test('slow network request', function (assert) {
230+
assert.ok(true);
231+
});
232+
});
233+
234+
QUnit.module('Integration tests', function () {
235+
QUnit.test('API integration', function (assert) {
236+
assert.ok(true);
237+
});
238+
239+
QUnit.test('flaky network test', function (assert) {
240+
assert.ok(true);
241+
});
242+
243+
QUnit.test('timing-dependent test', function (assert) {
244+
assert.ok(true);
245+
});
246+
});
247+
248+
QUnit.module('Feature-specific tests', function () {
249+
QUnit.test('[webgl] 3D rendering', function (assert) {
250+
assert.ok(true, 'WebGL test');
251+
});
252+
253+
QUnit.test('[webrtc] peer connection', function (assert) {
254+
assert.ok(true, 'WebRTC test');
255+
});
256+
257+
QUnit.test('[serviceWorker] caching', function (assert) {
258+
assert.ok(true, 'Service Worker test');
259+
});
260+
261+
QUnit.test('[indexedDB] storage', function (assert) {
262+
assert.ok(true, 'IndexedDB test');
263+
});
264+
});
265+
266+
// ============================================================================
267+
// Usage Examples:
268+
// ============================================================================
269+
//
270+
// Skip slow tests:
271+
// QUICK_RUN=true node bin/qunit.js demos/testFilter.js
272+
//
273+
// Run only "Fast tests" module:
274+
// TEST_MODULE="Fast tests" node bin/qunit.js demos/testFilter.js
275+
//
276+
// Enable quarantine in CI:
277+
// CI=true node bin/qunit.js demos/testFilter.js
278+
//
279+
// Parallel execution with 3 workers:
280+
// WORKER_ID=0 TOTAL_WORKERS=3 node bin/qunit.js demos/testFilter.js
281+
// WORKER_ID=1 TOTAL_WORKERS=3 node bin/qunit.js demos/testFilter.js
282+
// WORKER_ID=2 TOTAL_WORKERS=3 node bin/qunit.js demos/testFilter.js
283+
//
284+
// ============================================================================
285+
// Notes:
286+
// ============================================================================
287+
//
288+
// - testFilter runs AFTER test.only/test.skip/test.if checks
289+
// - testFilter runs BEFORE CLI --filter and --module parameters
290+
// - Return true to run the test, false to skip it
291+
// - Thrown errors are logged but don't stop the test run
292+
// - testInfo.skip is true if test was already marked to skip
293+
//

0 commit comments

Comments
 (0)