@@ -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 ) {
180184validateUint32 ( count , 'count' ) ;
185+ validateObject ( options , 'options' ) ;
181186this . expected = count ;
182187this . 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
185215check ( ) {
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( ) ) {
187230throw new ERR_TEST_FAILURE (
188231`plan expected ${ this . expected } assertions but received ${ this . actual } ` ,
189232kTestCodeFailure ,
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 {
228294this . #test. diagnostic ( message ) ;
229295}
230296
231- plan ( count ) {
297+ plan ( count , options = kEmptyObject ) {
232298if ( this . #test. plan !== null ) {
233299throw new ERR_TEST_FAILURE (
234300'cannot set plan more than once' ,
235301kTestCodeFailure ,
236302) ;
237303}
238304
239- this . #test. plan = new TestPlan ( count ) ;
305+ this . #test. plan = new TestPlan ( count , options ) ;
240306}
241307
242308get assert ( ) {
@@ -249,7 +315,7 @@ class TestContext {
249315map . forEach ( ( method , name ) => {
250316assert [ name ] = ( ...args ) => {
251317if ( plan !== null ) {
252- plan . actual ++ ;
318+ plan . count ( ) ;
253319}
254320return ReflectApply ( method , this , args ) ;
255321} ;
@@ -260,7 +326,7 @@ class TestContext {
260326// stacktrace from the correct starting point.
261327function ok ( ...args ) {
262328if ( plan !== null ) {
263- plan . actual ++ ;
329+ plan . count ( ) ;
264330}
265331innerOk ( ok , args . length , ...args ) ;
266332}
@@ -296,7 +362,7 @@ class TestContext {
296362
297363const { plan } = this . #test;
298364if ( plan !== null ) {
299- plan . actual ++ ;
365+ plan . count ( ) ;
300366}
301367
302368const subtest = this . #test. createSubtest (
@@ -959,30 +1025,46 @@ class Test extends AsyncResource {
9591025const runArgs = ArrayPrototypeSlice ( args ) ;
9601026ArrayPrototypeUnshift ( runArgs , this . fn , ctx ) ;
9611027
1028+ const promises = [ ] ;
9621029if ( this . fn . length === runArgs . length - 1 ) {
963- // This test is using legacy Node.js error first callbacks.
1030+ // This test is using legacy Node.js error- first callbacks.
9641031const { promise, cb } = createDeferredCallback ( ) ;
965-
9661032ArrayPrototypePush ( runArgs , cb ) ;
1033+
9671034const ret = ReflectApply ( this . runInAsyncScope , this , runArgs ) ;
9681035
9691036if ( isPromise ( ret ) ) {
9701037this . fail ( new ERR_TEST_FAILURE (
9711038'passed a callback but also returned a Promise' ,
9721039kCallbackAndPromisePresent ,
9731040) ) ;
974- await SafePromiseRace ( [ ret , stopPromise ] ) ;
1041+ ArrayPrototypePush ( promises , ret ) ;
9751042} else {
976- await SafePromiseRace ( [ PromiseResolve ( promise ) , stopPromise ] ) ;
1043+ ArrayPrototypePush ( promises , PromiseResolve ( promise ) ) ;
9771044}
9781045} else {
9791046// This test is synchronous or using Promises.
9801047const promise = ReflectApply ( this . runInAsyncScope , this , runArgs ) ;
981- await SafePromiseRace ( [ PromiseResolve ( promise ) , stopPromise ] ) ;
1048+ ArrayPrototypePush ( promises , PromiseResolve ( promise ) ) ;
9821049}
9831050
1051+ ArrayPrototypePush ( promises , stopPromise ) ;
1052+
1053+ // Wait for the race to finish
1054+ await SafePromiseRace ( promises ) ;
1055+
9841056this [ kShouldAbort ] ( ) ;
9851057this . plan ?. check ( ) ;
1058+
1059+ if ( this . plan !== null ) {
1060+ const planPromise = this . plan ?. check ( ) ;
1061+ // If the plan returns a promise, it means that it is waiting for more assertions to be made before
1062+ // continuing.
1063+ if ( planPromise ) {
1064+ await SafePromiseRace ( [ planPromise , stopPromise ] ) ;
1065+ }
1066+ }
1067+
9861068this . pass ( ) ;
9871069await afterEach ( ) ;
9881070await after ( ) ;
0 commit comments