Skip to content

Commit 0265142

Browse files
gkalpakellimist
authored andcommitted
test(*): introduce the toEqualMinErr() custom Jasmine matcher
Closes angular#15216
1 parent 59fd746 commit 0265142

File tree

8 files changed

+139
-112
lines changed

8 files changed

+139
-112
lines changed

test/helpers/matchers.js

+66-44
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,41 @@ beforeEach(function() {
4949
return hidden;
5050
}
5151

52+
function MinErrMatcher(isNot, namespace, code, content, wording) {
53+
var codeRegex = new RegExp('^' + escapeRegexp('[' + namespace + ':' + code + ']'));
54+
var contentRegex = angular.isUndefined(content) || jasmine.isA_('RegExp', content) ?
55+
content : new RegExp(escapeRegexp(content));
56+
57+
this.test = test;
58+
59+
function escapeRegexp(str) {
60+
// This function escapes all special regex characters.
61+
// We use it to create matching regex from arbitrary strings.
62+
// http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
63+
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
64+
}
65+
66+
function test(exception) {
67+
var exceptionMessage = (exception && exception.message) || exception || '';
68+
69+
var codeMatches = codeRegex.test(exceptionMessage);
70+
var contentMatches = angular.isUndefined(contentRegex) || contentRegex.test(exceptionMessage);
71+
var matches = codeMatches && contentMatches;
72+
73+
return {
74+
pass: isNot ? !matches : matches,
75+
message: message
76+
};
77+
78+
function message() {
79+
return 'Expected ' + wording.inputType + (isNot ? ' not' : '') + ' to ' +
80+
wording.expectedAction + ' ' + namespace + 'MinErr(\'' + code + '\')' +
81+
(contentRegex ? ' matching ' + contentRegex.toString() : '') +
82+
(!exception ? '.' : ', but it ' + wording.actualAction + ': ' + exceptionMessage);
83+
}
84+
}
85+
}
86+
5287
jasmine.addMatchers({
5388
toBeEmpty: cssMatcher('ng-empty', 'ng-not-empty'),
5489
toBeNotEmpty: cssMatcher('ng-not-empty', 'ng-empty'),
@@ -58,6 +93,7 @@ beforeEach(function() {
5893
toBePristine: cssMatcher('ng-pristine', 'ng-dirty'),
5994
toBeUntouched: cssMatcher('ng-untouched', 'ng-touched'),
6095
toBeTouched: cssMatcher('ng-touched', 'ng-untouched'),
96+
6197
toBeAPromise: function() {
6298
return {
6399
compare: generateCompare(false),
@@ -71,6 +107,7 @@ beforeEach(function() {
71107
};
72108
}
73109
},
110+
74111
toBeShown: function() {
75112
return {
76113
compare: generateCompare(false),
@@ -87,6 +124,7 @@ beforeEach(function() {
87124
};
88125
}
89126
},
127+
90128
toBeHidden: function() {
91129
return {
92130
compare: generateCompare(false),
@@ -267,26 +305,34 @@ beforeEach(function() {
267305
}
268306
},
269307

308+
toEqualMinErr: function() {
309+
return {
310+
compare: generateCompare(false),
311+
negativeCompare: generateCompare(true)
312+
};
313+
314+
function generateCompare(isNot) {
315+
return function(actual, namespace, code, content) {
316+
var matcher = new MinErrMatcher(isNot, namespace, code, content, {
317+
inputType: 'error',
318+
expectedAction: 'equal',
319+
actualAction: 'was'
320+
});
321+
322+
return matcher.test(actual);
323+
};
324+
}
325+
},
326+
270327
toThrowMinErr: function() {
271328
return {
272329
compare: generateCompare(false),
273330
negativeCompare: generateCompare(true)
274331
};
332+
275333
function generateCompare(isNot) {
276334
return function(actual, namespace, code, content) {
277-
var result,
278-
exception,
279-
exceptionMessage = '',
280-
escapeRegexp = function(str) {
281-
// This function escapes all special regex characters.
282-
// We use it to create matching regex from arbitrary strings.
283-
// http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
284-
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
285-
},
286-
codeRegex = new RegExp('^\\[' + escapeRegexp(namespace) + ':' + escapeRegexp(code) + '\\]'),
287-
not = isNot ? 'not ' : '',
288-
regex = jasmine.isA_('RegExp', content) ? content :
289-
angular.isDefined(content) ? new RegExp(escapeRegexp(content)) : undefined;
335+
var exception;
290336

291337
if (!angular.isFunction(actual)) {
292338
throw new Error('Actual is not a function');
@@ -298,41 +344,17 @@ beforeEach(function() {
298344
exception = e;
299345
}
300346

301-
if (exception) {
302-
exceptionMessage = exception.message || exception;
303-
}
304-
305-
var message = function() {
306-
return 'Expected function ' + not + 'to throw ' +
307-
namespace + 'MinErr(\'' + code + '\')' +
308-
(regex ? ' matching ' + regex.toString() : '') +
309-
(exception ? ', but it threw ' + exceptionMessage : '.');
310-
};
311-
312-
result = codeRegex.test(exceptionMessage);
313-
if (!result) {
314-
if (isNot) {
315-
return { pass: !result, message: message };
316-
} else {
317-
return { pass: result, message: message };
318-
}
319-
}
347+
var matcher = new MinErrMatcher(isNot, namespace, code, content, {
348+
inputType: 'function',
349+
expectedAction: 'throw',
350+
actualAction: 'threw'
351+
});
320352

321-
if (angular.isDefined(regex)) {
322-
if (isNot) {
323-
return { pass: !regex.test(exceptionMessage), message: message };
324-
} else {
325-
return { pass: regex.test(exceptionMessage), message: message };
326-
}
327-
}
328-
if (isNot) {
329-
return { pass: !result, message: message };
330-
} else {
331-
return { pass: result, message: message };
332-
}
353+
return matcher.test(exception);
333354
};
334355
}
335356
},
357+
336358
toBeMarkedAsSelected: function() {
337359
// Selected is special because the element property and attribute reflect each other's state.
338360
// IE9 will wrongly report hasAttribute('selected') === true when the property is

test/ng/compileSpec.js

+31-26
Original file line numberDiff line numberDiff line change
@@ -1862,8 +1862,8 @@ describe('$compile', function() {
18621862
$httpBackend.flush();
18631863

18641864
expect(sortedHtml(element)).toBe('<div><b class="hello"></b></div>');
1865-
expect($exceptionHandler.errors[0].message).toMatch(
1866-
/^\[\$compile:tpload] Failed to load template: hello\.html/);
1865+
expect($exceptionHandler.errors[0]).toEqualMinErr('$compile', 'tpload',
1866+
'Failed to load template: hello.html');
18671867
})
18681868
);
18691869

@@ -1885,9 +1885,9 @@ describe('$compile', function() {
18851885
$compile('<div><div class="sync async"></div></div>');
18861886
$httpBackend.flush();
18871887

1888-
expect($exceptionHandler.errors[0].message).toMatch(new RegExp(
1889-
'^\\[\\$compile:multidir] Multiple directives \\[async, sync] asking for ' +
1890-
'template on: <div class="sync async">'));
1888+
expect($exceptionHandler.errors[0]).toEqualMinErr('$compile', 'multidir',
1889+
'Multiple directives [async, sync] asking for template on: ' +
1890+
'<div class="sync async">');
18911891
});
18921892
});
18931893

@@ -2096,15 +2096,17 @@ describe('$compile', function() {
20962096
$templateCache.put('template.html', 'dada');
20972097
$compile('<p template></p>');
20982098
$rootScope.$digest();
2099-
expect($exceptionHandler.errors.pop().message).
2100-
toMatch(/\[\$compile:tplrt\] Template for directive 'template' must have exactly one root element\. template\.html/);
2099+
expect($exceptionHandler.errors.pop()).toEqualMinErr('$compile', 'tplrt',
2100+
'Template for directive \'template\' must have exactly one root element. ' +
2101+
'template.html');
21012102

21022103
// multi root
21032104
$templateCache.put('template.html', '<div></div><div></div>');
21042105
$compile('<p template></p>');
21052106
$rootScope.$digest();
2106-
expect($exceptionHandler.errors.pop().message).
2107-
toMatch(/\[\$compile:tplrt\] Template for directive 'template' must have exactly one root element\. template\.html/);
2107+
expect($exceptionHandler.errors.pop()).toEqualMinErr('$compile', 'tplrt',
2108+
'Template for directive \'template\' must have exactly one root element. ' +
2109+
'template.html');
21082110

21092111
// ws is ok
21102112
$templateCache.put('template.html', ' <div></div> \n');
@@ -2676,9 +2678,9 @@ describe('$compile', function() {
26762678
compile('<div class="tiscope-a; scope-b"></div>');
26772679
$httpBackend.flush();
26782680

2679-
expect($exceptionHandler.errors[0].message).toMatch(new RegExp(
2680-
'^\\[\\$compile:multidir] Multiple directives \\[scopeB, tiscopeA] ' +
2681-
'asking for new/isolated scope on: <div class="tiscope-a; scope-b ng-scope">'));
2681+
expect($exceptionHandler.errors[0]).toEqualMinErr('$compile', 'multidir',
2682+
'Multiple directives [scopeB, tiscopeA] asking for new/isolated scope on: ' +
2683+
'<div class="tiscope-a; scope-b ng-scope">');
26822684
})
26832685
);
26842686

@@ -4628,7 +4630,8 @@ describe('$compile', function() {
46284630
// Update val to trigger the unstable onChanges, which will result in an error
46294631
$rootScope.$apply('a = 42');
46304632
expect($exceptionHandler.errors.length).toEqual(1);
4631-
expect($exceptionHandler.errors[0].toString()).toContain('[$compile:infchng] 10 $onChanges() iterations reached.');
4633+
expect($exceptionHandler.errors[0]).
4634+
toEqualMinErr('$compile', 'infchng', '10 $onChanges() iterations reached.');
46324635
});
46334636
});
46344637

@@ -8821,16 +8824,19 @@ describe('$compile', function() {
88218824

88228825
it('should throw on an ng-transclude element inside no transclusion directive', function() {
88238826
inject(function($rootScope, $compile) {
8824-
// we need to do this because different browsers print empty attributes differently
8827+
var error;
8828+
88258829
try {
88268830
$compile('<div><div ng-transclude></div></div>')($rootScope);
88278831
} catch (e) {
8828-
expect(e.message).toMatch(new RegExp(
8829-
'^\\[ngTransclude:orphan\\] ' +
8830-
'Illegal use of ngTransclude directive in the template! ' +
8831-
'No parent directive that requires a transclusion found\\. ' +
8832-
'Element: <div ng-transclude.+'));
8832+
error = e;
88338833
}
8834+
8835+
expect(error).toEqualMinErr('ngTransclude', 'orphan',
8836+
'Illegal use of ngTransclude directive in the template! ' +
8837+
'No parent directive that requires a transclusion found. ' +
8838+
'Element: <div ng-transclude');
8839+
// we need to do this because different browsers print empty attributes differently
88348840
});
88358841
});
88368842

@@ -8898,10 +8904,10 @@ describe('$compile', function() {
88988904
$rootScope.$digest();
88998905

89008906
expect($exceptionHandler.errors[0][1]).toBe('<div class="bar" ng-transclude="">');
8901-
expect($exceptionHandler.errors[0][0].message).toMatch(new RegExp(
8902-
'^\\[ngTransclude:orphan] Illegal use of ngTransclude directive in the ' +
8903-
'template! No parent directive that requires a transclusion found. Element: ' +
8904-
'<div class="bar" ng-transclude="">'));
8907+
expect($exceptionHandler.errors[0][0]).toEqualMinErr('ngTransclude', 'orphan',
8908+
'Illegal use of ngTransclude directive in the template! ' +
8909+
'No parent directive that requires a transclusion found. ' +
8910+
'Element: <div class="bar" ng-transclude="">');
89058911
});
89068912
});
89078913

@@ -9717,9 +9723,8 @@ describe('$compile', function() {
97179723
$compile('<div template first></div>');
97189724
$httpBackend.flush();
97199725

9720-
expect($exceptionHandler.errors[0].message).toMatch(new RegExp(
9721-
'^\\[\\$compile:multidir] Multiple directives \\[first, second] asking for ' +
9722-
'transclusion on: <p '));
9726+
expect($exceptionHandler.errors[0]).toEqualMinErr('$compile', 'multidir',
9727+
'Multiple directives [first, second] asking for transclusion on: <p ');
97239728
});
97249729
});
97259730

test/ng/directive/ngRepeatSpec.js

+18-17
Original file line numberDiff line numberDiff line change
@@ -516,15 +516,12 @@ describe('ngRepeat', function() {
516516
' <div ng-repeat="' + expression + '">{{item}}</div>' +
517517
'</div>')(scope);
518518

519-
var expected = new RegExp('^\\[ngRepeat:badident\\] alias \'' + escape(expr) + '\' is invalid --- must be a valid JS identifier which is not a reserved name');
520-
expect($exceptionHandler.errors.shift()[0].message).
521-
toMatch(expected);
519+
expect($exceptionHandler.errors.shift()[0]).toEqualMinErr('ngRepeat', 'badident',
520+
'alias \'' + expr + '\' is invalid --- must be a valid JS identifier which is not a ' +
521+
'reserved name');
522+
522523
dealoc(element);
523524
});
524-
525-
function escape(text) {
526-
return text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
527-
}
528525
}));
529526
});
530527

@@ -580,16 +577,18 @@ describe('ngRepeat', function() {
580577
it('should error on wrong parsing of ngRepeat', function() {
581578
element = jqLite('<ul><li ng-repeat="i dont parse"></li></ul>');
582579
$compile(element)(scope);
583-
expect($exceptionHandler.errors.shift()[0].message).
584-
toMatch(/^\[ngRepeat:iexp\] Expected expression in form of '_item_ in _collection_\[ track by _id_\]' but got 'i dont parse'\./);
580+
expect($exceptionHandler.errors.shift()[0]).toEqualMinErr('ngRepeat', 'iexp',
581+
'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got ' +
582+
'\'i dont parse\'.');
585583
});
586584

587585

588586
it('should throw error when left-hand-side of ngRepeat can\'t be parsed', function() {
589587
element = jqLite('<ul><li ng-repeat="i dont parse in foo"></li></ul>');
590588
$compile(element)(scope);
591-
expect($exceptionHandler.errors.shift()[0].message).
592-
toMatch(/^\[ngRepeat:iidexp\] '_item_' in '_item_ in _collection_' should be an identifier or '\(_key_, _value_\)' expression, but got 'i dont parse'\./);
589+
expect($exceptionHandler.errors.shift()[0]).toEqualMinErr('ngRepeat', 'iidexp',
590+
'\'_item_\' in \'_item_ in _collection_\' should be an identifier or ' +
591+
'\'(_key_, _value_)\' expression, but got \'i dont parse\'.');
593592
});
594593

595594

@@ -1141,9 +1140,10 @@ describe('ngRepeat', function() {
11411140
it('should throw error on adding existing duplicates and recover', function() {
11421141
scope.items = [a, a, a];
11431142
scope.$digest();
1144-
expect($exceptionHandler.errors.shift().message).
1145-
toMatch(
1146-
/^\[ngRepeat:dupes] Duplicates in a repeater are not allowed\. Use 'track by' expression to specify unique keys\. Repeater: item in items, Duplicate key: object:3, Duplicate value: {}/);
1143+
expect($exceptionHandler.errors.shift()).toEqualMinErr('ngRepeat', 'dupes',
1144+
'Duplicates in a repeater are not allowed. ' +
1145+
'Use \'track by\' expression to specify unique keys. ' +
1146+
'Repeater: item in items, Duplicate key: object:3, Duplicate value: {}');
11471147

11481148
// recover
11491149
scope.items = [a];
@@ -1162,9 +1162,10 @@ describe('ngRepeat', function() {
11621162
it('should throw error on new duplicates and recover', function() {
11631163
scope.items = [d, d, d];
11641164
scope.$digest();
1165-
expect($exceptionHandler.errors.shift().message).
1166-
toMatch(
1167-
/^\[ngRepeat:dupes] Duplicates in a repeater are not allowed\. Use 'track by' expression to specify unique keys\. Repeater: item in items, Duplicate key: object:9, Duplicate value: {}/);
1165+
expect($exceptionHandler.errors.shift()).toEqualMinErr('ngRepeat', 'dupes',
1166+
'Duplicates in a repeater are not allowed. ' +
1167+
'Use \'track by\' expression to specify unique keys. ' +
1168+
'Repeater: item in items, Duplicate key: object:9, Duplicate value: {}');
11681169

11691170
// recover
11701171
scope.items = [a];

0 commit comments

Comments
 (0)