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