/* global jQuery: true, uid: true, jqCache: true */ 'use strict'; if (window.bindJQuery) bindJQuery(); beforeEach(function() { // all this stuff is not needed for module tests, where jqlite and publishExternalAPI and jqLite are not global vars if (window.publishExternalAPI) { publishExternalAPI(angular); // This resets global id counter; uid = 0; // reset to jQuery or default to us. bindJQuery(); // Clear the cache to prevent memory leak failures from previous tests // breaking subsequent tests unnecessarily jqCache = jqLite.cache = {}; } angular.element(window.document.body).empty().removeData(); }); afterEach(function() { var count, cache; // These Nodes are persisted across tests. // They used to be assigned a `$$hashKey` when animated, which we needed to clear after each test // to avoid affecting other tests. This is no longer the case, so we are just ensuring that there // is indeed no `$$hashKey` on them. var doc = window.document; var html = doc.querySelector('html'); var body = doc.body; expect(doc.$$hashKey).toBeFalsy(); expect(html && html.$$hashKey).toBeFalsy(); expect(body && body.$$hashKey).toBeFalsy(); if (this.$injector) { var $rootScope = this.$injector.get('$rootScope'); var $rootElement = this.$injector.get('$rootElement'); var $log = this.$injector.get('$log'); // release the injector dealoc($rootScope); dealoc($rootElement); // check $log mock if ($log.assertEmpty) { $log.assertEmpty(); } } if (!window.jQuery) { // jQuery 2.x doesn't expose the cache storage. // complain about uncleared jqCache references count = 0; cache = angular.element.cache; forEachSorted(cache, function(expando, key) { angular.forEach(expando.data, function(value, key) { count++; if (value && value.$element) { dump('LEAK', key, value.$id, sortedHtml(value.$element)); } else { dump('LEAK', key, angular.toJson(value)); } delete expando.data[key]; }); }); if (count) { throw new Error('Found jqCache references that were not deallocated! count: ' + count); } } // copied from Angular.js // we need this method here so that we can run module tests with wrapped angular.js function forEachSorted(obj, iterator, context) { var keys = Object.keys(obj).sort(); for (var i = 0; i < keys.length; i++) { iterator.call(context, obj[keys[i]], keys[i]); } return keys; } }); function dealoc(obj) { var jqCache = angular.element.cache; if (obj) { if (angular.isElement(obj)) { cleanup(angular.element(obj)); } else if (!window.jQuery) { // jQuery 2.x doesn't expose the cache storage. for (var key in jqCache) { var value = jqCache[key]; if (value.data && value.data.$scope === obj) { delete jqCache[key]; } } } } function cleanup(element) { angular.element.cleanData(element); // Note: We aren't using element.contents() here. Under jQuery, element.contents() can fail // for IFRAME elements. jQuery explicitly uses (element.contentDocument || // element.contentWindow.document) and both properties are null for IFRAMES that aren't attached // to a document. var children = element[0].childNodes || []; for (var i = 0; i < children.length; i++) { cleanup(angular.element(children[i])); } } } function jqLiteCacheSize() { return Object.keys(jqLite.cache).length; } /** * @param {DOMElement} element * @param {boolean=} showNgClass */ function sortedHtml(element, showNgClass) { var html = ''; forEach(jqLite(element), function toString(node) { if (node.nodeName === '#text') { html += node.nodeValue. replace(/&(\w+[&;\W])?/g, function(match, entity) {return entity ? match : '&';}). replace(//g, '>'); } else if (node.nodeName === '#comment') { html += ''; } else { html += '<' + (node.nodeName || '?NOT_A_NODE?').toLowerCase(); var attributes = node.attributes || []; var attrs = []; var className = node.className || ''; if (!showNgClass) { className = className.replace(/ng-[\w-]+\s*/g, ''); } className = trim(className); if (className) { attrs.push(' class="' + className + '"'); } for (var i = 0; i < attributes.length; i++) { if (i > 0 && attributes[i] === attributes[i - 1]) { continue; // IE9 creates dupes. Ignore them! } var attr = attributes[i]; if (attr.name.match(/^ng[:-]/) || !/^ng\d+/.test(attr.name) && (attr.value || attr.value === '') && attr.value !== 'null' && attr.value !== 'auto' && attr.value !== 'false' && attr.value !== 'inherit' && (attr.value !== '0' || attr.name === 'value') && attr.name !== 'loop' && attr.name !== 'complete' && attr.name !== 'maxLength' && attr.name !== 'size' && attr.name !== 'class' && attr.name !== 'start' && attr.name !== 'tabIndex' && attr.name !== 'style' && attr.name.substr(0, 6) !== 'jQuery') { attrs.push(' ' + attr.name + '="' + attr.value + '"'); } } attrs.sort(); html += attrs.join(''); if (node.style) { var style = []; if (node.style.cssText) { forEach(node.style.cssText.split(';'), function(value) { value = trim(value); if (value) { style.push(lowercase(value)); } }); } for (var css in node.style) { var value = node.style[css]; if (isString(value) && isString(css) && css !== 'cssText' && value && isNaN(Number(css))) { var text = lowercase(css + ': ' + value); if (value !== 'false' && style.indexOf(text) === -1) { style.push(text); } } } style.sort(); var tmp = style; style = []; forEach(tmp, function(value) { if (!value.match(/^max[^-]/)) { style.push(value); } }); if (style.length) { html += ' style="' + style.join('; ') + ';"'; } } html += '>'; var children = node.childNodes; for (var j = 0; j < children.length; j++) { toString(children[j]); } html += '' + node.nodeName.toLowerCase() + '>'; } }); return html; } function childrenTagsOf(element) { var tags = []; forEach(jqLite(element).children(), function(child) { tags.push(child.nodeName.toLowerCase()); }); return tags; } // TODO(vojta): migrate these helpers into jasmine matchers /**a * This method is a cheap way of testing if css for a given node is not set to 'none'. It doesn't * actually test if an element is displayed by the browser. Be aware!!! */ function isCssVisible(node) { var display = node.css('display'); return !node.hasClass('ng-hide') && display !== 'none'; } function assertHidden(node) { if (isCssVisible(node)) { throw new Error('Node should be hidden but was visible: ' + angular.mock.dump(node)); } } function assertVisible(node) { if (!isCssVisible(node)) { throw new Error('Node should be visible but was hidden: ' + angular.mock.dump(node)); } } function provideLog($provide) { $provide.factory('log', function() { var messages = []; function log(msg) { messages.push(msg); return msg; } log.toString = function() { return messages.join('; '); }; log.toArray = function() { return messages; }; log.reset = function() { messages = []; }; log.empty = function() { var currentMessages = messages; messages = []; return currentMessages; }; log.fn = function(msg) { return function() { log(msg); }; }; log.$$log = true; return log; }); } function pending() { window.dump('PENDING'); } function trace(name) { window.dump(new Error(name).stack); } var karmaDump = window.dump || function() { window.console.log.apply(window.console, arguments); }; window.dump = function() { karmaDump.apply(undefined, Array.prototype.map.call(arguments, function(arg) { return angular.mock.dump(arg); })); }; function generateInputCompilerHelper(helper) { beforeEach(function() { helper.validationCounter = {}; module(function($compileProvider) { $compileProvider.directive('validationSpy', function() { return { priority: 1, require: 'ngModel', link: function(scope, element, attrs, ctrl) { var validationName = attrs.validationSpy; var originalValidator = ctrl.$validators[validationName]; helper.validationCounter[validationName] = 0; ctrl.$validators[validationName] = function(modelValue, viewValue) { helper.validationCounter[validationName]++; return originalValidator(modelValue, viewValue); }; } }; }); $compileProvider.directive('attrCapture', function() { return function(scope, element, $attrs) { helper.attrs = $attrs; }; }); }); inject(function($compile, $rootScope, $sniffer, $document, $rootElement) { helper.compileInput = function(inputHtml, mockValidity, scope) { scope = helper.scope = scope || $rootScope; // Create the input element and dealoc when done helper.inputElm = jqLite(inputHtml); // Set up mock validation if necessary if (isObject(mockValidity)) { VALIDITY_STATE_PROPERTY = 'ngMockValidity'; helper.inputElm.prop(VALIDITY_STATE_PROPERTY, mockValidity); } // Create the form element and dealoc when done helper.formElm = jqLite('
'); helper.formElm.append(helper.inputElm); // Compile the lot and return the input element $compile(helper.formElm)(scope); $rootElement.append(helper.formElm); // Append the app to the document so that "click" on a radio/checkbox triggers "change" // Support: Chrome, Safari 8, 9 jqLite($document[0].body).append($rootElement); spyOn(scope.form, '$addControl').and.callThrough(); spyOn(scope.form, '$$renameControl').and.callThrough(); scope.$digest(); return helper.inputElm; }; helper.changeInputValueTo = function(value) { helper.changeGivenInputTo(helper.inputElm, value); }; helper.changeGivenInputTo = function(inputElm, value) { inputElm.val(value); browserTrigger(inputElm, $sniffer.hasEvent('input') ? 'input' : 'change'); }; helper.dealoc = function() { dealoc(helper.inputElm); dealoc(helper.formElm); }; }); }); afterEach(function() { helper.dealoc(); }); afterEach(function() { VALIDITY_STATE_PROPERTY = 'validity'; }); }