@@ -11,6 +11,7 @@ const {
1111 FunctionPrototype,
1212 MathMax,
1313 Number,
14+ ObjectEntries,
1415 ObjectSeal,
1516 PromisePrototypeThen,
1617 PromiseResolve,
@@ -85,6 +86,7 @@ const {
8586 testOnlyFlag,
8687} = parseCommandLine ( ) ;
8788let kResistStopPropagation ;
89+ let assertObj ;
8890let findSourceMap ;
8991
9092const kRunOnceOptions = { __proto__ : null , preserveReturnValue : true } ;
@@ -97,6 +99,19 @@ function lazyFindSourceMap(file) {
9799 return findSourceMap ( file ) ;
98100}
99101
102+ function lazyAssertObject ( ) {
103+ if ( assertObj === undefined ) {
104+ assertObj = new SafeMap ( ) ;
105+ const assert = require ( 'assert' ) ;
106+ for ( const { 0 : key , 1 : value } of ObjectEntries ( assert ) ) {
107+ if ( typeof value === 'function' ) {
108+ assertObj . set ( value , key ) ;
109+ }
110+ }
111+ }
112+ return assertObj ;
113+ }
114+
100115function stopTest ( timeout , signal ) {
101116 const deferred = createDeferredPromise ( ) ;
102117 const abortListener = addAbortListener ( signal , deferred . resolve ) ;
@@ -136,7 +151,38 @@ function stopTest(timeout, signal) {
136151 return deferred . promise ;
137152}
138153
154+ function testMatchesPattern ( test , patterns ) {
155+ const matchesByNameOrParent = ArrayPrototypeSome ( patterns , ( re ) =>
156+ RegExpPrototypeExec ( re , test . name ) !== null ,
157+ ) || ( test . parent && testMatchesPattern ( test . parent , patterns ) ) ;
158+ if ( matchesByNameOrParent ) return true ;
159+
160+ const testNameWithAncestors = StringPrototypeTrim ( test . getTestNameWithAncestors ( ) ) ;
161+
162+ return ArrayPrototypeSome ( patterns , ( re ) =>
163+ RegExpPrototypeExec ( re , testNameWithAncestors ) !== null ,
164+ ) ;
165+ }
166+
167+ class TestPlan {
168+ constructor ( count ) {
169+ validateUint32 ( count , 'count' , 0 ) ;
170+ this . expected = count ;
171+ this . actual = 0 ;
172+ }
173+
174+ check ( ) {
175+ if ( this . actual !== this . expected ) {
176+ throw new ERR_TEST_FAILURE (
177+ `plan expected ${ this . expected } assertions but received ${ this . actual } ` ,
178+ kTestCodeFailure ,
179+ ) ;
180+ }
181+ }
182+ }
183+
139184class TestContext {
185+ #assert;
140186 #test;
141187
142188 constructor ( test ) {
@@ -163,6 +209,36 @@ class TestContext {
163209 this . #test. diagnostic ( message ) ;
164210 }
165211
212+ plan ( count ) {
213+ if ( this . #test. plan !== null ) {
214+ throw new ERR_TEST_FAILURE (
215+ 'cannot set plan more than once' ,
216+ kTestCodeFailure ,
217+ ) ;
218+ }
219+
220+ this . #test. plan = new TestPlan ( count ) ;
221+ }
222+
223+ get assert ( ) {
224+ if ( this . #assert === undefined ) {
225+ const { plan } = this . #test;
226+ const assertions = lazyAssertObject ( ) ;
227+ const assert = { __proto__ : null } ;
228+
229+ this . #assert = assert ;
230+ for ( const { 0 : method , 1 : name } of assertions . entries ( ) ) {
231+ assert [ name ] = ( ...args ) => {
232+ if ( plan !== null ) {
233+ plan . actual ++ ;
234+ }
235+ return ReflectApply ( method , assert , args ) ;
236+ } ;
237+ }
238+ }
239+ return this . #assert;
240+ }
241+
166242 get mock ( ) {
167243 this . #test. mock ??= new MockTracker ( ) ;
168244 return this . #test. mock ;
@@ -186,6 +262,11 @@ class TestContext {
186262 loc : getCallerLocation ( ) ,
187263 } ;
188264
265+ const { plan } = this . #test;
266+ if ( plan !== null ) {
267+ plan . actual ++ ;
268+ }
269+
189270 const subtest = this . #test. createSubtest (
190271 // eslint-disable-next-line no-use-before-define
191272 Test , name , options , fn , overrides ,
@@ -240,7 +321,7 @@ class Test extends AsyncResource {
240321 super ( 'Test' ) ;
241322
242323 let { fn, name, parent, skip } = options ;
243- const { concurrency, loc, only, timeout, todo, signal } = options ;
324+ const { concurrency, loc, only, timeout, todo, signal, plan } = options ;
244325
245326 if ( typeof fn !== 'function' ) {
246327 fn = noop ;
@@ -351,6 +432,8 @@ class Test extends AsyncResource {
351432 this . fn = fn ;
352433 this . harness = null ; // Configured on the root test by the test harness.
353434 this . mock = null ;
435+ this . plan = null ;
436+ this . expectedAssertions = plan ;
354437 this . cancelled = false ;
355438 this . skipped = skip !== undefined && skip !== false ;
356439 this . isTodo = todo !== undefined && todo !== false ;
@@ -643,6 +726,11 @@ class Test extends AsyncResource {
643726
644727 const hookArgs = this . getRunArgs ( ) ;
645728 const { args, ctx } = hookArgs ;
729+
730+ if ( this . plan === null && this . expectedAssertions ) {
731+ ctx . plan ( this . expectedAssertions ) ;
732+ }
733+
646734 const after = async ( ) => {
647735 if ( this . hooks . after . length > 0 ) {
648736 await this . runHook ( 'after' , hookArgs ) ;
@@ -694,7 +782,7 @@ class Test extends AsyncResource {
694782 this . postRun ( ) ;
695783 return ;
696784 }
697-
785+ this . plan ?. check ( ) ;
698786 this . pass ( ) ;
699787 await afterEach ( ) ;
700788 await after ( ) ;
0 commit comments