UNPKG

14.6 kBJavaScriptView Raw
1(function setupNgDescribe(root) {
2 // check - kensho/check-more-types
3 // la - bahmutov/lazy-ass
4 la(check.object(root), 'missing root');
5
6 var _defaults = {
7 // primary options
8 name: 'default tests',
9 modules: [],
10 configs: {},
11 inject: [],
12 exposeApi: false,
13 tests: function () {},
14 mocks: {},
15 helpful: false,
16 controllers: [],
17 element: '',
18 http: {},
19 // secondary options
20 only: false,
21 verbose: false,
22 skip: false,
23 parentScope: {}
24 };
25
26 function defaults(opts) {
27 opts = opts || {};
28 return angular.extend(angular.copy(_defaults), opts);
29 }
30
31 var ngDescribeSchema = {
32 // primary options
33 name: check.unemptyString,
34 modules: check.arrayOfStrings,
35 configs: check.object,
36 inject: check.arrayOfStrings,
37 exposeApi: check.bool,
38 tests: check.fn,
39 mocks: check.object,
40 helpful: check.bool,
41 controllers: check.arrayOfStrings,
42 element: check.string,
43 // TODO allow object OR function
44 // http: check.object,
45 // secondary options
46 only: check.bool,
47 verbose: check.bool,
48 skip: check.or(check.bool, check.unemptyString),
49 parentScope: check.object
50 };
51
52 function uniq(a) {
53 var seen = {};
54 return a.filter(function(item) {
55 return seen.hasOwnProperty(item) ? false : (seen[item] = true);
56 });
57 }
58
59 function clone(a) {
60 return JSON.parse(JSON.stringify(a));
61 }
62
63 function methodNames(reference) {
64 la(check.object(reference), 'expected object reference, not', reference);
65
66 return Object.keys(reference).filter(function (key) {
67 return check.fn(reference[key]);
68 });
69 }
70
71 function copyAliases(options) {
72 if (options.config && !options.configs) {
73 options.configs = options.config;
74 }
75 if (options.mock && !options.mocks) {
76 options.mocks = options.mock;
77 }
78 if (options.module && !options.modules) {
79 options.modules = options.module;
80 }
81 if (options.test && !options.tests) {
82 options.tests = options.test;
83 }
84 if (options.controller && !options.controllers) {
85 options.controllers = options.controller;
86 }
87 return options;
88 }
89
90 function ensureArrays(options) {
91 if (check.string(options.modules)) {
92 options.modules = [options.modules];
93 }
94 if (check.string(options.inject)) {
95 options.inject = [options.inject];
96 }
97 if (check.string(options.controllers)) {
98 options.controllers = [options.controllers];
99 }
100 return options;
101 }
102
103 function collectInjects(options) {
104 la(check.object(options) && check.array(options.controllers),
105 'missing controllers', options);
106
107 if (options.controllers.length || options.exposeApi) {
108 options.inject.push('$controller');
109 options.inject.push('$rootScope');
110 }
111
112 if (check.unemptyString(options.element) || options.exposeApi) {
113 options.inject.push('$rootScope');
114 options.inject.push('$compile');
115 }
116
117 if (check.not.empty(options.http) || check.fn(options.http)) {
118 options.inject.push('$httpBackend');
119 }
120
121 // auto inject mocked modules
122 options.modules = options.modules.concat(Object.keys(options.mocks));
123 // auto inject configured modules
124 options.modules = options.modules.concat(Object.keys(options.configs));
125
126 return options;
127 }
128
129 function ensureUnique(options) {
130 options.inject = uniq(options.inject);
131 options.modules = uniq(options.modules);
132 options.controllers = uniq(options.controllers);
133 return options;
134 }
135
136 function decideSuiteFunction(options) {
137 var suiteFn = root.describe;
138 if (options.only) {
139 // run only this describe block using Jasmine or Mocha
140 // http://bahmutov.calepin.co/focus-on-specific-jasmine-suite-in-karma.html
141 // Jasmine 2.x vs 1.x syntax - fdescribe vs ddescribe
142 suiteFn = root.fdescribe || root.ddescribe || root.describe.only;
143 }
144 if (options.helpful) {
145 suiteFn = root.helpDescribe;
146 }
147 if (options.skip) {
148 la(!options.only, 'skip and only are exclusive options', options);
149 suiteFn = root.xdescribe || root.describe.skip;
150 }
151 return suiteFn;
152 }
153
154 function decideLogFunction(options) {
155 return options.verbose ? angular.bind(console, console.log) : angular.noop;
156 }
157
158 function ngDescribe(options) {
159 la(check.object(options), 'expected options object, see docs', options);
160 la(check.defined(angular), 'missing angular');
161
162 options = copyAliases(options);
163 options = defaults(options);
164 options = ensureArrays(options);
165 options = collectInjects(options);
166 options = ensureUnique(options);
167
168 var log = decideLogFunction(options);
169 la(check.fn(log), 'could not decide on log function', options);
170
171 var isValidNgDescribe = angular.bind(null, check.schema, ngDescribeSchema);
172 la(isValidNgDescribe(options), 'invalid input options', options);
173
174 var suiteFn = decideSuiteFunction(options);
175 la(check.fn(suiteFn), 'missing describe function', options);
176
177 // list of services to inject into mock functions
178 var mockInjects = [];
179
180 var aliasedDependencies = {
181 '$httpBackend': 'http'
182 };
183
184 function ngSpecs() {
185
186 var dependencies = {};
187
188 function partiallInjectMethod(owner, mockName, fn, $injector) {
189 la(check.unemptyString(mockName), 'expected mock name', mockName);
190 la(check.fn(fn), 'expected function for', mockName, 'got', fn);
191
192 var diNames = $injector.annotate(fn);
193 log('dinames for', mockName, diNames);
194 mockInjects.push.apply(mockInjects, diNames);
195
196 var wrappedFunction = function injectedDependenciesIntoMockFunction() {
197 var runtimeArguments = arguments;
198 var k = 0;
199 var args = diNames.map(function (name) {
200 if (check.has(dependencies, name)) {
201 // name is injected by dependency injection
202 return dependencies[name];
203 }
204 // argument is runtime
205 return runtimeArguments[k++];
206 });
207 return fn.apply(owner, args);
208 };
209 return wrappedFunction;
210 }
211
212 function partiallyInjectObject(reference, mockName, $injector) {
213 la(check.object(reference), 'expected object reference, not', reference);
214
215 methodNames(reference).forEach(function (key) {
216 reference[key] = partiallInjectMethod(reference,
217 mockName + '.' + key, reference[key], $injector);
218 });
219
220 return reference;
221 }
222
223 root.beforeEach(function mockModules() {
224 log('ngDescribe', options.name);
225 log('loading modules', options.modules);
226
227 options.modules.forEach(function loadAngularModules(moduleName) {
228 if (options.configs[moduleName]) {
229 var m = angular.module(moduleName);
230 m.config([moduleName + 'Provider', function (provider) {
231 var cloned = clone(options.configs[moduleName]);
232 log('setting config', moduleName + 'Provider to', cloned);
233 provider.set(cloned);
234 }]);
235 } else {
236 angular.mock.module(moduleName, function ($provide, $injector) {
237 var mocks = options.mocks[moduleName];
238 if (mocks) {
239 log('mocking', Object.keys(mocks));
240 Object.keys(mocks).forEach(function (mockName) {
241 var value = mocks[mockName];
242
243 if (check.fn(value) && !value.injected) {
244 value = partiallInjectMethod(mocks, mockName, value, $injector);
245 value.injected = true; // prevent multiple wrapping
246 } else if (check.object(value) && !value.injected) {
247 value = partiallyInjectObject(value, mockName, $injector);
248 value.injected = true; // prevent multiple wrapping
249 }
250 // should we inject a value or a constant?
251 $provide.constant(mockName, value);
252 });
253 }
254 });
255 }
256 });
257 });
258
259 function injectDependencies($injector) {
260 log('injecting', options.inject);
261
262 options.inject.forEach(function (dependencyName) {
263 var injectedUnderName = aliasedDependencies[dependencyName] || dependencyName;
264 la(check.unemptyString(injectedUnderName),
265 'could not rename dependency', dependencyName);
266 dependencies[injectedUnderName] =
267 dependencies[dependencyName] = $injector.get(dependencyName);
268 });
269
270 mockInjects = uniq(mockInjects);
271 log('injecting existing dependencies for mocks', mockInjects);
272 mockInjects.forEach(function (dependencyName) {
273 if ($injector.has(dependencyName)) {
274 dependencies[dependencyName] = $injector.get(dependencyName);
275 }
276 });
277 }
278
279 function setupControllers() {
280 log('setting up controllers', options.controllers);
281 options.controllers.forEach(function (controllerName) {
282 la(check.fn(dependencies.$controller), 'need $controller service', dependencies);
283 la(check.object(dependencies.$rootScope), 'need $rootScope service', dependencies);
284 var scope = dependencies.$rootScope.$new();
285 dependencies.$controller(controllerName, {
286 $scope: scope
287 });
288 dependencies[controllerName] = scope;
289 });
290 }
291
292 function isResponseCode(x) {
293 return check.number(x) && x >= 200 && x < 550;
294 }
295
296 function isResponsePair(x) {
297 return check.array(x) &&
298 x.length === 2 &&
299 isResponseCode(x[0]);
300 }
301
302 function setupMethodHttpResponses(methodName) {
303 la(check.unemptyString(methodName), 'expected method name', methodName);
304 var mockConfig = options.http[methodName];
305
306 if (check.fn(mockConfig)) {
307 mockConfig = mockConfig();
308 }
309
310 la(check.object(mockConfig),
311 'expected mock config for http method', methodName, mockConfig);
312 var method = methodName.toUpperCase();
313
314 Object.keys(mockConfig).forEach(function (url) {
315 log('mocking', method, 'response for url', url);
316
317 var value = mockConfig[url];
318 if (check.fn(value)) {
319 return dependencies.http.when(method, url).respond(function () {
320 var result = value.apply(null, arguments);
321 if (isResponsePair(result)) {
322 return result;
323 }
324 return [200, result];
325 });
326 }
327 if (check.number(value) && isResponseCode(value)) {
328 return dependencies.http.when(method, url).respond(value);
329 }
330 if (isResponsePair(value)) {
331 return dependencies.http.when(method, url).respond(value[0], value[1]);
332 }
333 return dependencies.http.when(method, url).respond(200, value);
334 });
335 }
336
337 function setupHttpResponses() {
338 if (check.not.has(options, 'http')) {
339 return;
340 }
341 if (check.empty(options.http)) {
342 return;
343 }
344
345 la(check.object(options.http), 'expected mock http object', options.http);
346
347 log('setting up mock http responses', options.http);
348 la(check.has(dependencies, 'http'), 'expected to inject http', dependencies);
349
350 function hasMockResponses(methodName) {
351 return check.has(options.http, methodName);
352 }
353
354 var validMethods = ['get', 'head', 'post', 'put', 'delete', 'jsonp', 'patch'];
355 validMethods
356 .filter(hasMockResponses)
357 .forEach(setupMethodHttpResponses);
358 }
359
360 function setupDigestcycleShortcut() {
361 if (dependencies.$httpBackend ||
362 dependencies.http ||
363 dependencies.$rootScope) {
364 dependencies.step = function step() {
365 if (dependencies.http && check.fn(dependencies.http.flush)) {
366 dependencies.http.flush();
367 }
368 if (dependencies.$rootScope) {
369 dependencies.$rootScope.$digest();
370 }
371 };
372 } else {
373 dependencies.step = null;
374 }
375 }
376
377 // treat http option a little differently
378 root.beforeEach(function loadDynamicHttp() {
379 if (check.fn(options.http)) {
380 options.http = options.http();
381 console.log('http function returned', options.http);
382 }
383 });
384
385 root.beforeEach(angular.mock.inject(injectDependencies));
386 root.beforeEach(setupDigestcycleShortcut);
387 root.beforeEach(setupControllers);
388 root.beforeEach(setupHttpResponses);
389
390 function setupElement(elementHtml) {
391 la(check.fn(dependencies.$compile), 'missing $compile', dependencies);
392
393 var scope = dependencies.$rootScope.$new();
394 angular.extend(scope, angular.copy(options.parentScope));
395 log('created element scope with values', options.parentScope);
396
397 var element = angular.element(elementHtml);
398 var compiled = dependencies.$compile(element);
399 compiled(scope);
400 dependencies.$rootScope.$digest();
401
402 dependencies.element = element;
403 dependencies.parentScope = scope;
404 }
405
406 function exposeApi() {
407 return {
408 setupElement: setupElement
409 };
410 }
411
412 var toExpose = options.exposeApi ? exposeApi() : undefined;
413 options.tests(dependencies, toExpose);
414
415 // Element setup comes after tests setup by default so that any beforeEach clauses
416 // within the tests occur before the element is compiled, i.e. $httpBackend setup.
417 if (check.unemptyString(options.element)) {
418 log('setting up element', options.element);
419 root.beforeEach(function () {
420 setupElement(options.element);
421 });
422 root.afterEach(function () {
423 delete dependencies.element;
424 });
425 }
426
427 function deleteDependencies() {
428 options.inject.forEach(function (dependencyName) {
429 la(check.unemptyString(dependencyName), 'missing dependency name', dependencyName);
430 var name = aliasedDependencies[dependencyName] || dependencyName;
431 la(check.has(dependencies, name),
432 'cannot find injected dependency', name, 'for', dependencyName);
433 la(check.has(dependencies, dependencyName),
434 'cannot find injected dependency', dependencyName);
435 delete dependencies[name];
436 delete dependencies[dependencyName];
437 });
438 }
439 root.afterEach(deleteDependencies);
440 }
441
442 suiteFn(options.name, ngSpecs);
443 }
444
445 root.ngDescribe = ngDescribe;
446
447}(this));