UNPKG

ng-describe

Version:

Convenient BDD specs for Angular

473 lines (401 loc) 15.5 kB
(function setupNgDescribe(root) { // check - kensho/check-more-types // la - bahmutov/lazy-ass la(check.object(root), 'missing root'); var _defaults = { // primary options name: 'default tests', modules: [], configs: {}, inject: [], exposeApi: false, tests: function () {}, mocks: {}, helpful: false, controllers: [], element: '', http: {}, // secondary options only: false, verbose: false, skip: false, parentScope: {} }; function defaults(opts) { opts = opts || {}; return angular.extend(angular.copy(_defaults), opts); } var ngDescribeSchema = { // primary options name: check.unemptyString, modules: check.arrayOfStrings, configs: check.object, inject: check.arrayOfStrings, exposeApi: check.bool, tests: check.fn, mocks: check.object, helpful: check.bool, controllers: check.arrayOfStrings, element: check.string, // TODO allow object OR function // http: check.object, // secondary options only: check.bool, verbose: check.bool, skip: check.or(check.bool, check.unemptyString), parentScope: check.object }; function uniq(a) { var seen = {}; return a.filter(function(item) { return seen.hasOwnProperty(item) ? false : (seen[item] = true); }); } function clone(a) { return JSON.parse(JSON.stringify(a)); } function methodNames(reference) { la(check.object(reference), 'expected object reference, not', reference); return Object.keys(reference).filter(function (key) { return check.fn(reference[key]); }); } function copyAliases(options) { if (options.config && !options.configs) { options.configs = options.config; } if (options.mock && !options.mocks) { options.mocks = options.mock; } if (options.module && !options.modules) { options.modules = options.module; } if (options.test && !options.tests) { options.tests = options.test; } if (options.controller && !options.controllers) { options.controllers = options.controller; } return options; } function ensureArrays(options) { if (check.string(options.modules)) { options.modules = [options.modules]; } if (check.string(options.inject)) { options.inject = [options.inject]; } if (check.string(options.controllers)) { options.controllers = [options.controllers]; } return options; } function collectInjects(options) { la(check.object(options) && check.array(options.controllers), 'missing controllers', options); if (options.controllers.length || options.exposeApi) { options.inject.push('$controller'); options.inject.push('$rootScope'); } if (check.unemptyString(options.element) || options.exposeApi) { options.inject.push('$rootScope'); options.inject.push('$compile'); } if (check.not.empty(options.http) || check.fn(options.http)) { options.inject.push('$httpBackend'); } // auto inject mocked modules options.modules = options.modules.concat(Object.keys(options.mocks)); // auto inject configured modules options.modules = options.modules.concat(Object.keys(options.configs)); return options; } function ensureUnique(options) { options.inject = uniq(options.inject); options.modules = uniq(options.modules); options.controllers = uniq(options.controllers); return options; } function decideSuiteFunction(options) { var suiteFn = root.describe; if (options.only) { // run only this describe block using Jasmine or Mocha // http://bahmutov.calepin.co/focus-on-specific-jasmine-suite-in-karma.html // Jasmine 2.x vs 1.x syntax - fdescribe vs ddescribe suiteFn = root.fdescribe || root.ddescribe || root.describe.only; } if (options.helpful) { suiteFn = root.helpDescribe; } if (options.skip) { la(!options.only, 'skip and only are exclusive options', options); suiteFn = root.xdescribe || root.describe.skip; } return suiteFn; } function decideLogFunction(options) { return options.verbose ? angular.bind(console, console.log) : angular.noop; } function ngDescribe(options) { la(check.object(options), 'expected options object, see docs', options); la(check.defined(angular), 'missing angular'); options = copyAliases(options); options = defaults(options); options = ensureArrays(options); options = collectInjects(options); options = ensureUnique(options); var log = decideLogFunction(options); la(check.fn(log), 'could not decide on log function', options); var isValidNgDescribe = angular.bind(null, check.schema, ngDescribeSchema); la(isValidNgDescribe(options), 'invalid input options', options); var suiteFn = decideSuiteFunction(options); la(check.fn(suiteFn), 'missing describe function', options); // list of services to inject into mock functions var mockInjects = []; var aliasedDependencies = { '$httpBackend': 'http' }; function ngSpecs() { var dependencies = {}; function partiallInjectMethod(owner, mockName, fn, $injector) { la(check.unemptyString(mockName), 'expected mock name', mockName); la(check.fn(fn), 'expected function for', mockName, 'got', fn); var diNames = $injector.annotate(fn); log('dinames for', mockName, diNames); mockInjects.push.apply(mockInjects, diNames); var wrappedFunction = function injectedDependenciesIntoMockFunction() { var runtimeArguments = arguments; var k = 0; var args = diNames.map(function (name) { if (check.has(dependencies, name)) { // name is injected by dependency injection return dependencies[name]; } // argument is runtime return runtimeArguments[k++]; }); return fn.apply(owner, args); }; return wrappedFunction; } function partiallyInjectObject(reference, mockName, $injector) { la(check.object(reference), 'expected object reference, not', reference); methodNames(reference).forEach(function (key) { reference[key] = partiallInjectMethod(reference, mockName + '.' + key, reference[key], $injector); }); return reference; } root.beforeEach(function mockModules() { log('ngDescribe', options.name); log('loading modules', options.modules); options.modules.forEach(function loadAngularModules(moduleName) { if (options.configs[moduleName]) { var m = angular.module(moduleName); m.config([moduleName + 'Provider', function (provider) { var cloned = clone(options.configs[moduleName]); log('setting config', moduleName + 'Provider to', cloned); provider.set(cloned); }]); } else { angular.mock.module(moduleName, function ($provide, $injector) { var mocks = options.mocks[moduleName]; if (mocks) { log('mocking', Object.keys(mocks)); Object.keys(mocks).forEach(function (mockName) { var value = mocks[mockName]; if (check.fn(value) && !value.injected) { value = partiallInjectMethod(mocks, mockName, value, $injector); value.injected = true; // prevent multiple wrapping } else if (check.object(value) && !value.injected) { value = partiallyInjectObject(value, mockName, $injector); value.injected = true; // prevent multiple wrapping } // should we inject a value or a constant? $provide.constant(mockName, value); }); } }); } }); }); function injectDependencies($injector) { log('injecting', options.inject); options.inject.forEach(function (dependencyName) { var injectedUnderName = aliasedDependencies[dependencyName] || dependencyName; la(check.unemptyString(injectedUnderName), 'could not rename dependency', dependencyName); dependencies[injectedUnderName] = dependencies[dependencyName] = $injector.get(dependencyName); }); mockInjects = uniq(mockInjects); log('injecting existing dependencies for mocks', mockInjects); mockInjects.forEach(function (dependencyName) { if ($injector.has(dependencyName)) { dependencies[dependencyName] = $injector.get(dependencyName); } }); } function setupControllers(controllerNames) { if (check.unemptyString(controllerNames)) { controllerNames = [controllerNames]; } log('setting up controllers', controllerNames); la(check.arrayOfStrings(controllerNames), 'expected list of controller names', controllerNames); controllerNames.forEach(function (controllerName) { la(check.fn(dependencies.$controller), 'need $controller service', dependencies); la(check.object(dependencies.$rootScope), 'need $rootScope service', dependencies); var scope = dependencies.$rootScope.$new(); dependencies.$controller(controllerName, { $scope: scope }); dependencies[controllerName] = scope; }); // need to clean up anything created when setupControllers was called root.afterEach(function cleanupControllers() { controllerNames.forEach(function (controllerName) { delete dependencies[controllerName]; }); }); } function isResponseCode(x) { return check.number(x) && x >= 200 && x < 550; } function isResponsePair(x) { return check.array(x) && x.length === 2 && isResponseCode(x[0]); } function setupMethodHttpResponses(methodName) { la(check.unemptyString(methodName), 'expected method name', methodName); var mockConfig = options.http[methodName]; if (check.fn(mockConfig)) { mockConfig = mockConfig(); } la(check.object(mockConfig), 'expected mock config for http method', methodName, mockConfig); var method = methodName.toUpperCase(); Object.keys(mockConfig).forEach(function (url) { log('mocking', method, 'response for url', url); var value = mockConfig[url]; if (check.fn(value)) { return dependencies.http.when(method, url).respond(function () { var result = value.apply(null, arguments); if (isResponsePair(result)) { return result; } return [200, result]; }); } if (check.number(value) && isResponseCode(value)) { return dependencies.http.when(method, url).respond(value); } if (isResponsePair(value)) { return dependencies.http.when(method, url).respond(value[0], value[1]); } return dependencies.http.when(method, url).respond(200, value); }); } function setupHttpResponses() { if (check.not.has(options, 'http')) { return; } if (check.empty(options.http)) { return; } la(check.object(options.http), 'expected mock http object', options.http); log('setting up mock http responses', options.http); la(check.has(dependencies, 'http'), 'expected to inject http', dependencies); function hasMockResponses(methodName) { return check.has(options.http, methodName); } var validMethods = ['get', 'head', 'post', 'put', 'delete', 'jsonp', 'patch']; validMethods .filter(hasMockResponses) .forEach(setupMethodHttpResponses); } function setupDigestCycleShortcut() { if (dependencies.$httpBackend || dependencies.http || dependencies.$rootScope) { dependencies.step = function step() { if (dependencies.http && check.fn(dependencies.http.flush)) { dependencies.http.flush(); } if (dependencies.$rootScope) { dependencies.$rootScope.$digest(); } }; } else { dependencies.step = null; } } // treat http option a little differently function loadDynamicHttp() { if (check.fn(options.http)) { options.http = options.http(); console.log('http function returned', options.http); } } root.beforeEach(loadDynamicHttp); root.beforeEach(angular.mock.inject(injectDependencies)); root.beforeEach(setupDigestCycleShortcut); root.beforeEach(setupHttpResponses); function setupElement(elementHtml) { la(check.fn(dependencies.$compile), 'missing $compile', dependencies); var scope = dependencies.$rootScope.$new(); angular.extend(scope, angular.copy(options.parentScope)); log('created element scope with values', options.parentScope); var element = angular.element(elementHtml); var compiled = dependencies.$compile(element); compiled(scope); dependencies.$rootScope.$digest(); dependencies.element = element; dependencies.parentScope = scope; } function exposeApi() { return { setupElement: setupElement, setupControllers: setupControllers }; } var toExpose = options.exposeApi ? exposeApi() : undefined; // call the user-supplied test function to register the actual unit tests options.tests(dependencies, toExpose); // Element setup comes after tests setup by default so that any beforeEach clauses // within the tests occur before the element is compiled, i.e. $httpBackend setup. if (check.unemptyString(options.element)) { log('setting up element', options.element); root.beforeEach(function () { setupElement(options.element); }); root.afterEach(function () { delete dependencies.element; }); } if (check.has(options, 'controllers') && check.unempty(options.controllers)) { root.beforeEach(function () { setupControllers(options.controllers); }); } function deleteDependencies() { options.inject.forEach(function (dependencyName) { la(check.unemptyString(dependencyName), 'missing dependency name', dependencyName); var name = aliasedDependencies[dependencyName] || dependencyName; la(check.has(dependencies, name), 'cannot find injected dependency', name, 'for', dependencyName); la(check.has(dependencies, dependencyName), 'cannot find injected dependency', dependencyName); delete dependencies[name]; delete dependencies[dependencyName]; }); } root.afterEach(deleteDependencies); } suiteFn(options.name, ngSpecs); return ngDescribe; } root.ngDescribe = ngDescribe; }(this));