Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
477 additions
and
136 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import { ErrorHandler } from '@angular/core'; | ||
import { TestBed } from '@angular/core/testing'; | ||
import { AngularContext } from './angular-context'; | ||
import { MockErrorHandler } from './mock-error-handler'; | ||
import { expectSingleCallAndReset } from './spies'; | ||
|
||
describe('MockErrorHandler', () => { | ||
let consoleSpy: jasmine.Spy; | ||
beforeEach(() => { | ||
consoleSpy = spyOn(console, 'error'); | ||
}); | ||
|
||
describe('createProvider()', () => { | ||
it('makes MockErrorHandler the ErrorHandler', () => { | ||
TestBed.configureTestingModule({ | ||
providers: [MockErrorHandler.createProvider()], | ||
}); | ||
expect(TestBed.inject(ErrorHandler)).toBe( | ||
TestBed.inject(MockErrorHandler), | ||
); | ||
}); | ||
}); | ||
|
||
describe('handleError()', () => { | ||
it("calls through to Angular's handleError()", () => { | ||
TestBed.inject(MockErrorHandler).handleError('blah'); | ||
expectSingleCallAndReset(consoleSpy, 'ERROR', 'blah'); | ||
}); | ||
}); | ||
|
||
describe('.expectOne()', () => { | ||
it('finds a matching error', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
handler.handleError(new Error('error 1')); | ||
handler.handleError(new Error('error 2')); | ||
|
||
const match = handler.expectOne('error 2'); | ||
|
||
expect(match.message).toEqual('error 2'); | ||
}); | ||
|
||
it('throws when there is no match', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
handler.handleError('blah'); | ||
|
||
expect(() => { | ||
handler.expectOne(() => false); | ||
}).toThrowError( | ||
'Expected one matching error(s) for criterion "Match by function: ", found 0', | ||
); | ||
}); | ||
|
||
it('throws when there have been no errors', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
|
||
expect(() => { | ||
handler.expectOne(() => true); | ||
}).toThrowError( | ||
'Expected one matching error(s) for criterion "Match by function: ", found 0', | ||
); | ||
}); | ||
|
||
it('throws when there is more than one match', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
handler.handleError('blah'); | ||
handler.handleError('blah'); | ||
|
||
expect(() => { | ||
handler.expectOne(() => true); | ||
}).toThrowError( | ||
'Expected one matching error(s) for criterion "Match by function: ", found 2', | ||
); | ||
}); | ||
}); | ||
|
||
describe('expectNone()', () => { | ||
it('throws if any error matches', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
handler.handleError(new Error('error 1')); | ||
handler.handleError(new Error('error 2')); | ||
|
||
expect(() => { | ||
handler.expectNone('error 2'); | ||
}).toThrowError( | ||
'Expected zero matching error(s) for criterion "Match by string: error 2", found 1', | ||
); | ||
}); | ||
|
||
it('does not throw when no error matches', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
|
||
expect(() => { | ||
handler.expectNone(() => false); | ||
}).not.toThrowError(); | ||
}); | ||
}); | ||
|
||
describe('match()', () => { | ||
it('finds matching errors', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
handler.handleError('error 1'); | ||
handler.handleError('error 2'); | ||
handler.handleError('error 3'); | ||
|
||
const matches = handler.match((error) => error !== 'error 2'); | ||
|
||
expect(matches).toEqual(['error 1', 'error 3']); | ||
}); | ||
|
||
it('accepts string shorthand', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
handler.handleError(new Error('error 1')); | ||
handler.handleError(new Error('error 2')); | ||
handler.handleError(new Error('error 3')); | ||
|
||
const matches = handler.match('error 2'); | ||
|
||
expect(matches).toEqual([new Error('error 2')]); | ||
}); | ||
|
||
it('accepts RegExp shorthand', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
handler.handleError(new Error('error 1')); | ||
handler.handleError(new Error('error 2')); | ||
handler.handleError(new Error('error 3')); | ||
|
||
const matches = handler.match(/2/); | ||
|
||
expect(matches).toEqual([new Error('error 2')]); | ||
}); | ||
|
||
it('removes the matching calls from future matching', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
handler.handleError(new Error('error 1')); | ||
handler.handleError(new Error('error 2')); | ||
|
||
handler.match('error 2'); | ||
|
||
expect(handler.match(() => true)).toEqual([new Error('error 1')]); | ||
}); | ||
|
||
it('returns an empty array when there have been no errors', () => { | ||
expect(TestBed.inject(MockErrorHandler).match(() => false)).toEqual([]); | ||
}); | ||
|
||
it('gracefully handles when no errors match', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
handler.handleError(new Error('error 1')); | ||
expect(handler.match(() => false)).toEqual([]); | ||
}); | ||
|
||
it('gracefully handles cleverly constructed errors that try to cause errors', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
handler.handleError(undefined); | ||
handler.handleError({ messages: new Date() }); | ||
handler.handleError({ messages: { matches: new Date() } }); | ||
expect(() => { | ||
handler.match('a string'); | ||
handler.match(/a regexp/); | ||
handler.match(() => true); | ||
}).not.toThrowError(); | ||
}); | ||
}); | ||
|
||
describe('verify()', () => { | ||
it('does not throw when all errors have been expected', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
|
||
// no error when no calls were made at all | ||
expect(() => { | ||
handler.verify(); | ||
}).not.toThrowError(); | ||
|
||
// no error when a call was made, but also already expected | ||
handler.handleError(new Error('error 1')); | ||
handler.match(() => true); | ||
expect(() => { | ||
handler.verify(); | ||
}).not.toThrowError(); | ||
}); | ||
|
||
it('throws if there is an outstanding error, including the number of them', () => { | ||
const handler = TestBed.inject(MockErrorHandler); | ||
|
||
// when multiple errors have not been expected | ||
handler.handleError(new Error('error 1')); | ||
handler.handleError(new Error('error 2')); | ||
expect(() => { | ||
handler.verify(); | ||
}).toThrowError('Expected no error(s), found 2'); | ||
|
||
// when SOME errors have already been expected, but not all | ||
handler.match('error 2'); | ||
expect(() => { | ||
handler.verify(); | ||
}).toThrowError('Expected no error(s), found 1'); | ||
}); | ||
}); | ||
|
||
describe('example from the docs', () => { | ||
it('tracks errors', () => { | ||
const ctx = new AngularContext(); | ||
ctx.run(() => { | ||
// test something that is supposed to throw an error | ||
ctx.inject(ErrorHandler).handleError(new Error('special message')); | ||
|
||
// expect that it did | ||
ctx.inject(MockErrorHandler).expectOne('special message'); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { ErrorHandler, Injectable, Provider } from '@angular/core'; | ||
import { isRegExp, isString, remove } from '@s-libs/micro-dash'; | ||
import { buildErrorMessage } from './utils'; | ||
|
||
type ErrorType = Parameters<ErrorHandler['handleError']>[0]; | ||
type Match = string | RegExp | ((error: ErrorType) => boolean); | ||
|
||
/** | ||
* An error handler to be used in tests, that keeps track of all errors it handles and allows for expectations to run on them. | ||
* | ||
* ```ts | ||
* it('tracks errors', () => { | ||
* const ctx = new AngularContext(); | ||
* ctx.run(() => { | ||
* // test something that is supposed to throw an error | ||
* ctx.inject(ErrorHandler).handleError(new Error('special message')); | ||
* | ||
* // expect that it did | ||
* ctx.inject(MockErrorHandler).expectOne('special message'); | ||
* }); | ||
* }); | ||
* ``` | ||
*/ | ||
@Injectable({ providedIn: 'root' }) | ||
export class MockErrorHandler extends ErrorHandler { | ||
/** | ||
* Convenience method to put in a `provide` array, to override Angular's default error handler. Note that this is provided automatically by {@linkcode AngularContext}. | ||
* | ||
* ```ts | ||
* TestBed.configureTestingModule({ | ||
* providers: [MockErrorHandler.createProvider()], | ||
* }); | ||
* ``` | ||
*/ | ||
static createProvider(): Provider { | ||
return { provide: ErrorHandler, useExisting: MockErrorHandler }; | ||
} | ||
|
||
#errors: ErrorType[] = []; | ||
|
||
/** | ||
* In addition to tracking the error, this call's Angular's [ErrorHandler.handleError]{@linkcode https://angular.io/api/core/ErrorHandler#handleError}, prints the error to the console and may print additional information that could be helpful for finding the source of the error. | ||
*/ | ||
override handleError(error: ErrorType): void { | ||
super.handleError(error); | ||
this.#errors.push(error); | ||
} | ||
|
||
/** | ||
* Expect that a single error was handled that matches the given message or predicate, and return it. If no such error was handled, or more than one, fail with a message including `description`, if provided. | ||
*/ | ||
expectOne(match: Match, description?: string): ErrorType { | ||
const matches = this.match(match); | ||
if (matches.length !== 1) { | ||
throw new Error( | ||
buildErrorMessage({ | ||
matchType: 'one matching', | ||
itemType: 'error', | ||
matches, | ||
stringifiedUserInput: stringifyUserInput(match, description), | ||
}), | ||
); | ||
} | ||
return matches[0]; | ||
} | ||
|
||
/** | ||
* Expect that no errors were handled which match the given message or predicate. If a matching error was handled, fail with a message including `description`, if provided. | ||
*/ | ||
expectNone(match: Match, description?: string): void { | ||
const matches = this.match(match); | ||
if (matches.length > 0) { | ||
throw new Error( | ||
buildErrorMessage({ | ||
matchType: 'zero matching', | ||
itemType: 'error', | ||
matches, | ||
stringifiedUserInput: stringifyUserInput(match, description), | ||
}), | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Search for errors that match the given message or predicate, without any expectations. | ||
*/ | ||
match(match: Match): ErrorType[] { | ||
if (isString(match)) { | ||
const message = match; | ||
match = (error) => error?.message === message; | ||
} else if (isRegExp(match)) { | ||
const regex = match; | ||
match = (error) => regex.test(error?.message); | ||
} | ||
return remove(this.#errors, match); | ||
} | ||
|
||
/** | ||
* Verify that no unmatched errors are outstanding. If any are, fail with a message indicating which calls were not matched. | ||
*/ | ||
verify() { | ||
const count = this.#errors.length; | ||
if (count > 0) { | ||
throw new Error(`Expected no error(s), found ${count}`); | ||
} | ||
} | ||
} | ||
|
||
function stringifyUserInput(match: Match, description?: string): string { | ||
if (!description) { | ||
if (isString(match)) { | ||
description = 'Match by string: ' + match; | ||
} else if (isRegExp(match)) { | ||
description = 'Match by regexp: ' + match; | ||
} else { | ||
description = 'Match by function: ' + match.name; | ||
} | ||
} | ||
return description; | ||
} |
Oops, something went wrong.