or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

custom-extensions.mdenvironment-config.mdexpectations-matchers.mdindex.mdspy-system.mdtest-organization.mdtest-utilities.md
tile.json

custom-extensions.mddocs/

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

```