mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-01-12 22:45:52 +08:00
This is complement to the previous commit. It also refactors the input compile helpers to make it cleaner and more consistent.
406 lines
11 KiB
JavaScript
406 lines
11 KiB
JavaScript
/* global jQuery: true, uid: true */
|
|
'use strict';
|
|
|
|
/**
|
|
* Here is the problem: http://bugs.jquery.com/ticket/7292
|
|
* basically jQuery treats change event on some browsers (IE) as a
|
|
* special event and changes it form 'change' to 'click/keydown' and
|
|
* few others. This horrible hack removes the special treatment
|
|
*/
|
|
if (window._jQuery) _jQuery.event.special.change = undefined;
|
|
|
|
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);
|
|
|
|
// workaround for IE bug https://plus.google.com/104744871076396904202/posts/Kqjuj6RSbbT
|
|
// IE overwrite window.jQuery with undefined because of empty jQuery var statement, so we have to
|
|
// correct this, but only if we are not running in jqLite mode
|
|
if (!_jqLiteMode && _jQuery !== jQuery) {
|
|
jQuery = _jQuery;
|
|
}
|
|
|
|
// This resets global id counter;
|
|
uid = 0;
|
|
|
|
// reset to jQuery or default to us.
|
|
bindJQuery();
|
|
jqLiteCacheSizeInit();
|
|
}
|
|
|
|
angular.element(document.body).empty().removeData();
|
|
});
|
|
|
|
afterEach(function() {
|
|
var count, cache;
|
|
|
|
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
|
|
$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));
|
|
}
|
|
});
|
|
});
|
|
if (count) {
|
|
throw new Error('Found jqCache references that were not deallocated! count: ' + count);
|
|
}
|
|
}
|
|
|
|
|
|
// copied from Angular.js
|
|
// we need these two methods here so that we can run module tests with wrapped angular.js
|
|
function sortedKeys(obj) {
|
|
var keys = [];
|
|
for (var key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
keys.push(key);
|
|
}
|
|
}
|
|
return keys.sort();
|
|
}
|
|
|
|
function forEachSorted(obj, iterator, context) {
|
|
var keys = sortedKeys(obj);
|
|
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) {
|
|
element.off().removeData();
|
|
if (window.jQuery) {
|
|
// jQuery 2.x doesn't expose the cache storage; ensure all element data
|
|
// is removed during its cleanup.
|
|
jQuery.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() {
|
|
var size = 0;
|
|
forEach(jqLite.cache, function() { size++; });
|
|
return size - jqLiteCacheSize.initSize;
|
|
}
|
|
jqLiteCacheSize.initSize = 0;
|
|
|
|
function jqLiteCacheSizeInit() {
|
|
jqLiteCacheSize.initSize = jqLiteCacheSize.initSize + jqLiteCacheSize();
|
|
}
|
|
|
|
|
|
/**
|
|
* @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, '<').
|
|
replace(/>/g, '>');
|
|
} else if (node.nodeName == "#comment") {
|
|
html += '<!--' + node.nodeValue + '-->';
|
|
} 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[\:\-]/) ||
|
|
(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') {
|
|
// in IE we need to check for all of these.
|
|
if (/ng\d+/.exec(attr.name) ||
|
|
attr.name == 'getElementById' ||
|
|
// IE7 has `selected` in attributes
|
|
attr.name == 'selected' ||
|
|
// IE7 adds `value` attribute to all LI tags
|
|
(node.nodeName == 'LI' && attr.name == 'value') ||
|
|
// IE8 adds bogus rowspan=1 and colspan=1 to TD elements
|
|
(node.nodeName == 'TD' && attr.name == 'rowSpan' && attr.value == '1') ||
|
|
(node.nodeName == 'TD' && attr.name == 'colSpan' && attr.value == '1')) {
|
|
continue;
|
|
}
|
|
|
|
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 && (1 * css != 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 getInputCompileHelper(currentSpec) {
|
|
|
|
var helper = {};
|
|
|
|
module(function($compileProvider) {
|
|
$compileProvider.directive('attrCapture', function() {
|
|
return function(scope, element, $attrs) {
|
|
helper.attrs = $attrs;
|
|
};
|
|
});
|
|
});
|
|
|
|
inject(function($compile, $rootScope, $sniffer) {
|
|
|
|
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);
|
|
currentSpec.after(function() {
|
|
VALIDITY_STATE_PROPERTY = 'validity';
|
|
});
|
|
}
|
|
|
|
// Create the form element and dealoc when done
|
|
helper.formElm = jqLite('<form name="form"></form>');
|
|
helper.formElm.append(helper.inputElm);
|
|
|
|
// Compile the lot and return the input element
|
|
$compile(helper.formElm)(scope);
|
|
|
|
spyOn(scope.form, '$addControl').andCallThrough();
|
|
spyOn(scope.form, '$$renameControl').andCallThrough();
|
|
|
|
scope.$digest();
|
|
|
|
return helper.inputElm;
|
|
};
|
|
|
|
helper.changeInputValueTo = function(value) {
|
|
helper.inputElm.val(value);
|
|
browserTrigger(helper.inputElm, $sniffer.hasEvent('input') ? 'input' : 'change');
|
|
};
|
|
|
|
helper.changeGivenInputTo = function(inputElm, value) {
|
|
inputElm.val(value);
|
|
browserTrigger(inputElm, $sniffer.hasEvent('input') ? 'input' : 'change');
|
|
};
|
|
|
|
helper.dealoc = function() {
|
|
dealoc(helper.inputElm);
|
|
dealoc(helper.formElm);
|
|
};
|
|
});
|
|
|
|
return helper;
|
|
}
|