@@ -12,6 +12,7 @@ const {
1212 MathMax,
1313 Number,
1414 ObjectDefineProperty,
15+ ObjectEntries,
1516 ObjectSeal,
1617 PromisePrototypeThen,
1718 PromiseResolve,
@@ -88,6 +89,7 @@ const {
8889 testOnlyFlag,
8990} = parseCommandLine ( ) ;
9091let kResistStopPropagation ;
92+ let assertObj ;
9193let findSourceMap ;
9294let noopTestStream ;
9395
@@ -101,6 +103,19 @@ function lazyFindSourceMap(file) {
101103 return findSourceMap ( file ) ;
102104}
103105
106+ function lazyAssertObject ( ) {
107+ if ( assertObj === undefined ) {
108+ assertObj = new SafeMap ( ) ;
109+ const assert = require ( 'assert' ) ;
110+ for ( const { 0 : key , 1 : value } of ObjectEntries ( assert ) ) {
111+ if ( typeof value === 'function' ) {
112+ assertObj . set ( value , key ) ;
113+ }
114+ }
115+ }
116+ return assertObj ;
117+ }
118+
104119function stopTest ( timeout , signal ) {
105120 const deferred = createDeferredPromise ( ) ;
106121 const abortListener = addAbortListener ( signal , deferred . resolve ) ;
@@ -153,7 +168,25 @@ function testMatchesPattern(test, patterns) {
153168 ) ;
154169}
155170
171+ class TestPlan {
172+ constructor ( count ) {
173+ validateUint32 ( count , 'count' , 0 ) ;
174+ this . expected = count ;
175+ this . actual = 0 ;
176+ }
177+
178+ check ( ) {
179+ if ( this . actual !== this . expected ) {
180+ throw new ERR_TEST_FAILURE (
181+ `plan expected ${ this . expected } assertions but received ${ this . actual } ` ,
182+ kTestCodeFailure ,
183+ ) ;
184+ }
185+ }
186+ }
187+
156188class TestContext {
189+ #assert;
157190 #test;
158191
159192 constructor ( test ) {
@@ -180,6 +213,36 @@ class TestContext {
180213 this . #test. diagnostic ( message ) ;
181214 }
182215
216+ plan ( count ) {
217+ if ( this . #test. plan !== null ) {
218+ throw new ERR_TEST_FAILURE (
219+ 'cannot set plan more than once' ,
220+ kTestCodeFailure ,
221+ ) ;
222+ }
223+
224+ this . #test. plan = new TestPlan ( count ) ;
225+ }
226+
227+ get assert ( ) {
228+ if ( this . #assert === undefined ) {
229+ const { plan } = this . #test;
230+ const assertions = lazyAssertObject ( ) ;
231+ const assert = { __proto__ : null } ;
232+
233+ this . #assert = assert ;
234+ for ( const { 0 : method , 1 : name } of assertions . entries ( ) ) {
235+ assert [ name ] = ( ...args ) => {
236+ if ( plan !== null ) {
237+ plan . actual ++ ;
238+ }
239+ return ReflectApply ( method , assert , args ) ;
240+ } ;
241+ }
242+ }
243+ return this . #assert;
244+ }
245+
183246 get mock ( ) {
184247 this . #test. mock ??= new MockTracker ( ) ;
185248 return this . #test. mock ;
@@ -257,7 +320,7 @@ class Test extends AsyncResource {
257320 super ( 'Test' ) ;
258321
259322 let { fn, name, parent } = options ;
260- const { concurrency, loc, only, timeout, todo, skip, signal } = options ;
323+ const { concurrency, loc, only, timeout, todo, skip, signal, plan } = options ;
261324
262325 if ( typeof fn !== 'function' ) {
263326 fn = noop ;
@@ -373,6 +436,8 @@ class Test extends AsyncResource {
373436 this . fn = fn ;
374437 this . harness = null ; // Configured on the root test by the test harness.
375438 this . mock = null ;
439+ this . plan = null ;
440+ this . expectedAssertions = plan ;
376441 this . cancelled = false ;
377442 this . skipped = skip !== undefined && skip !== false ;
378443 this . isTodo = todo !== undefined && todo !== false ;
@@ -703,6 +768,11 @@ class Test extends AsyncResource {
703768
704769 const hookArgs = this . getRunArgs ( ) ;
705770 const { args, ctx } = hookArgs ;
771+
772+ if ( this . plan === null && this . expectedAssertions ) {
773+ ctx . plan ( this . expectedAssertions ) ;
774+ }
775+
706776 const after = async ( ) => {
707777 if ( this . hooks . after . length > 0 ) {
708778 await this . runHook ( 'after' , hookArgs ) ;
@@ -754,7 +824,7 @@ class Test extends AsyncResource {
754824 this . postRun ( ) ;
755825 return ;
756826 }
757-
827+ this . plan ?. check ( ) ;
758828 this . pass ( ) ;
759829 await afterEach ( ) ;
760830 await after ( ) ;
@@ -910,7 +980,7 @@ class Test extends AsyncResource {
910980 this . finished = true ;
911981
912982 if ( this . parent === this . root &&
913- this . root . waitingOn > this . root . subtests . length ) {
983+ this . root . waitingOn > this . root . subtests . length ) {
914984 // At this point all of the tests have finished running. However, there
915985 // might be ref'ed handles keeping the event loop alive. This gives the
916986 // global after() hook a chance to clean them up. The user may also
@@ -1008,7 +1078,7 @@ class TestHook extends Test {
10081078
10091079 // Report failures in the root test's after() hook.
10101080 if ( error && parent !== null &&
1011- parent === parent . root && this . hookType === 'after' ) {
1081+ parent === parent . root && this . hookType === 'after' ) {
10121082
10131083 if ( isTestFailureError ( error ) ) {
10141084 error . failureType = kHookFailure ;
0 commit comments