@@ -176,22 +176,88 @@ function testMatchesPattern(test, patterns) {
176176}
177177
178178class TestPlan {
179- constructor ( count ) {
179+ #waitIndefinitely = false ;
180+ #planPromise = null ;
181+ #timeoutId = null ;
182+
183+ constructor ( count , options = kEmptyObject ) {
180184 validateUint32 ( count , 'count' ) ;
185+ validateObject ( options , 'options' ) ;
181186 this . expected = count ;
182187 this . actual = 0 ;
188+
189+ const { wait } = options ;
190+ if ( typeof wait === 'boolean' ) {
191+ this . wait = wait ;
192+ this . #waitIndefinitely = wait ;
193+ } else if ( typeof wait === 'number' ) {
194+ validateNumber ( wait , 'options.wait' , 0 , TIMEOUT_MAX ) ;
195+ this . wait = wait ;
196+ } else if ( wait !== undefined ) {
197+ throw new ERR_INVALID_ARG_TYPE ( 'options.wait' , [ 'boolean' , 'number' ] , wait ) ;
198+ }
199+ }
200+
201+ #planMet( ) {
202+ return this . actual === this . expected ;
203+ }
204+
205+ #createTimeout( reject ) {
206+ return setTimeout ( ( ) => {
207+ const err = new ERR_TEST_FAILURE (
208+ `plan timed out after ${ this . wait } ms with ${ this . actual } assertions when expecting ${ this . expected } ` ,
209+ kTestTimeoutFailure ,
210+ ) ;
211+ reject ( err ) ;
212+ } , this . wait ) ;
183213 }
184214
185215 check ( ) {
186- if ( this . actual !== this . expected ) {
216+ if ( this . #planMet( ) ) {
217+ if ( this . #timeoutId) {
218+ clearTimeout ( this . #timeoutId) ;
219+ this . #timeoutId = null ;
220+ }
221+ if ( this . #planPromise) {
222+ const { resolve } = this . #planPromise;
223+ resolve ( ) ;
224+ this . #planPromise = null ;
225+ }
226+ return ;
227+ }
228+
229+ if ( ! this . #shouldWait( ) ) {
187230 throw new ERR_TEST_FAILURE (
188231 `plan expected ${ this . expected } assertions but received ${ this . actual } ` ,
189232 kTestCodeFailure ,
190233 ) ;
191234 }
235+
236+ if ( ! this . #planPromise) {
237+ const { promise, resolve, reject } = PromiseWithResolvers ( ) ;
238+ this . #planPromise = { __proto__ : null , promise, resolve, reject } ;
239+
240+ if ( ! this . #waitIndefinitely) {
241+ this . #timeoutId = this . #createTimeout( reject ) ;
242+ }
243+ }
244+
245+ return this . #planPromise. promise ;
246+ }
247+
248+ count ( ) {
249+ this . actual ++ ;
250+ if ( this . #planPromise) {
251+ this . check ( ) ;
252+ }
253+ }
254+
255+ #shouldWait( ) {
256+ return this . wait !== undefined && this . wait !== false ;
192257 }
193258}
194259
260+
195261class TestContext {
196262 #assert;
197263 #test;
@@ -228,15 +294,15 @@ class TestContext {
228294 this . #test. diagnostic ( message ) ;
229295 }
230296
231- plan ( count ) {
297+ plan ( count , options = kEmptyObject ) {
232298 if ( this . #test. plan !== null ) {
233299 throw new ERR_TEST_FAILURE (
234300 'cannot set plan more than once' ,
235301 kTestCodeFailure ,
236302 ) ;
237303 }
238304
239- this . #test. plan = new TestPlan ( count ) ;
305+ this . #test. plan = new TestPlan ( count , options ) ;
240306 }
241307
242308 get assert ( ) {
@@ -249,7 +315,7 @@ class TestContext {
249315 map . forEach ( ( method , name ) => {
250316 assert [ name ] = ( ...args ) => {
251317 if ( plan !== null ) {
252- plan . actual ++ ;
318+ plan . count ( ) ;
253319 }
254320 return ReflectApply ( method , this , args ) ;
255321 } ;
@@ -260,7 +326,7 @@ class TestContext {
260326 // stacktrace from the correct starting point.
261327 function ok ( ...args ) {
262328 if ( plan !== null ) {
263- plan . actual ++ ;
329+ plan . count ( ) ;
264330 }
265331 innerOk ( ok , args . length , ...args ) ;
266332 }
@@ -296,7 +362,7 @@ class TestContext {
296362
297363 const { plan } = this . #test;
298364 if ( plan !== null ) {
299- plan . actual ++ ;
365+ plan . count ( ) ;
300366 }
301367
302368 const subtest = this . #test. createSubtest (
@@ -958,30 +1024,46 @@ class Test extends AsyncResource {
9581024 const runArgs = ArrayPrototypeSlice ( args ) ;
9591025 ArrayPrototypeUnshift ( runArgs , this . fn , ctx ) ;
9601026
1027+ const promises = [ ] ;
9611028 if ( this . fn . length === runArgs . length - 1 ) {
962- // This test is using legacy Node.js error first callbacks.
1029+ // This test is using legacy Node.js error- first callbacks.
9631030 const { promise, cb } = createDeferredCallback ( ) ;
964-
9651031 ArrayPrototypePush ( runArgs , cb ) ;
1032+
9661033 const ret = ReflectApply ( this . runInAsyncScope , this , runArgs ) ;
9671034
9681035 if ( isPromise ( ret ) ) {
9691036 this . fail ( new ERR_TEST_FAILURE (
9701037 'passed a callback but also returned a Promise' ,
9711038 kCallbackAndPromisePresent ,
9721039 ) ) ;
973- await SafePromiseRace ( [ ret , stopPromise ] ) ;
1040+ ArrayPrototypePush ( promises , ret ) ;
9741041 } else {
975- await SafePromiseRace ( [ PromiseResolve ( promise ) , stopPromise ] ) ;
1042+ ArrayPrototypePush ( promises , PromiseResolve ( promise ) ) ;
9761043 }
9771044 } else {
9781045 // This test is synchronous or using Promises.
9791046 const promise = ReflectApply ( this . runInAsyncScope , this , runArgs ) ;
980- await SafePromiseRace ( [ PromiseResolve ( promise ) , stopPromise ] ) ;
1047+ ArrayPrototypePush ( promises , PromiseResolve ( promise ) ) ;
9811048 }
9821049
1050+ ArrayPrototypePush ( promises , stopPromise ) ;
1051+
1052+ // Wait for the race to finish
1053+ await SafePromiseRace ( promises ) ;
1054+
9831055 this [ kShouldAbort ] ( ) ;
9841056 this . plan ?. check ( ) ;
1057+
1058+ if ( this . plan !== null ) {
1059+ const planPromise = this . plan ?. check ( ) ;
1060+ // If the plan returns a promise, it means that it is waiting for more assertions to be made before
1061+ // continuing.
1062+ if ( planPromise ) {
1063+ await SafePromiseRace ( [ planPromise , stopPromise ] ) ;
1064+ }
1065+ }
1066+
9851067 this . pass ( ) ;
9861068 await afterEach ( ) ;
9871069 await after ( ) ;
0 commit comments