0
# Custom Extensions
1
2
Jasmine's system for extending functionality with custom matchers, equality testers, object formatters, and spy strategies. Allows developers to create domain-specific testing utilities and enhance Jasmine's built-in capabilities.
3
4
## Capabilities
5
6
### Custom Matchers
7
8
Functions for adding custom synchronous and asynchronous matchers.
9
10
```javascript { .api }
11
/**
12
* Add custom synchronous matchers to Jasmine
13
* @param matchers - Object mapping matcher names to factory functions
14
*/
15
jasmine.addMatchers(matchers: { [matcherName: string]: MatcherFactory }): void;
16
17
/**
18
* Add custom asynchronous matchers to Jasmine
19
* @param matchers - Object mapping matcher names to async factory functions
20
*/
21
jasmine.addAsyncMatchers(matchers: { [matcherName: string]: AsyncMatcherFactory }): void;
22
23
interface MatcherFactory {
24
(util: MatchersUtil, customEqualityTesters: EqualityTester[]): Matcher;
25
}
26
27
interface AsyncMatcherFactory {
28
(util: MatchersUtil, customEqualityTesters: EqualityTester[]): AsyncMatcher;
29
}
30
31
interface Matcher {
32
compare(actual: any, expected?: any): MatcherResult;
33
negativeCompare?(actual: any, expected?: any): MatcherResult;
34
}
35
36
interface AsyncMatcher {
37
compare(actual: any, expected?: any): Promise<MatcherResult>;
38
negativeCompare?(actual: any, expected?: any): Promise<MatcherResult>;
39
}
40
41
interface MatcherResult {
42
pass: boolean;
43
message?: string | (() => string);
44
}
45
```
46
47
**Usage Examples:**
48
49
```javascript
50
// Custom synchronous matcher
51
jasmine.addMatchers({
52
toBeValidEmail: (util, customEqualityTesters) => {
53
return {
54
compare: (actual, expected) => {
55
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
56
const pass = emailRegex.test(actual);
57
58
return {
59
pass: pass,
60
message: pass
61
? `Expected ${actual} not to be a valid email`
62
: `Expected ${actual} to be a valid email`
63
};
64
}
65
};
66
},
67
68
toBeWithinRange: (util, customEqualityTesters) => {
69
return {
70
compare: (actual, min, max) => {
71
const pass = actual >= min && actual <= max;
72
73
return {
74
pass: pass,
75
message: pass
76
? `Expected ${actual} not to be within range ${min}-${max}`
77
: `Expected ${actual} to be within range ${min}-${max}`
78
};
79
}
80
};
81
}
82
});
83
84
// Using custom matchers
85
expect('user@example.com').toBeValidEmail();
86
expect(50).toBeWithinRange(1, 100);
87
expect(150).not.toBeWithinRange(1, 100);
88
89
// Custom asynchronous matcher
90
jasmine.addAsyncMatchers({
91
toEventuallyContain: (util, customEqualityTesters) => {
92
return {
93
compare: async (actualPromise, expected) => {
94
try {
95
const actual = await actualPromise;
96
const pass = Array.isArray(actual) && actual.includes(expected);
97
98
return {
99
pass: pass,
100
message: pass
101
? `Expected array not to eventually contain ${expected}`
102
: `Expected array to eventually contain ${expected}`
103
};
104
} catch (error) {
105
return {
106
pass: false,
107
message: `Promise rejected: ${error.message}`
108
};
109
}
110
}
111
};
112
}
113
});
114
115
// Using async matcher
116
await expectAsync(fetchUserList()).toEventuallyContain('Alice');
117
```
118
119
### Custom Equality Testing
120
121
Functions for adding custom equality comparison logic.
122
123
```javascript { .api }
124
/**
125
* Add a custom equality tester function
126
* @param tester - Function that tests equality between two values
127
*/
128
jasmine.addCustomEqualityTester(tester: EqualityTester): void;
129
130
interface EqualityTester {
131
(first: any, second: any): boolean | undefined;
132
}
133
```
134
135
**Usage Examples:**
136
137
```javascript
138
// Custom equality for objects with 'equals' method
139
jasmine.addCustomEqualityTester((first, second) => {
140
if (first && second && typeof first.equals === 'function') {
141
return first.equals(second);
142
}
143
// Return undefined to let Jasmine handle with default equality
144
return undefined;
145
});
146
147
// Custom equality for case-insensitive strings
148
jasmine.addCustomEqualityTester((first, second) => {
149
if (typeof first === 'string' && typeof second === 'string') {
150
return first.toLowerCase() === second.toLowerCase();
151
}
152
return undefined;
153
});
154
155
// Custom equality for arrays ignoring order
156
jasmine.addCustomEqualityTester((first, second) => {
157
if (Array.isArray(first) && Array.isArray(second)) {
158
if (first.length !== second.length) return false;
159
160
const sortedFirst = [...first].sort();
161
const sortedSecond = [...second].sort();
162
163
for (let i = 0; i < sortedFirst.length; i++) {
164
if (sortedFirst[i] !== sortedSecond[i]) return false;
165
}
166
return true;
167
}
168
return undefined;
169
});
170
171
// Usage with custom equality
172
class Person {
173
constructor(name, age) {
174
this.name = name;
175
this.age = age;
176
}
177
178
equals(other) {
179
return other instanceof Person &&
180
this.name === other.name &&
181
this.age === other.age;
182
}
183
}
184
185
const person1 = new Person('Alice', 30);
186
const person2 = new Person('Alice', 30);
187
expect(person1).toEqual(person2); // Uses custom equality
188
189
expect('Hello').toEqual('HELLO'); // Case-insensitive comparison
190
expect([1, 2, 3]).toEqual([3, 1, 2]); // Order-independent array comparison
191
```
192
193
### Custom Object Formatting
194
195
Functions for customizing how objects are displayed in test output.
196
197
```javascript { .api }
198
/**
199
* Add a custom object formatter for pretty-printing
200
* @param formatter - Function that formats objects for display
201
*/
202
jasmine.addCustomObjectFormatter(formatter: ObjectFormatter): void;
203
204
interface ObjectFormatter {
205
(object: any): string | undefined;
206
}
207
```
208
209
**Usage Examples:**
210
211
```javascript
212
// Custom formatter for Date objects
213
jasmine.addCustomObjectFormatter((object) => {
214
if (object instanceof Date) {
215
return `Date(${object.toISOString()})`;
216
}
217
return undefined; // Let Jasmine handle other types
218
});
219
220
// Custom formatter for User objects
221
class User {
222
constructor(id, name, email) {
223
this.id = id;
224
this.name = name;
225
this.email = email;
226
}
227
}
228
229
jasmine.addCustomObjectFormatter((object) => {
230
if (object instanceof User) {
231
return `User{id: ${object.id}, name: "${object.name}", email: "${object.email}"}`;
232
}
233
return undefined;
234
});
235
236
// Custom formatter for large arrays
237
jasmine.addCustomObjectFormatter((object) => {
238
if (Array.isArray(object) && object.length > 10) {
239
return `Array[${object.length}] [${object.slice(0, 3).join(', ')}, ..., ${object.slice(-2).join(', ')}]`;
240
}
241
return undefined;
242
});
243
244
// These will now use custom formatting in error messages
245
const user = new User(1, 'Alice', 'alice@example.com');
246
const largeArray = new Array(100).fill(0).map((_, i) => i);
247
248
expect(user.name).toBe('Bob'); // Error shows User{...} format
249
expect(largeArray).toContain(200); // Error shows Array[100] [...] format
250
```
251
252
### Custom Spy Strategies
253
254
Functions for adding custom spy behavior strategies.
255
256
```javascript { .api }
257
/**
258
* Add a custom spy strategy that can be used by any spy
259
* @param name - Name of the strategy
260
* @param factory - Function that creates the strategy behavior
261
*/
262
jasmine.addSpyStrategy(name: string, factory: SpyStrategyFactory): void;
263
264
/**
265
* Set the default spy strategy for all new spies
266
* @param defaultStrategyFn - Function that returns default strategy behavior
267
*/
268
jasmine.setDefaultSpyStrategy(defaultStrategyFn: DefaultSpyStrategyFunction): void;
269
270
interface SpyStrategyFactory {
271
(spy: Spy): Function;
272
}
273
274
interface DefaultSpyStrategyFunction {
275
(name: string, originalFn?: Function): Function;
276
}
277
```
278
279
**Usage Examples:**
280
281
```javascript
282
// Custom spy strategy that logs all calls
283
jasmine.addSpyStrategy('logCalls', (spy) => {
284
return function(...args) {
285
console.log(`Spy ${spy.identity} called with:`, args);
286
return undefined;
287
};
288
});
289
290
// Custom spy strategy that tracks call count
291
jasmine.addSpyStrategy('countCalls', (spy) => {
292
let callCount = 0;
293
return function(...args) {
294
callCount++;
295
spy.callCount = callCount;
296
return `Called ${callCount} times`;
297
};
298
});
299
300
// Custom spy strategy for delayed responses
301
jasmine.addSpyStrategy('delayedReturn', (spy) => {
302
return function(value, delay = 100) {
303
return new Promise(resolve => {
304
setTimeout(() => resolve(value), delay);
305
});
306
};
307
});
308
309
// Using custom spy strategies
310
const spy = jasmine.createSpy('testSpy');
311
312
spy.and.logCalls();
313
spy('arg1', 'arg2'); // Logs: "Spy testSpy called with: ['arg1', 'arg2']"
314
315
spy.and.countCalls();
316
console.log(spy()); // "Called 1 times"
317
console.log(spy.callCount); // 1
318
319
const asyncSpy = jasmine.createSpy('asyncSpy');
320
asyncSpy.and.delayedReturn('response', 200);
321
const result = await asyncSpy(); // Returns 'response' after 200ms
322
323
// Set default spy behavior
324
jasmine.setDefaultSpyStrategy((name, originalFn) => {
325
return function(...args) {
326
console.log(`Default spy ${name} intercepted call`);
327
return originalFn ? originalFn.apply(this, args) : undefined;
328
};
329
});
330
331
// All new spies will use the default strategy
332
const newSpy = jasmine.createSpy('newSpy'); // Logs when called
333
```
334
335
### Matcher Utilities
336
337
Utility functions and objects available to custom matchers.
338
339
```javascript { .api }
340
interface MatchersUtil {
341
/**
342
* Test equality using Jasmine's equality logic
343
* @param a - First value
344
* @param b - Second value
345
* @param customTesters - Custom equality testers
346
* @returns Whether values are equal
347
*/
348
equals(a: any, b: any, customTesters?: EqualityTester[]): boolean;
349
350
/**
351
* Check if value contains expected value
352
* @param haystack - Container to search in
353
* @param needle - Value to find
354
* @param customTesters - Custom equality testers
355
* @returns Whether container contains value
356
*/
357
contains(haystack: any, needle: any, customTesters?: EqualityTester[]): boolean;
358
359
/**
360
* Build failure message for negative comparison
361
* @param matcherName - Name of the matcher
362
* @param isNot - Whether this is a negative assertion
363
* @param actual - Actual value
364
* @param expected - Expected arguments
365
* @returns Formatted error message
366
*/
367
buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: any[]): string;
368
}
369
```
370
371
**Usage Examples:**
372
373
```javascript
374
jasmine.addMatchers({
375
toBeArrayContainingInOrder: (util, customEqualityTesters) => {
376
return {
377
compare: (actual, ...expectedItems) => {
378
if (!Array.isArray(actual)) {
379
return {
380
pass: false,
381
message: 'Expected actual to be an array'
382
};
383
}
384
385
let actualIndex = 0;
386
for (const expectedItem of expectedItems) {
387
let found = false;
388
for (let i = actualIndex; i < actual.length; i++) {
389
if (util.equals(actual[i], expectedItem, customEqualityTesters)) {
390
actualIndex = i + 1;
391
found = true;
392
break;
393
}
394
}
395
if (!found) {
396
return {
397
pass: false,
398
message: util.buildFailureMessage(
399
'toBeArrayContainingInOrder',
400
false,
401
actual,
402
...expectedItems
403
)
404
};
405
}
406
}
407
408
return { pass: true };
409
}
410
};
411
}
412
});
413
414
// Usage
415
expect([1, 2, 3, 4, 5]).toBeArrayContainingInOrder(1, 3, 5);
416
expect(['a', 'b', 'c', 'd']).toBeArrayContainingInOrder('a', 'c');
417
```
418
419
### Extension Best Practices
420
421
Guidelines and patterns for creating effective custom extensions.
422
423
```javascript { .api }
424
// Helper function for creating consistent matcher results
425
function createMatcherResult(pass: boolean, actualDesc: string, expectedDesc: string, not: boolean = false): MatcherResult {
426
const verb = not ? 'not to' : 'to';
427
return {
428
pass: pass,
429
message: pass === not
430
? `Expected ${actualDesc} ${verb} ${expectedDesc}`
431
: undefined
432
};
433
}
434
```
435
436
**Usage Examples:**
437
438
```javascript
439
// Well-structured custom matcher with proper error messages
440
jasmine.addMatchers({
441
toHaveProperty: (util, customEqualityTesters) => {
442
return {
443
compare: (actual, propertyName, expectedValue) => {
444
if (actual == null) {
445
return {
446
pass: false,
447
message: `Expected ${actual} to have property '${propertyName}'`
448
};
449
}
450
451
const hasProperty = propertyName in actual;
452
453
if (arguments.length === 2) {
454
// Just checking property existence
455
return {
456
pass: hasProperty,
457
message: hasProperty
458
? `Expected object not to have property '${propertyName}'`
459
: `Expected object to have property '${propertyName}'`
460
};
461
} else {
462
// Checking property value
463
const hasCorrectValue = hasProperty &&
464
util.equals(actual[propertyName], expectedValue, customEqualityTesters);
465
466
return {
467
pass: hasCorrectValue,
468
message: () => {
469
if (!hasProperty) {
470
return `Expected object to have property '${propertyName}'`;
471
} else {
472
return `Expected property '${propertyName}' to be ${jasmine.pp(expectedValue)} but was ${jasmine.pp(actual[propertyName])}`;
473
}
474
}
475
};
476
}
477
}
478
};
479
}
480
});
481
482
// Usage
483
expect({ name: 'Alice', age: 30 }).toHaveProperty('name');
484
expect({ name: 'Alice', age: 30 }).toHaveProperty('age', 30);
485
expect({ name: 'Alice', age: 30 }).not.toHaveProperty('email');
486
```
487
488
## Types
489
490
```javascript { .api }
491
interface MatcherFactory {
492
(util: MatchersUtil, customEqualityTesters: EqualityTester[]): Matcher;
493
}
494
495
interface AsyncMatcherFactory {
496
(util: MatchersUtil, customEqualityTesters: EqualityTester[]): AsyncMatcher;
497
}
498
499
interface Matcher {
500
compare(actual: any, expected?: any): MatcherResult;
501
negativeCompare?(actual: any, expected?: any): MatcherResult;
502
}
503
504
interface AsyncMatcher {
505
compare(actual: any, expected?: any): Promise<MatcherResult>;
506
negativeCompare?(actual: any, expected?: any): Promise<MatcherResult>;
507
}
508
509
interface MatcherResult {
510
pass: boolean;
511
message?: string | (() => string);
512
}
513
514
interface EqualityTester {
515
(first: any, second: any): boolean | undefined;
516
}
517
518
interface ObjectFormatter {
519
(object: any): string | undefined;
520
}
521
522
interface SpyStrategyFactory {
523
(spy: Spy): Function;
524
}
525
526
interface DefaultSpyStrategyFunction {
527
(name: string, originalFn?: Function): Function;
528
}
529
530
interface MatchersUtil {
531
equals(a: any, b: any, customTesters?: EqualityTester[]): boolean;
532
contains(haystack: any, needle: any, customTesters?: EqualityTester[]): boolean;
533
buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: any[]): string;
534
}
535
```