1import * as common from '../common/index.mjs'; 2import * as fixtures from '../common/fixtures.mjs'; 3import { join } from 'node:path'; 4import { describe, it, run } from 'node:test'; 5import { dot, spec, tap } from 'node:test/reporters'; 6import assert from 'node:assert'; 7 8const testFixtures = fixtures.path('test-runner'); 9 10describe('require(\'node:test\').run', { concurrency: true }, () => { 11 12 it('should run with no tests', async () => { 13 const stream = run({ files: [] }); 14 stream.on('test:fail', common.mustNotCall()); 15 stream.on('test:pass', common.mustNotCall()); 16 // eslint-disable-next-line no-unused-vars 17 for await (const _ of stream); 18 }); 19 20 it('should fail with non existing file', async () => { 21 const stream = run({ files: ['a-random-file-that-does-not-exist.js'] }); 22 stream.on('test:fail', common.mustCall(1)); 23 stream.on('test:pass', common.mustNotCall()); 24 // eslint-disable-next-line no-unused-vars 25 for await (const _ of stream); 26 }); 27 28 it('should succeed with a file', async () => { 29 const stream = run({ files: [join(testFixtures, 'default-behavior/test/random.cjs')] }); 30 stream.on('test:fail', common.mustNotCall()); 31 stream.on('test:pass', common.mustCall(1)); 32 // eslint-disable-next-line no-unused-vars 33 for await (const _ of stream); 34 }); 35 36 it('should run same file twice', async () => { 37 const stream = run({ 38 files: [ 39 join(testFixtures, 'default-behavior/test/random.cjs'), 40 join(testFixtures, 'default-behavior/test/random.cjs'), 41 ] 42 }); 43 stream.on('test:fail', common.mustNotCall()); 44 stream.on('test:pass', common.mustCall(2)); 45 // eslint-disable-next-line no-unused-vars 46 for await (const _ of stream); 47 }); 48 49 it('should run a failed test', async () => { 50 const stream = run({ files: [testFixtures] }); 51 stream.on('test:fail', common.mustCall(1)); 52 stream.on('test:pass', common.mustNotCall()); 53 // eslint-disable-next-line no-unused-vars 54 for await (const _ of stream); 55 }); 56 57 it('should support timeout', async () => { 58 const stream = run({ timeout: 50, files: [ 59 fixtures.path('test-runner', 'never_ending_sync.js'), 60 fixtures.path('test-runner', 'never_ending_async.js'), 61 ] }); 62 stream.on('test:fail', common.mustCall(2)); 63 stream.on('test:pass', common.mustNotCall()); 64 // eslint-disable-next-line no-unused-vars 65 for await (const _ of stream); 66 }); 67 68 it('should validate files', async () => { 69 [Symbol(), {}, () => {}, 0, 1, 0n, 1n, '', '1', Promise.resolve([]), true, false] 70 .forEach((files) => assert.throws(() => run({ files }), { 71 code: 'ERR_INVALID_ARG_TYPE' 72 })); 73 }); 74 75 it('should be piped with dot', async () => { 76 const result = await run({ 77 files: [join(testFixtures, 'default-behavior/test/random.cjs')] 78 }).compose(dot).toArray(); 79 assert.deepStrictEqual(result, [ 80 '.', 81 '\n', 82 ]); 83 }); 84 85 describe('should be piped with spec reporter', () => { 86 it('new spec', async () => { 87 const specReporter = new spec(); 88 const result = await run({ 89 files: [join(testFixtures, 'default-behavior/test/random.cjs')] 90 }).compose(specReporter).toArray(); 91 const stringResults = result.map((bfr) => bfr.toString()); 92 assert.match(stringResults[0], /this should pass/); 93 assert.match(stringResults[1], /tests 1/); 94 assert.match(stringResults[1], /pass 1/); 95 }); 96 97 it('spec()', async () => { 98 const specReporter = spec(); 99 const result = await run({ 100 files: [join(testFixtures, 'default-behavior/test/random.cjs')] 101 }).compose(specReporter).toArray(); 102 const stringResults = result.map((bfr) => bfr.toString()); 103 assert.match(stringResults[0], /this should pass/); 104 assert.match(stringResults[1], /tests 1/); 105 assert.match(stringResults[1], /pass 1/); 106 }); 107 }); 108 109 it('should be piped with tap', async () => { 110 const result = await run({ 111 files: [join(testFixtures, 'default-behavior/test/random.cjs')] 112 }).compose(tap).toArray(); 113 assert.strictEqual(result.length, 13); 114 assert.strictEqual(result[0], 'TAP version 13\n'); 115 assert.strictEqual(result[1], '# Subtest: this should pass\n'); 116 assert.strictEqual(result[2], 'ok 1 - this should pass\n'); 117 assert.match(result[3], /duration_ms: \d+\.?\d*/); 118 assert.strictEqual(result[4], '1..1\n'); 119 assert.strictEqual(result[5], '# tests 1\n'); 120 assert.strictEqual(result[6], '# suites 0\n'); 121 assert.strictEqual(result[7], '# pass 1\n'); 122 assert.strictEqual(result[8], '# fail 0\n'); 123 assert.strictEqual(result[9], '# cancelled 0\n'); 124 assert.strictEqual(result[10], '# skipped 0\n'); 125 assert.strictEqual(result[11], '# todo 0\n'); 126 assert.match(result[12], /# duration_ms \d+\.?\d*/); 127 }); 128 129 it('should skip tests not matching testNamePatterns - RegExp', async () => { 130 const result = await run({ 131 files: [join(testFixtures, 'default-behavior/test/skip_by_name.cjs')], 132 testNamePatterns: [/executed/] 133 }) 134 .compose(tap) 135 .toArray(); 136 assert.strictEqual(result[2], 'ok 1 - this should be skipped # SKIP test name does not match pattern\n'); 137 assert.strictEqual(result[5], 'ok 2 - this should be executed\n'); 138 }); 139 140 it('should skip tests not matching testNamePatterns - string', async () => { 141 const result = await run({ 142 files: [join(testFixtures, 'default-behavior/test/skip_by_name.cjs')], 143 testNamePatterns: ['executed'] 144 }) 145 .compose(tap) 146 .toArray(); 147 assert.strictEqual(result[2], 'ok 1 - this should be skipped # SKIP test name does not match pattern\n'); 148 assert.strictEqual(result[5], 'ok 2 - this should be executed\n'); 149 }); 150 151 it('should pass only to children', async () => { 152 const result = await run({ 153 files: [join(testFixtures, 'test_only.js')], 154 only: true 155 }) 156 .compose(tap) 157 .toArray(); 158 159 assert.strictEqual(result[2], 'ok 1 - this should be skipped # SKIP \'only\' option not set\n'); 160 assert.strictEqual(result[5], 'ok 2 - this should be executed\n'); 161 }); 162 163 it('should emit "test:watch:drained" event on watch mode', async () => { 164 const controller = new AbortController(); 165 await run({ 166 files: [join(testFixtures, 'default-behavior/test/random.cjs')], 167 watch: true, 168 signal: controller.signal, 169 }).on('data', function({ type }) { 170 if (type === 'test:watch:drained') { 171 controller.abort(); 172 } 173 }); 174 }); 175 176 describe('AbortSignal', () => { 177 it('should stop watch mode when abortSignal aborts', async () => { 178 const controller = new AbortController(); 179 const result = await run({ 180 files: [join(testFixtures, 'default-behavior/test/random.cjs')], 181 watch: true, 182 signal: controller.signal, 183 }) 184 .compose(async function* (source) { 185 for await (const chunk of source) { 186 if (chunk.type === 'test:pass') { 187 controller.abort(); 188 yield chunk.data.name; 189 } 190 } 191 }) 192 .toArray(); 193 assert.deepStrictEqual(result, ['this should pass']); 194 }); 195 196 it('should abort when test succeeded', async () => { 197 const stream = run({ 198 files: [ 199 fixtures.path( 200 'test-runner', 201 'aborts', 202 'successful-test-still-call-abort.js' 203 ), 204 ], 205 }); 206 207 let passedTestCount = 0; 208 let failedTestCount = 0; 209 210 let output = ''; 211 for await (const data of stream) { 212 if (data.type === 'test:stdout') { 213 output += data.data.message.toString(); 214 } 215 if (data.type === 'test:fail') { 216 failedTestCount++; 217 } 218 if (data.type === 'test:pass') { 219 passedTestCount++; 220 } 221 } 222 223 assert.match(output, /abort called for test 1/); 224 assert.match(output, /abort called for test 2/); 225 assert.strictEqual(failedTestCount, 0, new Error('no tests should fail')); 226 assert.strictEqual(passedTestCount, 2); 227 }); 228 229 it('should abort when test failed', async () => { 230 const stream = run({ 231 files: [ 232 fixtures.path( 233 'test-runner', 234 'aborts', 235 'failed-test-still-call-abort.js' 236 ), 237 ], 238 }); 239 240 let passedTestCount = 0; 241 let failedTestCount = 0; 242 243 let output = ''; 244 for await (const data of stream) { 245 if (data.type === 'test:stdout') { 246 output += data.data.message.toString(); 247 } 248 if (data.type === 'test:fail') { 249 failedTestCount++; 250 } 251 if (data.type === 'test:pass') { 252 passedTestCount++; 253 } 254 } 255 256 assert.match(output, /abort called for test 1/); 257 assert.match(output, /abort called for test 2/); 258 assert.strictEqual(passedTestCount, 0, new Error('no tests should pass')); 259 assert.strictEqual(failedTestCount, 2); 260 }); 261 }); 262 263 describe('sharding', () => { 264 const shardsTestsFixtures = fixtures.path('test-runner', 'shards'); 265 const shardsTestsFiles = [ 266 'a.cjs', 267 'b.cjs', 268 'c.cjs', 269 'd.cjs', 270 'e.cjs', 271 'f.cjs', 272 'g.cjs', 273 'h.cjs', 274 'i.cjs', 275 'j.cjs', 276 ].map((file) => join(shardsTestsFixtures, file)); 277 278 describe('validation', () => { 279 it('should require shard.total when having shard option', () => { 280 assert.throws(() => run({ files: shardsTestsFiles, shard: {} }), { 281 name: 'TypeError', 282 code: 'ERR_INVALID_ARG_TYPE', 283 message: 'The "options.shard.total" property must be of type number. Received undefined' 284 }); 285 }); 286 287 it('should require shard.index when having shards option', () => { 288 assert.throws(() => run({ 289 files: shardsTestsFiles, 290 shard: { 291 total: 5 292 } 293 }), { 294 name: 'TypeError', 295 code: 'ERR_INVALID_ARG_TYPE', 296 message: 'The "options.shard.index" property must be of type number. Received undefined' 297 }); 298 }); 299 300 it('should require shard.total to be greater than 0 when having shard option', () => { 301 assert.throws(() => run({ 302 files: shardsTestsFiles, 303 shard: { 304 total: 0, 305 index: 1 306 } 307 }), { 308 name: 'RangeError', 309 code: 'ERR_OUT_OF_RANGE', 310 message: 311 'The value of "options.shard.total" is out of range. It must be >= 1 && <= 9007199254740991. Received 0' 312 }); 313 }); 314 315 it('should require shard.index to be greater than 0 when having shard option', () => { 316 assert.throws(() => run({ 317 files: shardsTestsFiles, 318 shard: { 319 total: 6, 320 index: 0 321 } 322 }), { 323 name: 'RangeError', 324 code: 'ERR_OUT_OF_RANGE', 325 // eslint-disable-next-line max-len 326 message: 'The value of "options.shard.index" is out of range. It must be >= 1 && <= 6 ("options.shard.total"). Received 0' 327 }); 328 }); 329 330 it('should require shard.index to not be greater than the shards total when having shard option', () => { 331 assert.throws(() => run({ 332 files: shardsTestsFiles, 333 shard: { 334 total: 6, 335 index: 7 336 } 337 }), { 338 name: 'RangeError', 339 code: 'ERR_OUT_OF_RANGE', 340 // eslint-disable-next-line max-len 341 message: 'The value of "options.shard.index" is out of range. It must be >= 1 && <= 6 ("options.shard.total"). Received 7' 342 }); 343 }); 344 345 it('should require watch mode to be disabled when having shard option', () => { 346 assert.throws(() => run({ 347 files: shardsTestsFiles, 348 watch: true, 349 shard: { 350 total: 6, 351 index: 1 352 } 353 }), { 354 name: 'TypeError', 355 code: 'ERR_INVALID_ARG_VALUE', 356 message: 'The property \'options.shard\' shards not supported with watch mode. Received true' 357 }); 358 }); 359 }); 360 361 it('should run only the tests files matching the shard index', async () => { 362 const stream = run({ 363 files: shardsTestsFiles, 364 shard: { 365 total: 5, 366 index: 1 367 } 368 }); 369 370 const executedTestFiles = []; 371 stream.on('test:fail', common.mustNotCall()); 372 stream.on('test:pass', (passedTest) => { 373 executedTestFiles.push(passedTest.file); 374 }); 375 // eslint-disable-next-line no-unused-vars 376 for await (const _ of stream) ; 377 378 assert.deepStrictEqual(executedTestFiles, [ 379 join(shardsTestsFixtures, 'a.cjs'), 380 join(shardsTestsFixtures, 'f.cjs'), 381 ]); 382 }); 383 384 it('different shards should not run the same file', async () => { 385 const executedTestFiles = []; 386 387 const testStreams = []; 388 const shards = 5; 389 for (let i = 1; i <= shards; i++) { 390 const stream = run({ 391 files: shardsTestsFiles, 392 shard: { 393 total: shards, 394 index: i 395 } 396 }); 397 stream.on('test:fail', common.mustNotCall()); 398 stream.on('test:pass', (passedTest) => { 399 executedTestFiles.push(passedTest.file); 400 }); 401 testStreams.push(stream); 402 } 403 404 await Promise.all(testStreams.map(async (stream) => { 405 // eslint-disable-next-line no-unused-vars 406 for await (const _ of stream) ; 407 })); 408 409 assert.deepStrictEqual(executedTestFiles, [...new Set(executedTestFiles)]); 410 }); 411 412 it('combination of all shards should be all the tests', async () => { 413 const executedTestFiles = []; 414 415 const testStreams = []; 416 const shards = 5; 417 for (let i = 1; i <= shards; i++) { 418 const stream = run({ 419 files: shardsTestsFiles, 420 shard: { 421 total: shards, 422 index: i 423 } 424 }); 425 stream.on('test:fail', common.mustNotCall()); 426 stream.on('test:pass', (passedTest) => { 427 executedTestFiles.push(passedTest.file); 428 }); 429 testStreams.push(stream); 430 } 431 432 await Promise.all(testStreams.map(async (stream) => { 433 // eslint-disable-next-line no-unused-vars 434 for await (const _ of stream) ; 435 })); 436 437 assert.deepStrictEqual(executedTestFiles.sort(), [...shardsTestsFiles].sort()); 438 }); 439 }); 440}); 441