33// deno-lint-ignore-file prefer-primordials
44
55( function ( ) {
6+ "use strict" ;
67const { core, primordials } = globalThis . __bootstrap ;
78const {
89 ArrayPrototypeForEach,
@@ -21,6 +22,7 @@ const {
2122 Promise,
2223 PromisePrototypeThen,
2324 ReflectApply,
25+ ReflectConstruct,
2426 SafeArrayIterator,
2527 SafeMap,
2628 String,
@@ -327,33 +329,65 @@ class NodeTestContext {
327329
328330let currentSuite = null ;
329331
332+ const rootBeforeHooks = [ ] ;
333+ const rootAfterHooks = [ ] ;
334+ const rootBeforeEachHooks = [ ] ;
335+ const rootAfterEachHooks = [ ] ;
336+ let rootBeforeRan = false ;
337+
338+ async function runRootBeforeOnce ( ) {
339+ if ( rootBeforeRan ) return ;
340+ rootBeforeRan = true ;
341+ if ( rootBeforeHooks . length === 0 ) return ;
342+ const rootCtx = { name : "<root>" , fullName : "<root>" } ;
343+ for ( const hook of new SafeArrayIterator ( rootBeforeHooks ) ) {
344+ await hook ( rootCtx ) ;
345+ }
346+ }
347+
348+ async function runRootAfterIfDone ( ) {
349+ if ( activeNodeTests !== 0 ) return ;
350+ if ( rootAfterHooks . length === 0 ) return ;
351+ const rootCtx = { name : "<root>" , fullName : "<root>" } ;
352+ // Snapshot and clear so we only run once even if more tests get queued.
353+ const hooks = ArrayPrototypeSplice ( rootAfterHooks , 0 , rootAfterHooks . length ) ;
354+ for ( const hook of new SafeArrayIterator ( hooks ) ) {
355+ try {
356+ await hook ( rootCtx ) ;
357+ } catch { /* ignore */ }
358+ }
359+ }
360+
330361class TestSuite {
331362 #denoTestContext;
363+ nodeTestContext ;
332364 entries = [ ] ;
333365 beforeAllHooks = [ ] ;
334366 afterAllHooks = [ ] ;
335367 beforeEachHooks = [ ] ;
336368 afterEachHooks = [ ] ;
337369
338- constructor ( t ) {
370+ constructor ( t , nodeTestContext ) {
339371 this . #denoTestContext = t ;
372+ this . nodeTestContext = nodeTestContext ;
340373 }
341374
342375 addTest ( name , options , fn , overrides ) {
343376 const prepared = prepareOptions ( name , options , fn , overrides ) ;
344377 const beforeEach = this . beforeEachHooks ;
345378 const afterEach = this . afterEachHooks ;
379+ const suiteNodeContext = this . nodeTestContext ;
346380 ArrayPrototypePush ( this . entries , {
347381 name : prepared . name ,
348382 fn : async ( denoTestContext ) => {
349383 const newNodeTextContext = new NodeTestContext (
350384 denoTestContext ,
351- undefined ,
385+ suiteNodeContext ,
352386 prepared . name ,
353387 ) ;
354388 try {
355389 for ( const hook of new SafeArrayIterator ( beforeEach ) ) {
356- await hook ( ) ;
390+ await hook ( newNodeTextContext ) ;
357391 }
358392 const result = await prepared . fn ( newNodeTextContext ) ;
359393 newNodeTextContext . _checkPlan ( ) ;
@@ -366,7 +400,9 @@ class TestSuite {
366400 }
367401 } finally {
368402 for ( const hook of new SafeArrayIterator ( afterEach ) ) {
369- await hook ( ) ;
403+ try {
404+ await hook ( newNodeTextContext ) ;
405+ } catch { /* ignore */ }
370406 }
371407 }
372408 } ,
@@ -377,9 +413,10 @@ class TestSuite {
377413 addSuite ( name , options , fn , overrides ) {
378414 const prepared = prepareOptions ( name , options , fn , overrides ) ;
379415 const { promise, resolve } = Promise . withResolvers ( ) ;
416+ const parentSuiteContext = this . nodeTestContext ;
380417 ArrayPrototypePush ( this . entries , {
381418 name : prepared . name ,
382- fn : wrapSuiteFn ( prepared . fn , resolve ) ,
419+ fn : wrapSuiteFn ( prepared . fn , resolve , prepared . name , parentSuiteContext ) ,
383420 ignore : ! ! prepared . options . todo || ! ! prepared . options . skip ,
384421 } ) ;
385422 return promise ;
@@ -429,7 +466,13 @@ function prepareOptions(name, options, fn, overrides) {
429466function wrapTestFn ( fn , resolve , name ) {
430467 return async function ( t ) {
431468 const nodeTestContext = new NodeTestContext ( t , undefined , name ) ;
469+ let beforeEachOk = false ;
432470 try {
471+ await runRootBeforeOnce ( ) ;
472+ for ( const hook of new SafeArrayIterator ( rootBeforeEachHooks ) ) {
473+ await hook ( nodeTestContext ) ;
474+ }
475+ beforeEachOk = true ;
433476 if ( fn . length >= 2 ) {
434477 await new Promise ( ( testResolve , testReject ) => {
435478 pendingCallbackReject = testReject ;
@@ -469,7 +512,15 @@ function wrapTestFn(fn, resolve, name) {
469512 throw sanitizeThrowValue ( err ) ;
470513 }
471514 } finally {
515+ if ( beforeEachOk ) {
516+ for ( const hook of new SafeArrayIterator ( rootAfterEachHooks ) ) {
517+ try {
518+ await hook ( nodeTestContext ) ;
519+ } catch { /* swallow to match node behavior on hook error */ }
520+ }
521+ }
472522 activeNodeTests -- ;
523+ await runRootAfterIfDone ( ) ;
473524 resolve ( ) ;
474525 }
475526 } ;
@@ -496,12 +547,15 @@ function prepareDenoTest(name, options, fn, overrides) {
496547 return promise ;
497548}
498549
499- function wrapSuiteFn ( fn , resolve ) {
550+ function wrapSuiteFn ( fn , resolve , name , parentNodeContext ) {
500551 return async function ( t ) {
552+ const isTopLevel = parentNodeContext === undefined ;
553+ if ( isTopLevel ) await runRootBeforeOnce ( ) ;
554+ const suiteNodeContext = new NodeTestContext ( t , parentNodeContext , name ) ;
501555 const prevSuite = currentSuite ;
502- const suite = currentSuite = new TestSuite ( t ) ;
556+ const suite = currentSuite = new TestSuite ( t , suiteNodeContext ) ;
503557 try {
504- fn ( ) ;
558+ fn ( suiteNodeContext ) ;
505559 } finally {
506560 currentSuite = prevSuite ;
507561 }
@@ -516,7 +570,10 @@ function wrapSuiteFn(fn, resolve) {
516570 await hook ( ) ;
517571 }
518572 } finally {
519- activeNodeTests -- ;
573+ if ( isTopLevel ) {
574+ activeNodeTests -- ;
575+ await runRootAfterIfDone ( ) ;
576+ }
520577 resolve ( ) ;
521578 }
522579 }
@@ -532,7 +589,7 @@ function prepareDenoTestForSuite(name, options, fn, overrides) {
532589
533590 const denoTestOptions = {
534591 name : prepared . name ,
535- fn : wrapSuiteFn ( prepared . fn , resolve ) ,
592+ fn : wrapSuiteFn ( prepared . fn , resolve , prepared . name , undefined ) ,
536593 only : prepared . options . only ,
537594 ignore : ! ! prepared . options . todo || ! ! prepared . options . skip ,
538595 sanitizeOnly : false ,
@@ -593,7 +650,7 @@ function before(fn, _options) {
593650 ArrayPrototypePush ( currentSuite . beforeAllHooks , fn ) ;
594651 return ;
595652 }
596- notImplemented ( "test.before (module-level, outside suite)" ) ;
653+ ArrayPrototypePush ( rootBeforeHooks , fn ) ;
597654}
598655
599656function after ( fn , _options ) {
@@ -604,7 +661,7 @@ function after(fn, _options) {
604661 ArrayPrototypePush ( currentSuite . afterAllHooks , fn ) ;
605662 return ;
606663 }
607- notImplemented ( "test.after (module-level, outside suite)" ) ;
664+ ArrayPrototypePush ( rootAfterHooks , fn ) ;
608665}
609666
610667function beforeEach ( fn , _options ) {
@@ -615,7 +672,7 @@ function beforeEach(fn, _options) {
615672 ArrayPrototypePush ( currentSuite . beforeEachHooks , fn ) ;
616673 return ;
617674 }
618- notImplemented ( "test.beforeEach (module-level, outside suite)" ) ;
675+ ArrayPrototypePush ( rootBeforeEachHooks , fn ) ;
619676}
620677
621678function afterEach ( fn , _options ) {
@@ -626,7 +683,7 @@ function afterEach(fn, _options) {
626683 ArrayPrototypePush ( currentSuite . afterEachHooks , fn ) ;
627684 return ;
628685 }
629- notImplemented ( "test.afterEach (module-level, outside suite)" ) ;
686+ ArrayPrototypePush ( rootAfterEachHooks , fn ) ;
630687}
631688
632689test . it = test ;
@@ -679,23 +736,26 @@ class MockFunctionContext {
679736 this . #restore( ) ;
680737 this . #restore = undefined ;
681738 }
739+ this . _restored = true ;
682740 const idx = ArrayPrototypeIndexOf ( activeMocks , this ) ;
683741 if ( idx !== - 1 ) {
684742 ArrayPrototypeSplice ( activeMocks , idx , 1 ) ;
685743 }
686744 }
687745
688- _recordCall ( thisArg , args , result , error ) {
746+ _recordCall ( thisArg , args , result , error , target ) {
689747 ArrayPrototypePush ( this . #calls, {
690748 arguments : args ,
691749 error,
692750 result,
693751 stack : new Error ( ) ,
752+ target,
694753 this : thisArg ,
695754 } ) ;
696755 }
697756
698757 _shouldMock ( ) {
758+ if ( this . _restored ) return false ;
699759 if ( this . #times === undefined ) return true ;
700760 return this . #calls. length < this . #times;
701761 }
@@ -717,22 +777,47 @@ class MockFunctionContext {
717777
718778function createMockFunction ( original , implementation , ctx ) {
719779 const mockFn = function ( ...args ) {
780+ const newTarget = new . target ;
781+ const isCtor = newTarget !== undefined ;
782+ // The IIFE wrapping this module is sloppy, so a plain call leaks
783+ // globalThis as `this`. Match strict-mode/Node semantics.
784+ const thisArg = ! isCtor && this === globalThis ? undefined : this ;
720785 const impl = ctx . _shouldMock ( )
721786 ? ( ctx . _nextImpl ( ) ?? implementation ?? original )
722787 : original ;
723788
724789 let result ;
725790 let error ;
726791
792+ // If called directly (not via subclass), use the original constructor
793+ // so the produced instance has its prototype, and so call.target reports
794+ // the user's class (not the mock wrapper).
795+ const ctorTarget = isCtor && newTarget === mockFn ? impl : newTarget ;
727796 try {
728- result = impl ? ReflectApply ( impl , this , args ) : undefined ;
797+ if ( isCtor ) {
798+ result = impl ? ReflectConstruct ( impl , args , ctorTarget ) : undefined ;
799+ } else {
800+ result = impl ? ReflectApply ( impl , thisArg , args ) : undefined ;
801+ }
729802 } catch ( e ) {
730803 error = e ;
731- ctx . _recordCall ( this , args , undefined , error ) ;
804+ ctx . _recordCall (
805+ isCtor ? thisArg : thisArg ,
806+ args ,
807+ undefined ,
808+ error ,
809+ ctorTarget ,
810+ ) ;
732811 throw e ;
733812 }
734813
735- ctx . _recordCall ( this , args , result ) ;
814+ ctx . _recordCall (
815+ isCtor ? result : thisArg ,
816+ args ,
817+ result ,
818+ undefined ,
819+ ctorTarget ,
820+ ) ;
736821 return result ;
737822 } ;
738823
@@ -907,6 +992,11 @@ const mock = {
907992
908993test . test = test ;
909994test . mock = mock ;
995+ test . before = before ;
996+ test . after = after ;
997+ test . beforeEach = beforeEach ;
998+ test . afterEach = afterEach ;
999+ test . run = run ;
9101000
9111001return {
9121002 run,
0 commit comments