I'm using jasmine on an angular2 project and having some trouble writing a custom matcher for a test. I want to be able to compare two relatively complex objects. I found this article which claims to solve the issue but it simply results in a typescript error stating that it doesn't recognize the new method on jasmine's Matchers
object. The relevant code is this:
declare module jasmine {
interface Matchers {
toBeNumeric(): void;
}
}
Another article gives a similar, but slightly different solution that gives the same error.
declare namespace jasmine {
interface Matchers {
toHaveText(expected: string): boolean;
}
}
I tried this
let m: jasmine.Matchers = expect(someSpy.someMethod).toHaveBeenCalled();
and got this error:
Type 'jasmine.Matchers' is not assignable to type 'jasmine.Matchers'. Two different types with this name exist, but they are unrelated.
That seems to indicate that the declare namespace jasmine
statement is creating a new jasmine
namespace rather than extending the existing one.
So how can I create my own matcher that typescript will be happy with?
Daf's answer mostly worked for me I just noticed an issue with his sample code and the way he named his files. I also happened upon another unrelated issue. Hence a new answer.
Matcher - custom-matchers.ts
import MatchersUtil = jasmine.MatchersUtil;
import CustomMatcherFactories = jasmine.CustomMatcherFactories;
import CustomEqualityTester = jasmine.CustomEqualityTester;
import CustomMatcher = jasmine.CustomMatcher;
import CustomMatcherResult = jasmine.CustomMatcherResult;
export const SomeCustomMatchers: CustomMatcherFactories = {
toReallyEqual: function (util: MatchersUtil, customEqualityTester: CustomEqualityTester[]): CustomMatcher {
return {
compare: function (actual: any, expected: any, anotherCustomArg: any): CustomMatcherResult {
// Your checks here.
const passes = actual === expected;
// Result and message generation.
return {
pass: passes,
message: passes ? `Actual equals expected`
: `Actual does not equal expected`,
}
}
}
}
};
NOTE that
compare
function can have as many custom-parameters as we want (or even Variadic), and that ONLY first-argument is required/reserved (to know actual-value); but if the function name begins with "toHave
" (instead oftoReallyEqual
), then the second argument is reserved for "key: string
" (to know object's field name, I mean, Jasmine2 will loop for us).Also, we could relay on Jasmine for message-generation, like:
message: util.buildFailureMessage('toReallyEqual', passes, actual, expected, anotherCustomArg),
Interface file - matcher-types.d.ts - cannot be the same name as your matcher file
declare namespace jasmine {
interface Matchers<T> {
toReallyEqual(expected: any, anotherCustomArg: any, expectationFailOutput?: any): boolean;
}
}
Custom matcher test
describe('Hello', () => {
beforeEach(() => {
jasmine.addMatchers(SomeCustomMatchers)
});
it('should allow custom matchers', () => {
expect('foo').toReallyEqual('foo');
expect('bar').not.toReallyEqual('test');
})
});
Basically, your second example ("declare namespace") is the way to go, with your logic for the matchers somewhere else, of course.
You're welcome to take a look at https://github.com/fluffynuts/polymer-ts-scratch/tree/5eb799f7c8d144dd8239ab2d2bcc72821327cb24/src/specs/test-utils/jasmine-matchers where I have written some Jasmine matchers and typings to go along with them -- though technically I wrote the actual matchers in Javascript and just named the logic files .ts to placate my build process.
You will need to install @types/jasmine
-- and keep it current.
Just bear in mind that different versions of @types/jasmine
may break things; specifically, the commit linked above was when Jasmine types introduced the Matchers
type having a type parameter (ie, Matchers<T>
) which broke all my .d.ts files.
If you're using ES modules then the namespace declaration needs to be wrapped in a declare global
block.
Here is the updated example with both custom matchers merged into the same definition:
declare global {
namespace jasmine {
interface Matchers {
toBeNumeric(): void;
toHaveText(expected: string): boolean;
}
}
}
It's also possible to have the declarations split apart (or even spread across multiple files):
// File 1 declares this matcher
declare global {
namespace jasmine {
interface Matchers {
toBeNumeric(): void;
}
}
}
// File 2 declares this matcher
declare global {
namespace jasmine {
interface Matchers {
toHaveText(expected: string): boolean;
}
}
}
// File 3: use the custom matchers
it(function(){
expect(3).toBeNumeric();
expect(result).toHaveText('custom matcher');
});
NOTE: In these snippets the namespace
and module
keywords are equivalent but module
is deprecated and namespace
is preferred (to avoid confusion with ES modules or CommonJS modules).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With