diff options
Diffstat (limited to 'test/helpers')
| -rw-r--r-- | test/helpers/matchers.js | 266 | ||||
| -rw-r--r-- | test/helpers/privateMocks.js | 28 | ||||
| -rw-r--r-- | test/helpers/privateMocksSpec.js | 36 | ||||
| -rw-r--r-- | test/helpers/testabilityPatch.js | 297 |
4 files changed, 627 insertions, 0 deletions
diff --git a/test/helpers/matchers.js b/test/helpers/matchers.js new file mode 100644 index 00000000..57bf35c7 --- /dev/null +++ b/test/helpers/matchers.js @@ -0,0 +1,266 @@ +beforeEach(function() { + + function cssMatcher(presentClasses, absentClasses) { + return function() { + var element = angular.element(this.actual); + var present = true; + var absent = false; + + angular.forEach(presentClasses.split(' '), function(className){ + present = present && element.hasClass(className); + }); + + angular.forEach(absentClasses.split(' '), function(className){ + absent = absent || element.hasClass(className); + }); + + this.message = function() { + return "Expected to have " + presentClasses + + (absentClasses ? (" and not have " + absentClasses + "" ) : "") + + " but had " + element[0].className + "."; + }; + return present && !absent; + }; + } + + function indexOf(array, obj) { + for ( var i = 0; i < array.length; i++) { + if (obj === array[i]) return i; + } + return -1; + } + + function isNgElementHidden(element) { + return angular.element(element).hasClass('ng-hide'); + }; + + this.addMatchers({ + toBeInvalid: cssMatcher('ng-invalid', 'ng-valid'), + toBeValid: cssMatcher('ng-valid', 'ng-invalid'), + toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'), + toBePristine: cssMatcher('ng-pristine', 'ng-dirty'), + toBeShown: function() { + this.message = valueFn( + "Expected element " + (this.isNot ? "": "not ") + "to have 'ng-hide' class"); + return !isNgElementHidden(this.actual); + }, + toBeHidden: function() { + this.message = valueFn( + "Expected element " + (this.isNot ? "not ": "") + "to have 'ng-hide' class"); + return isNgElementHidden(this.actual); + }, + + toEqual: function(expected) { + if (this.actual && this.actual.$$log) { + this.actual = (typeof expected === 'string') + ? this.actual.toString() + : this.actual.toArray(); + } + return jasmine.Matchers.prototype.toEqual.call(this, expected); + }, + + toEqualData: function(expected) { + return angular.equals(this.actual, expected); + }, + + toEqualError: function(message) { + this.message = function() { + var expected; + if (this.actual.message && this.actual.name == 'Error') { + expected = toJson(this.actual.message); + } else { + expected = toJson(this.actual); + } + return "Expected " + expected + " to be an Error with message " + toJson(message); + }; + return this.actual.name == 'Error' && this.actual.message == message; + }, + + toMatchError: function(messageRegexp) { + this.message = function() { + var expected; + if (this.actual.message && this.actual.name == 'Error') { + expected = angular.toJson(this.actual.message); + } else { + expected = angular.toJson(this.actual); + } + return "Expected " + expected + " to match an Error with message " + angular.toJson(messageRegexp); + }; + return this.actual.name == 'Error' && messageRegexp.test(this.actual.message); + }, + + toHaveBeenCalledOnce: function() { + if (arguments.length > 0) { + throw new Error('toHaveBeenCalledOnce does not take arguments, use toHaveBeenCalledWith'); + } + + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + var msg = 'Expected spy ' + this.actual.identity + ' to have been called once, but was ', + count = this.actual.callCount; + return [ + count === 0 ? msg + 'never called.' : + msg + 'called ' + count + ' times.', + msg.replace('to have', 'not to have') + 'called once.' + ]; + }; + + return this.actual.callCount == 1; + }, + + + toHaveBeenCalledOnceWith: function() { + var expectedArgs = jasmine.util.argsToArray(arguments); + + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + if (this.actual.callCount != 1) { + if (this.actual.callCount == 0) { + return [ + 'Expected spy ' + this.actual.identity + ' to have been called once with ' + + jasmine.pp(expectedArgs) + ' but it was never called.', + 'Expected spy ' + this.actual.identity + ' not to have been called with ' + + jasmine.pp(expectedArgs) + ' but it was.' + ]; + } + + return [ + 'Expected spy ' + this.actual.identity + ' to have been called once with ' + + jasmine.pp(expectedArgs) + ' but it was called ' + this.actual.callCount + ' times.', + 'Expected spy ' + this.actual.identity + ' not to have been called once with ' + + jasmine.pp(expectedArgs) + ' but it was.' + ]; + } else { + return [ + 'Expected spy ' + this.actual.identity + ' to have been called once with ' + + jasmine.pp(expectedArgs) + ' but was called with ' + jasmine.pp(this.actual.argsForCall), + 'Expected spy ' + this.actual.identity + ' not to have been called once with ' + + jasmine.pp(expectedArgs) + ' but was called with ' + jasmine.pp(this.actual.argsForCall) + ]; + } + }; + + return this.actual.callCount === 1 && this.env.contains_(this.actual.argsForCall, expectedArgs); + }, + + + toBeOneOf: function() { + return indexOf(arguments, this.actual) !== -1; + }, + + toHaveClass: function(clazz) { + this.message = function() { + return "Expected '" + angular.mock.dump(this.actual) + "' to have class '" + clazz + "'."; + }; + return this.actual.hasClass ? + this.actual.hasClass(clazz) : + angular.element(this.actual).hasClass(clazz); + }, + + toThrowMatching: function(expected) { + return jasmine.Matchers.prototype.toThrow.call(this, expected); + }, + + toThrowMinErr: function(namespace, code, content) { + var result, + exception, + exceptionMessage = '', + escapeRegexp = function (str) { + // This function escapes all special regex characters. + // We use it to create matching regex from arbitrary strings. + // http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + codeRegex = new RegExp('^\\[' + escapeRegexp(namespace) + ':' + escapeRegexp(code) + '\\]'), + not = this.isNot ? "not " : "", + regex = jasmine.isA_("RegExp", content) ? content : + isDefined(content) ? new RegExp(escapeRegexp(content)) : undefined; + + if(!isFunction(this.actual)) { + throw new Error('Actual is not a function'); + } + + try { + this.actual(); + } catch (e) { + exception = e; + } + + if (exception) { + exceptionMessage = exception.message || exception; + } + + this.message = function () { + return "Expected function " + not + "to throw " + + namespace + "MinErr('" + code + "')" + + (regex ? " matching " + regex.toString() : "") + + (exception ? ", but it threw " + exceptionMessage : "."); + }; + + result = codeRegex.test(exceptionMessage); + if (!result) { + return result; + } + + if (isDefined(regex)) { + return regex.test(exceptionMessage); + } + return result; + } + }); +}); + + +// TODO(vojta): remove this once Jasmine in Karma gets updated +// https://github.com/pivotal/jasmine/blob/c40b64a24c607596fa7488f2a0ddb98d063c872a/src/core/Matchers.js#L217-L246 +// This toThrow supports RegExps. +jasmine.Matchers.prototype.toThrow = function(expected) { + var result = false; + var exception, exceptionMessage; + if (typeof this.actual != 'function') { + throw new Error('Actual is not a function'); + } + try { + this.actual(); + } catch (e) { + exception = e; + } + + if (exception) { + exceptionMessage = exception.message || exception; + result = (isUndefined(expected) || this.env.equals_(exceptionMessage, expected.message || expected) || (jasmine.isA_("RegExp", expected) && expected.test(exceptionMessage))); + } + + var not = this.isNot ? "not " : ""; + var regexMatch = jasmine.isA_("RegExp", expected) ? " an exception matching" : ""; + + this.message = function() { + if (exception) { + return ["Expected function " + not + "to throw" + regexMatch, expected ? expected.message || expected : "an exception", ", but it threw", exceptionMessage].join(' '); + } else { + return "Expected function to throw an exception."; + } + }; + + return result; +}; + + +/** + * Create jasmine.Spy on given method, but ignore calls without arguments + * This is helpful when need to spy only setter methods and ignore getters + */ +function spyOnlyCallsWithArgs(obj, method) { + var spy = spyOn(obj, method); + obj[method] = function() { + if (arguments.length) return spy.apply(this, arguments); + return spy.originalValue.apply(this); + }; + return spy; +} diff --git a/test/helpers/privateMocks.js b/test/helpers/privateMocks.js new file mode 100644 index 00000000..6d9fb34f --- /dev/null +++ b/test/helpers/privateMocks.js @@ -0,0 +1,28 @@ +function createMockStyleSheet(doc, wind) { + doc = doc ? doc[0] : document; + wind = wind || window; + + var node = doc.createElement('style'); + var head = doc.getElementsByTagName('head')[0]; + head.appendChild(node); + + var ss = doc.styleSheets[doc.styleSheets.length - 1]; + + return { + addRule : function(selector, styles) { + try { + ss.insertRule(selector + '{ ' + styles + '}', 0); + } + catch(e) { + try { + ss.addRule(selector, styles); + } + catch(e) {} + } + }, + + destroy : function() { + head.removeChild(node); + } + }; +}; diff --git a/test/helpers/privateMocksSpec.js b/test/helpers/privateMocksSpec.js new file mode 100644 index 00000000..e58a2b75 --- /dev/null +++ b/test/helpers/privateMocksSpec.js @@ -0,0 +1,36 @@ +describe('private mocks', function() { + describe('createMockStyleSheet', function() { + + it('should allow custom styles to be created and removed when the stylesheet is destroyed', + inject(function($compile, $document, $window, $rootElement, $rootScope) { + + var doc = $document[0]; + var count = doc.styleSheets.length; + var stylesheet = createMockStyleSheet($document, $window); + expect(doc.styleSheets.length).toBe(count + 1); + + jqLite(doc.body).append($rootElement); + + var elm = $compile('<div class="padded">...</div>')($rootScope); + $rootElement.append(elm); + + expect(getStyle(elm, 'paddingTop')).toBe('0px'); + + stylesheet.addRule('.padded', 'padding-top:2px'); + + expect(getStyle(elm, 'paddingTop')).toBe('2px'); + + stylesheet.destroy(); + + expect(getStyle(elm, 'paddingTop')).toBe('0px'); + + function getStyle(element, key) { + var node = element[0]; + return node.currentStyle ? + node.currentStyle[key] : + $window.getComputedStyle(node)[key]; + }; + })); + + }); +}); diff --git a/test/helpers/testabilityPatch.js b/test/helpers/testabilityPatch.js new file mode 100644 index 00000000..514a5fdb --- /dev/null +++ b/test/helpers/testabilityPatch.js @@ -0,0 +1,297 @@ +'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', '0', '0']; + + // reset to jQuery or default to us. + bindJQuery(); + } + + + angular.element(document.body).html('').removeData(); +}); + +afterEach(function() { + 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(); + } + + // complain about uncleared jqCache references + var count = 0; + + // This line should be enabled as soon as this bug is fixed: http://bugs.jquery.com/ticket/11775 + //var cache = jqLite.cache; + var 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 { + 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(); + // 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])); + } + } +} + +/** + * @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' && indexOf(style, 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; +} + + +// 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.module.ngMock.dump(node)); + } +} + +function assertVisible(node) { + if (!isCssVisible(node)) { + throw new Error('Node should be visible but was hidden: ' + angular.module.ngMock.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.fn = function(msg) { + return function() { + log(msg); + } + } + + log.$$log = true; + + return log; + }); +} + +function pending() { + dump('PENDING'); +}; + +function trace(name) { + dump(new Error(name).stack); +} + +var karmaDump = dump; +window.dump = function () { + karmaDump.apply(undefined, map(arguments, function(arg) { + return angular.mock.dump(arg); + })); +}; |
