1
1
// An initial implementation for a feature that will hopefully exist in tap
2
2
// https://github.com/tapjs/node-tap/issues/789
3
- // This file is only used in tests but it is still tested itself. There's
4
- // a lot going on in this file, but hopefully it can be removed from a
5
- // feature in tap in the future
3
+ // This file is only used in tests but it is still tested itself.
4
+ // Hopefully it can be removed for a feature in tap in the future
6
5
6
+ // Path can be different cases across platform so get the original case
7
+ // of the path before anything is changed
7
8
const originalPathKey = process . env . PATH ? 'PATH' : process . env . Path ? 'Path' : 'path'
9
+
8
10
const last = ( arr ) => arr [ arr . length - 1 ]
9
11
const has = ( obj , key ) => Object . prototype . hasOwnProperty . call ( obj , key )
10
-
11
- // Get lineage of all object references for a path on `global`.
12
- // So `process.env.NODE_ENV` would return
13
- // [global, global.process, global.process.env, 'production']
14
- const getGlobalAncestors = ( keys ) =>
15
- keys . split ( '.' ) . reduce ( ( acc , k ) => {
16
- const value = last ( acc ) [ k ]
17
- acc . push ( value )
18
- return acc
19
- } , [ global ] )
12
+ const splitOnLast = ( str ) => {
13
+ const index = str . lastIndexOf ( '.' )
14
+ return index > - 1 && [ str . slice ( 0 , index ) , str . slice ( index + 1 ) ]
15
+ }
20
16
21
17
// A weird getter that can look up keys on nested objects but also
22
18
// match keys with dots in their names, eg { 'process.env': { TERM: 'a' } }
@@ -25,16 +21,24 @@ const get = (obj, fullKey, childKey) => {
25
21
if ( has ( obj , fullKey ) ) {
26
22
return childKey ? get ( obj [ fullKey ] , childKey ) : obj [ fullKey ]
27
23
} else {
28
- const lastDot = fullKey . lastIndexOf ( '.' )
29
- return lastDot === - 1 ? undefined : get (
24
+ const split = splitOnLast ( fullKey )
25
+ return split ? get (
30
26
obj ,
31
- fullKey . slice ( 0 , lastDot ) ,
32
- fullKey . slice ( lastDot + 1 ) + ( childKey ? `.${ childKey } ` : '' )
33
- )
27
+ split [ 0 ] ,
28
+ split [ 1 ] + ( childKey ? `.${ childKey } ` : '' )
29
+ ) : undefined
34
30
}
35
31
}
36
32
37
- // { a: 1, b: { c: 2 } } => ['a', 'b.c']
33
+ // Get object reference for the parent of a full key path on `global`
34
+ // So `process.env.NODE_ENV` would return a reference to global.process.env
35
+ const getGlobalParent = ( fullKey ) => {
36
+ const split = splitOnLast ( fullKey )
37
+ return split ? get ( global , split [ 0 ] ) : global
38
+ }
39
+
40
+ // Map an object to an array of nested keys separated by dots
41
+ // { a: 1, b: { c: 2, d: [1] } } => ['a', 'b.c', 'b.d']
38
42
const getKeys = ( values , p = '' , acc = [ ] ) =>
39
43
Object . entries ( values ) . reduce ( ( memo , [ k , value ] ) => {
40
44
const key = p ? `${ p } .${ k } ` : k
@@ -45,18 +49,11 @@ const getKeys = (values, p = '', acc = []) =>
45
49
46
50
// Walk prototype chain to get first available descriptor. This is necessary
47
51
// to get the current property descriptor for things like `process.on`.
48
- // `Object.getOwnPropertyDescriptor (process, 'on') === undefined` but if you
52
+ // Since `getOPD (process, 'on') === undefined` but if you
49
53
// walk up the prototype chain you get the original descriptor
50
- // `Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Object.getPrototypeOf(process)), 'on')`
51
- // {
52
- // value: [Function: addListener],
53
- // writable: true,
54
- // enumerable: true,
55
- // configurable: true
56
- // }
54
+ // `getOPD(getPO(getPO(process)), 'on') === { value: [Function], ... }`
57
55
const getPropertyDescriptor = ( obj , key , fullKey ) => {
58
56
if ( fullKey . toUpperCase ( ) === 'PROCESS.ENV.PATH' ) {
59
- // if getting original env.path value, use cross platform compatible key
60
57
key = originalPathKey
61
58
}
62
59
let d = Object . getOwnPropertyDescriptor ( obj , key )
@@ -70,99 +67,114 @@ const getPropertyDescriptor = (obj, key, fullKey) => {
70
67
return d
71
68
}
72
69
70
+ const createDescriptor = ( currentDescriptor = {
71
+ configurable : true ,
72
+ writable : true ,
73
+ enumerable : true ,
74
+ } , value ) => {
75
+ if ( value === undefined ) {
76
+ // Mocking a global to undefined is the same
77
+ // as deleting it so return early since no
78
+ // descriptor will be created
79
+ return value
80
+ }
81
+ // Either set the descriptor value or getter depending
82
+ // on what the current descriptor has
83
+ return {
84
+ ...currentDescriptor ,
85
+ ...( currentDescriptor . get ? { get : ( ) => value } : { value } ) ,
86
+ }
87
+ }
88
+
89
+ // Define a descriptor or delete a key on an object
90
+ const defineProperty = ( obj , key , descriptor ) => {
91
+ if ( descriptor === undefined ) {
92
+ delete obj [ key ]
93
+ } else {
94
+ Object . defineProperty ( obj , key , descriptor )
95
+ }
96
+ }
97
+
73
98
const _pushDescriptor = Symbol ( 'pushDescriptor' )
74
99
const _popDescriptor = Symbol ( 'popDescriptor' )
75
- const _createReset = Symbol ( 'createReset' )
76
100
const _set = Symbol ( 'set' )
77
101
78
102
class MockGlobals {
79
- #cache = new Map ( )
80
- #resets = [ ]
81
- #defaultDescriptor = {
82
- configurable : true ,
83
- writable : true ,
84
- enumerable : true ,
103
+ #skipDescriptor = Symbol ( 'skipDescriptor' )
104
+ #descriptors = {
105
+ // [fullKey]: [descriptor, descriptor, ...]
85
106
}
86
107
87
108
teardown ( ) {
88
- this . #resets. forEach ( r => r . reset ( true ) )
109
+ Object . entries ( this . #descriptors)
110
+ . forEach ( ( [ fullKey , descriptors ] ) => {
111
+ defineProperty (
112
+ getGlobalParent ( fullKey ) ,
113
+ last ( fullKey . split ( '.' ) ) ,
114
+ // On teardown reset to the initial descriptor
115
+ descriptors [ 0 ]
116
+ )
117
+ } )
89
118
}
90
119
91
120
registerGlobals ( globals , { replace = false } = { } ) {
121
+ // Replace means dont merge in object values but replace them instead
92
122
const keys = replace ? Object . keys ( globals ) : getKeys ( globals )
93
- const resets = keys . map ( k => this [ _set ] ( k , globals ) )
94
- this . #resets. push ( ...resets )
95
- return resets . reduce ( ( acc , r ) => {
96
- acc [ r . fullKey ] = r . reset
97
- return acc
98
- } , { } )
123
+ return keys
124
+ // Set each property passed in and return fns to reset them
125
+ . map ( k => this [ _set ] ( k , globals ) )
126
+ // Return an object with each path as a key for manually
127
+ // resetting in each test
128
+ . reduce ( ( acc , r ) => {
129
+ acc [ r . fullKey ] = r . reset
130
+ return acc
131
+ } , { } )
99
132
}
100
133
101
- [ _pushDescriptor ] ( key , value ) {
102
- const cache = this . #cache. get ( key )
103
- if ( cache ) {
104
- this . #cache. get ( key ) . push ( value )
105
- } else {
106
- this . #cache. set ( key , [ value ] )
134
+ [ _pushDescriptor ] ( fullKey , value ) {
135
+ if ( ! this . #descriptors[ fullKey ] ) {
136
+ this . #descriptors[ fullKey ] = [ ]
107
137
}
108
- return value
138
+ this . #descriptors [ fullKey ] . push ( value )
109
139
}
110
140
111
- [ _popDescriptor ] ( key ) {
112
- const cache = this . #cache. get ( key )
113
- if ( ! cache ) {
114
- return null
115
- }
116
- const value = cache . pop ( )
117
- if ( ! cache . length ) {
118
- this . #cache. delete ( key )
141
+ [ _popDescriptor ] ( fullKey ) {
142
+ const descriptors = this . #descriptors[ fullKey ]
143
+ if ( ! descriptors ) {
144
+ return this . #skipDescriptor
119
145
}
120
- return value
121
- }
122
-
123
- [ _createReset ] ( parent , key , fullKey ) {
124
- return {
125
- fullKey,
126
- key,
127
- reset : ( ) => {
128
- const popped = this [ _popDescriptor ] ( fullKey )
129
- // undefined means delete the property so only skip
130
- // if it is explicitly null
131
- if ( popped === null ) {
132
- return
133
- }
134
- return popped
135
- ? Object . defineProperty ( parent , key , popped )
136
- : ( delete parent [ key ] )
137
- } ,
146
+ const descriptor = descriptors . pop ( )
147
+ if ( ! descriptors . length ) {
148
+ delete this . #descriptors[ fullKey ]
138
149
}
150
+ return descriptor
139
151
}
140
152
141
153
[ _set ] ( fullKey , globals ) {
142
- const values = getGlobalAncestors ( fullKey )
143
- const parentValue = values [ values . length - 2 ]
144
-
154
+ const obj = getGlobalParent ( fullKey )
145
155
const key = last ( fullKey . split ( '.' ) )
146
- const newValue = get ( globals , fullKey )
147
156
148
- const currentDescriptor = getPropertyDescriptor ( parentValue , key , fullKey )
157
+ const currentDescriptor = getPropertyDescriptor ( obj , key , fullKey )
149
158
this [ _pushDescriptor ] ( fullKey , currentDescriptor )
150
159
151
- const reset = this [ _createReset ] ( parentValue , key , fullKey )
152
-
153
- if ( newValue === undefined ) {
154
- delete parentValue [ key ]
155
- } else {
156
- const newDescriptor = { ...( currentDescriptor || this . #defaultDescriptor) }
157
- if ( newDescriptor . get ) {
158
- newDescriptor . get = ( ) => newValue
159
- } else {
160
- newDescriptor . value = newValue
161
- }
162
- Object . defineProperty ( parentValue , key , newDescriptor )
163
- }
160
+ defineProperty (
161
+ obj ,
162
+ key ,
163
+ createDescriptor (
164
+ currentDescriptor ,
165
+ get ( globals , fullKey )
166
+ )
167
+ )
164
168
165
- return reset
169
+ return {
170
+ fullKey,
171
+ reset : ( ) => {
172
+ const lastDescriptor = this [ _popDescriptor ] ( fullKey )
173
+ if ( lastDescriptor !== this . #skipDescriptor) {
174
+ defineProperty ( obj , key , lastDescriptor )
175
+ }
176
+ } ,
177
+ }
166
178
}
167
179
}
168
180
@@ -173,15 +185,18 @@ const cache = new Map()
173
185
const mockGlobals = ( t , globals , options ) => {
174
186
const hasInstance = cache . has ( t )
175
187
const instance = hasInstance ? cache . get ( t ) : new MockGlobals ( )
176
- const reset = instance . registerGlobals ( globals , options )
188
+
177
189
if ( ! hasInstance ) {
178
190
cache . set ( t , instance )
179
191
t . teardown ( ( ) => {
180
192
instance . teardown ( )
181
193
cache . delete ( t )
182
194
} )
183
195
}
184
- return { reset }
196
+
197
+ return {
198
+ reset : instance . registerGlobals ( globals , options ) ,
199
+ }
185
200
}
186
201
187
202
module . exports = mockGlobals
0 commit comments