Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80629 views
1
/**
2
* Copyright (c) 2014, Facebook, Inc. All rights reserved.
3
*
4
* This source code is licensed under the BSD-style license found in the
5
* LICENSE file in the root directory of this source tree. An additional grant
6
* of patent rights can be found in the PATENTS file in the same directory.
7
*/
8
// This module uses the Function constructor, so it can't currently run
9
// in strict mode
10
/* jshint strict:false */
11
12
function isA(typeName, value) {
13
return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
14
}
15
16
function getType(ref) {
17
if (isA('RegExp', ref)) {
18
return 'regexp';
19
}
20
21
if (isA('Array', ref)) {
22
return 'array';
23
}
24
25
if (isA('Function', ref)) {
26
return 'function';
27
}
28
29
if (isA('Object', ref)) {
30
return 'object';
31
}
32
33
// consider number and string fields to be constants that we want to
34
// pick up as they are
35
if (isA('Number', ref) || isA('String', ref)) {
36
return 'constant';
37
}
38
39
if (ref === undefined) {
40
return 'undefined';
41
}
42
43
if (ref === null) {
44
return 'null';
45
}
46
47
return null;
48
}
49
50
/**
51
* methods on ES6 classes are not enumerable, so they can't be found with a
52
* simple `for (var slot in ...) {` so that had to be replaced with getSlots()
53
*/
54
function getSlots(object) {
55
var slots = {};
56
if (!object) {
57
return [];
58
}
59
// Simply attempting to access any of these throws an error.
60
var forbiddenProps = [ 'caller', 'callee', 'arguments' ];
61
var collectProp = function(prop) {
62
if (forbiddenProps.indexOf(prop) === -1) {
63
slots[prop] = true;
64
}
65
};
66
do {
67
Object.getOwnPropertyNames(object).forEach(collectProp);
68
object = Object.getPrototypeOf(object);
69
} while (object && Object.getPrototypeOf(object) !== null);
70
return Object.keys(slots);
71
}
72
73
function makeComponent(metadata) {
74
switch (metadata.type) {
75
case 'object':
76
return {};
77
78
case 'array':
79
return [];
80
81
case 'regexp':
82
return new RegExp();
83
84
case 'constant':
85
case 'null':
86
case 'undefined':
87
return metadata.value;
88
89
case 'function':
90
var defaultReturnValue;
91
var specificReturnValues = [];
92
var mockImpl;
93
var isReturnValueLastSet = false;
94
var calls = [];
95
var instances = [];
96
var prototype =
97
(metadata.members && metadata.members.prototype &&
98
metadata.members.prototype.members) || {};
99
100
var mockConstructor = function() {
101
instances.push(this);
102
calls.push(Array.prototype.slice.call(arguments));
103
if (this instanceof f) {
104
// This is probably being called as a constructor
105
getSlots(prototype).forEach(function(slot) {
106
// Copy prototype methods to the instance to make
107
// it easier to interact with mock instance call and
108
// return values
109
if (prototype[slot].type === 'function') {
110
var protoImpl = this[slot];
111
this[slot] = generateFromMetadata(prototype[slot]);
112
this[slot]._protoImpl = protoImpl;
113
}
114
}, this);
115
116
// Run the mock constructor implementation
117
return mockImpl && mockImpl.apply(this, arguments);
118
}
119
120
var returnValue;
121
// If return value is last set, either specific or default, i.e.
122
// mockReturnValueOnce()/mockReturnValue() is called and no
123
// mockImplementation() is called after that.
124
// use the set return value.
125
if (isReturnValueLastSet) {
126
returnValue = specificReturnValues.shift();
127
if (returnValue === undefined) {
128
returnValue = defaultReturnValue;
129
}
130
}
131
132
// If mockImplementation() is last set, or specific return values
133
// are used up, use the mock implementation.
134
if (mockImpl && returnValue === undefined) {
135
return mockImpl.apply(this, arguments);
136
}
137
138
// Otherwise use prototype implementation
139
if (returnValue === undefined && f._protoImpl) {
140
return f._protoImpl.apply(this, arguments);
141
}
142
143
return returnValue;
144
};
145
146
// Preserve `name` property of mocked function.
147
/* jshint evil:true */
148
var f = new Function(
149
'mockConstructor',
150
'return function ' + metadata.name + '(){' +
151
'return mockConstructor.apply(this,arguments);' +
152
'}'
153
)(mockConstructor);
154
155
f._isMockFunction = true;
156
157
f.mock = {
158
calls: calls,
159
instances: instances
160
};
161
162
f.mockClear = function() {
163
calls.length = 0;
164
instances.length = 0;
165
};
166
167
f.mockReturnValueOnce = function(value) {
168
// next function call will return this value or default return value
169
isReturnValueLastSet = true;
170
specificReturnValues.push(value);
171
return f;
172
};
173
174
f.mockReturnValue = function(value) {
175
// next function call will return specified return value or this one
176
isReturnValueLastSet = true;
177
defaultReturnValue = value;
178
return f;
179
};
180
181
f.mockImplementation = f.mockImpl = function(fn) {
182
// next function call will use mock implementation return value
183
isReturnValueLastSet = false;
184
mockImpl = fn;
185
return f;
186
};
187
188
f.mockReturnThis = function() {
189
return f.mockImplementation(function() {
190
return this;
191
});
192
};
193
194
f._getMockImplementation = function() {
195
return mockImpl;
196
};
197
198
if (metadata.mockImpl) {
199
f.mockImplementation(metadata.mockImpl);
200
}
201
202
return f;
203
}
204
205
throw new Error('Unrecognized type ' + metadata.type);
206
}
207
208
function generateFromMetadata(_metadata) {
209
var callbacks = [];
210
var refs = {};
211
212
function generateMock(metadata) {
213
var mock = makeComponent(metadata);
214
if (metadata.refID !== null && metadata.refID !== undefined) {
215
refs[metadata.refID] = mock;
216
}
217
218
function getRefCallback(slot, ref) {
219
return function() {
220
mock[slot] = refs[ref];
221
};
222
}
223
224
if (metadata.__TCmeta) {
225
mock.__TCmeta = metadata.__TCmeta;
226
}
227
228
getSlots(metadata.members).forEach(function(slot) {
229
var slotMetadata = metadata.members[slot];
230
if (slotMetadata.ref !== null && slotMetadata.ref !== undefined) {
231
callbacks.push(getRefCallback(slot, slotMetadata.ref));
232
} else {
233
mock[slot] = generateMock(slotMetadata);
234
}
235
});
236
237
if (metadata.type !== 'undefined'
238
&& metadata.type !== 'null'
239
&& mock.prototype) {
240
mock.prototype.constructor = mock;
241
}
242
243
return mock;
244
}
245
246
var mock = generateMock(_metadata);
247
callbacks.forEach(function(setter) {
248
setter();
249
});
250
251
return mock;
252
}
253
254
function _getMetadata(component, _refs) {
255
var refs = _refs || [];
256
257
// This is a potential performance drain, since the whole list is scanned
258
// for every component
259
var ref = refs.indexOf(component);
260
if (ref > -1) {
261
return {ref: ref};
262
}
263
264
var type = getType(component);
265
if (!type) {
266
return null;
267
}
268
269
var metadata = {type : type};
270
if (type === 'constant'
271
|| type === 'undefined'
272
|| type === 'null') {
273
metadata.value = component;
274
return metadata;
275
} else if (type === 'function') {
276
metadata.name = component.name;
277
metadata.__TCmeta = component.__TCmeta;
278
if (component._isMockFunction) {
279
metadata.mockImpl = component._getMockImplementation();
280
}
281
}
282
283
metadata.refID = refs.length;
284
refs.push(component);
285
286
var members = null;
287
288
function addMember(slot, data) {
289
if (!data) {
290
return;
291
}
292
if (!members) {
293
members = {};
294
}
295
members[slot] = data;
296
}
297
298
// Leave arrays alone
299
if (type !== 'array') {
300
if (type !== 'undefined') {
301
getSlots(component).forEach(function(slot) {
302
if (slot.charAt(0) === '_' ||
303
(type === 'function' && component._isMockFunction &&
304
slot.match(/^mock/))) {
305
return;
306
}
307
308
if (!component.hasOwnProperty && component[slot] !== undefined ||
309
component.hasOwnProperty(slot) ||
310
/* jshint eqeqeq:false */
311
(type === 'object' && component[slot] != Object.prototype[slot])) {
312
addMember(slot, _getMetadata(component[slot], refs));
313
}
314
});
315
}
316
317
// If component is native code function, prototype might be undefined
318
if (type === 'function' && component.prototype) {
319
var prototype = _getMetadata(component.prototype, refs);
320
if (prototype && prototype.members) {
321
addMember('prototype', prototype);
322
}
323
}
324
}
325
326
if (members) {
327
metadata.members = members;
328
}
329
330
return metadata;
331
}
332
333
function removeUnusedRefs(metadata) {
334
function visit(metadata, f) {
335
f(metadata);
336
if (metadata.members) {
337
getSlots(metadata.members).forEach(function(slot) {
338
visit(metadata.members[slot], f);
339
});
340
}
341
}
342
343
var usedRefs = {};
344
visit(metadata, function(metadata) {
345
if (metadata.ref !== null && metadata.ref !== undefined) {
346
usedRefs[metadata.ref] = true;
347
}
348
});
349
350
visit(metadata, function(metadata) {
351
if (!usedRefs[metadata.refID]) {
352
delete metadata.refID;
353
}
354
});
355
}
356
357
module.exports = {
358
/**
359
* Generates a mock based on the given metadata. Mocks treat functions
360
* specially, and all mock functions have additional members, described in the
361
* documentation for getMockFunction in this module.
362
*
363
* One important note: function prototoypes are handled specially by this
364
* mocking framework. For functions with prototypes, when called as a
365
* constructor, the mock will install mocked function members on the instance.
366
* This allows different instances of the same constructor to have different
367
* values for its mocks member and its return values.
368
*
369
* @param metadata Metadata for the mock in the schema returned by the
370
* getMetadata method of this module.
371
*
372
*/
373
generateFromMetadata: generateFromMetadata,
374
375
/**
376
* Inspects the argument and returns its schema in the following recursive
377
* format:
378
* {
379
* type: ...
380
* members : {}
381
* }
382
*
383
* Where type is one of 'array', 'object', 'function', or 'ref', and members
384
* is an optional dictionary where the keys are member names and the values
385
* are metadata objects. Function prototypes are defined simply by defining
386
* metadata for the member.prototype of the function. The type of a function
387
* prototype should always be "object". For instance, a simple class might be
388
* defined like this:
389
*
390
* {
391
* type: 'function',
392
* members: {
393
* staticMethod: {type: 'function'},
394
* prototype: {
395
* type: 'object',
396
* members: {
397
* instanceMethod: {type: 'function'}
398
* }
399
* }
400
* }
401
* }
402
*
403
* Metadata may also contain references to other objects defined within the
404
* same metadata object. The metadata for the referent must be marked with
405
* 'refID' key and an arbitrary value. The referer must be marked with a
406
* 'ref' key that has the same value as object with refID that it refers to.
407
* For instance, this metadata blob:
408
* {
409
* type: 'object',
410
* refID: 1,
411
* members: {
412
* self: {ref: 1}
413
* }
414
* }
415
*
416
* defines an object with a slot named 'self' that refers back to the object.
417
*
418
* @param component The component for which to retrieve metadata.
419
*/
420
getMetadata: function(component) {
421
var metadata = _getMetadata(component);
422
// to make it easier to work with mock metadata, only preserve references
423
// that are actually used
424
if (metadata !== null) {
425
removeUnusedRefs(metadata);
426
}
427
return metadata;
428
},
429
430
/**
431
* Generates a stand-alone function with members that help drive unit tests or
432
* confirm expectations. Specifically, functions returned by this method have
433
* the following members:
434
*
435
* .mock:
436
* An object with two members, "calls", and "instances", which are both
437
* lists. The items in the "calls" list are the arguments with which the
438
* function was called. The "instances" list stores the value of 'this' for
439
* each call to the function. This is useful for retrieving instances from a
440
* constructor.
441
*
442
* .mockReturnValueOnce(value)
443
* Pushes the given value onto a FIFO queue of return values for the
444
* function.
445
*
446
* .mockReturnValue(value)
447
* Sets the default return value for the function.
448
*
449
* .mockImplementation(function)
450
* Sets a mock implementation for the function.
451
*
452
* .mockReturnThis()
453
* Syntactic sugar for .mockImplementation(function() {return this;})
454
*
455
* In case both mockImplementation() and
456
* mockReturnValueOnce()/mockReturnValue() are called. The priority of
457
* which to use is based on what is the last call:
458
* - if the last call is mockReturnValueOnce() or mockReturnValue(),
459
* use the specific return specific return value or default return value.
460
* If specific return values are used up or no default return value is set,
461
* fall back to try mockImplementation();
462
* - if the last call is mockImplementation(), run the given implementation
463
* and return the result.
464
*/
465
getMockFunction: function() {
466
return makeComponent({type: 'function'});
467
},
468
469
// Just a short-hand alias
470
getMockFn: function() {
471
return this.getMockFunction();
472
}
473
};
474
475