1- import { inspect , InspectOptions } from 'util'
1+ import { inspect , InspectOptions , types } from 'util'
22
3- import { PropertyType , isSubstitutionMethod , isAssertionMethod , AssertionMethod , SubstitutionMethod , textModifier , ConfigurationMethod , isSubstituteMethod } from './Utilities'
4- import { SubstituteException } from './SubstituteException'
5- import { RecordedArguments } from './RecordedArguments'
63import { SubstituteNodeBase } from './SubstituteNodeBase'
7- import { SubstituteBase } from './SubstituteBase'
8- import { createSubstituteProxy } from './SubstituteProxy'
9- import { ClearType } from './Transformations'
4+ import { RecordedArguments } from './RecordedArguments'
5+ import { ClearType as ClearTypeMap , PropertyType as PropertyTypeMap , isAssertionMethod , isSubstituteMethod , isSubstitutionMethod , textModifier } from './Utilities'
6+ import { SubstituteException } from './SubstituteException'
7+ import type { FilterFunction , SubstituteContext , SubstitutionMethod , ClearType , PropertyType } from './Types'
8+
9+ const instance = Symbol ( 'Substitute:Instance' )
10+ type SpecialProperty = typeof instance | typeof inspect . custom | 'then'
11+ type RootContext = { substituteMethodsEnabled : boolean }
1012
11- type SubstituteContext = SubstitutionMethod | AssertionMethod | ConfigurationMethod | 'none'
12- const clearTypeToFilterMap : Record < ClearType , ( node : SubstituteNode ) => boolean > = {
13+ const clearTypeToFilterMap : Record < ClearType , FilterFunction < SubstituteNode > > = {
1314 all : ( ) => true ,
1415 receivedCalls : node => ! node . hasContext ,
1516 substituteValues : node => node . isSubstitution
1617}
1718
18- export class SubstituteNode extends SubstituteNodeBase < SubstituteNode > {
19+ export class SubstituteNode extends SubstituteNodeBase {
1920 private _proxy : SubstituteNode
20- private _propertyType : PropertyType = PropertyType . property
21+ private _rootContext : RootContext
22+
23+ private _propertyType : PropertyType = PropertyTypeMap . Property
2124 private _accessorType : 'get' | 'set' = 'get'
2225 private _recordedArguments : RecordedArguments = RecordedArguments . none ( )
2326
2427 private _context : SubstituteContext = 'none'
25- private _disabledAssertions : boolean = false
28+ private _disabledSubstituteMethods : boolean = false
2629
27- constructor ( property : PropertyKey , parent : SubstituteNode | SubstituteBase ) {
28- super ( property , parent )
29- this . _proxy = createSubstituteProxy (
30+ private constructor ( key : PropertyKey , parent ?: SubstituteNode ) {
31+ super ( key , parent )
32+ if ( this . isRoot ( ) ) this . _rootContext = { substituteMethodsEnabled : true }
33+ if ( this . isIntermediateNode ( ) ) this . _rootContext = this . root . rootContext
34+ this . _proxy = new Proxy (
3035 this ,
3136 {
32- get : ( node , _ , __ , nextNode ) => {
33- if ( node . isAssertion ) nextNode . executeAssertion ( )
37+ get : function ( target , property ) {
38+ if ( target . isSpecialProperty ( property ) ) return target . evaluateSpecialProperty ( property )
39+ const newNode = SubstituteNode . createChild ( property , target )
40+ if ( target . isRoot ( ) && ! target . rootContext . substituteMethodsEnabled ) newNode . disableSubstituteMethods ( )
41+ if ( target . isIntermediateNode ( ) && target . isAssertion ) newNode . executeAssertion ( )
42+ return newNode . read ( )
3443 } ,
35- set : ( node , _ , __ , ___ , nextNode ) => {
36- if ( node . isAssertion ) nextNode . executeAssertion ( )
44+ set : function ( target , property , value ) {
45+ const newNode = SubstituteNode . createChild ( property , target )
46+ newNode . write ( value )
47+ if ( target . isAssertion ) newNode . executeAssertion ( )
48+ return true
3749 } ,
38- apply : ( node , _ , rawArguments ) => {
39- node . handleMethod ( rawArguments )
40- if ( node . context === 'clearSubstitute' ) return node . clear ( )
41- return node . parent ?. isAssertion ?? false ? node . executeAssertion ( ) : node . read ( )
50+ apply : function ( target , _thisArg , rawArguments ) {
51+ target . handleMethod ( rawArguments )
52+ if ( target . hasContext ) target . handleSpecialContext ( )
53+ return ( target . parent ?. isAssertion ?? false ) ? target . executeAssertion ( ) : target . read ( )
4254 }
4355 }
4456 )
4557 }
4658
59+ public static instance : typeof instance = instance
60+
61+ public static createRoot ( ) : SubstituteNode {
62+ return new this ( '*Substitute<Root>' )
63+ }
64+
65+ public static createChild ( key : PropertyKey , parent : SubstituteNode ) : SubstituteNode {
66+ return new this ( key , parent )
67+ }
68+
4769 public get proxy ( ) {
4870 return this . _proxy
4971 }
5072
73+ public get rootContext ( ) {
74+ return this . _rootContext
75+ }
76+
5177 get context ( ) : SubstituteContext {
5278 return this . _context
5379 }
@@ -80,20 +106,20 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
80106 return this . _recordedArguments
81107 }
82108
83- public get disabledAssertions ( ) {
84- return this . _disabledAssertions
109+ public get disabledSubstituteMethods ( ) {
110+ return this . _disabledSubstituteMethods
85111 }
86112
87113 public assignContext ( context : SubstituteContext ) : void {
88114 this . _context = context
89115 }
90116
91- public disableAssertions ( ) {
92- this . _disabledAssertions = true
117+ public disableSubstituteMethods ( ) {
118+ this . _disabledSubstituteMethods = true
93119 }
94120
95121 public read ( ) : SubstituteNode | void | never {
96- if ( this . parent ?. isSubstitution ?? false ) return
122+ if ( ( this . parent ?. isSubstitution ?? false ) || this . context === 'clearSubstitute' ) return
97123 if ( this . isAssertion ) return this . proxy
98124
99125 const mostSuitableSubstitution = this . getMostSuitableSubstitution ( )
@@ -108,9 +134,9 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
108134 }
109135
110136 public clear ( ) {
111- const clearType : ClearType = this . recordedArguments . value [ 0 ] ?? 'all'
112- const filter = clearTypeToFilterMap [ clearType ] as ( node : SubstituteNodeBase ) => boolean
113- this . root . recorder . clearRecords ( filter )
137+ const clearType : ClearType = this . recordedArguments . value [ 0 ] ?? ClearTypeMap . All
138+ const filter = clearTypeToFilterMap [ clearType ]
139+ this . recorder . clearRecords ( filter )
114140 }
115141
116142 public executeSubstitution ( contextArguments : RecordedArguments ) {
@@ -122,7 +148,7 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
122148 case 'throws' :
123149 throw substitutionValue
124150 case 'mimicks' :
125- const argumentsToApply = this . propertyType === PropertyType . property ? [ ] : contextArguments . value
151+ const argumentsToApply = this . propertyType === PropertyTypeMap . Property ? [ ] : contextArguments . value
126152 return substitutionValue ( ...argumentsToApply )
127153 case 'resolves' :
128154 return Promise . resolve ( substitutionValue )
@@ -136,8 +162,8 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
136162 }
137163
138164 public executeAssertion ( ) : void | never {
139- const siblings = [ ...this . getAllSiblings ( ) . filter ( n => ! n . hasContext && n . accessorType === this . accessorType ) ]
140165 if ( ! this . isIntermediateNode ( ) ) throw new Error ( 'Not possible' )
166+ const siblings = [ ...this . getAllSiblings ( ) . filter ( n => ! n . hasContext && n . accessorType === this . accessorType ) ]
141167
142168 const expectedCount = this . parent . recordedArguments . value [ 0 ] ?? undefined
143169 const finiteExpectation = expectedCount !== undefined
@@ -168,14 +194,20 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
168194 }
169195
170196 public handleMethod ( rawArguments : any [ ] ) : void {
171- this . _propertyType = PropertyType . method
197+ this . _propertyType = PropertyTypeMap . Method
172198 this . _recordedArguments = RecordedArguments . from ( rawArguments )
173- if ( ! isSubstituteMethod ( this . property ) ) return
199+ this . tryToAssignContext ( )
200+ }
174201
202+ private tryToAssignContext ( ) {
203+ if ( ! isSubstituteMethod ( this . property ) ) return
175204 if ( this . isIntermediateNode ( ) && isSubstitutionMethod ( this . property ) ) return this . parent . assignContext ( this . property )
176- if ( this . disabledAssertions || ! this . isHead ( ) ) return
177-
205+ if ( this . disabledSubstituteMethods ) return
178206 this . assignContext ( this . property )
207+ }
208+
209+ private handleSpecialContext ( ) : void {
210+ if ( this . context === 'clearSubstitute' ) return this . clear ( )
179211 if ( this . context === 'didNotReceive' ) this . _recordedArguments = RecordedArguments . from ( [ 0 ] )
180212 }
181213
@@ -188,7 +220,38 @@ export class SubstituteNode extends SubstituteNodeBase<SubstituteNode> {
188220 return sortedNodes [ 0 ]
189221 }
190222
191- protected printableForm ( _ : number , options : InspectOptions ) : string {
223+ private isSpecialProperty ( property : PropertyKey ) : property is SpecialProperty {
224+ return property === SubstituteNode . instance || property === inspect . custom || property === 'then'
225+ }
226+
227+ private evaluateSpecialProperty ( property : SpecialProperty ) {
228+ switch ( property ) {
229+ case SubstituteNode . instance :
230+ return this
231+ case inspect . custom :
232+ return this . printableForm . bind ( this )
233+ case 'then' :
234+ return
235+ default :
236+ throw SubstituteException . generic ( `Evaluation of special property ${ property } is not implemented` )
237+ }
238+ }
239+
240+ public [ inspect . custom ] ( ...args : [ _ : number , options : InspectOptions ] ) : string {
241+ return types . isProxy ( this ) ? this [ inspect . custom ] ( ...args ) : this . printableForm ( ...args )
242+ }
243+
244+ private printableForm ( _ : number , options : InspectOptions ) : string {
245+ return this . isRoot ( ) ? this . printRootNode ( options ) : this . printNode ( options )
246+ }
247+
248+ private printRootNode ( options : InspectOptions ) : string {
249+ const records = inspect ( this . recorder , options )
250+ const instanceName = '*Substitute<Root>' // Substitute<FooThing>
251+ return instanceName + ' {' + records + '\n}'
252+ }
253+
254+ private printNode ( options : InspectOptions ) : string {
192255 const hasContext = this . hasContext
193256 const args = inspect ( this . recordedArguments , options )
194257 const label = this . isSubstitution
0 commit comments