This describes the process to add in a new matcher for maintainers. Adding a new matcher as a plugin will be a subset of these steps.
First create the types in packages/case-entities. These describe
the raw data (mostly parameters if your matcher has any)
-
All matchers much have a constant for the type of the matcher. The type must have an exported constant for its type. This is used to determine what type of matcher it is and to run the associated matching functions. For example:
export const ARRAY_LENGTH_MATCHER_TYPE = 'ArrayLength' as const;
-
Export a new interface that describes the actual matcher JSON. This is what will be written to the contract file, and generated by the matcher DSL.
It must include
case:matcher:type, set to the exact type constant string you created in the previous step. All parameter fields must be prefixed withcase:matcher:. For example:export interface CoreArrayLengthMatcher { 'case:matcher:type': typeof ARRAY_LENGTH_MATCHER_TYPE; 'case:matcher:minLength': number; 'case:matcher:maxLength': number; }
If your matcher modifies the context, add fields prefixed with
case:context:- these are automatically picked up by ContractCase and rolled into the context before this matcher is invoked (and passed down to any child matchers). -
Add these new types to both
AnyCaseNodeTypeandAnyCaseMatcher:export type AnyCaseNodeType = // ...etc typeof ARRAY_LENGTH_MATCHER_TYPE; export type AnyCaseMatcher = // ...etc CoreArrayLengthMatcher;
Next, we will add the behaviour of the matcher, both for matching, and for
stripping the matchers. This goes in packages/case-core
-
Add a new
MatcherExecutor<typeof YOUR_NEW_TYPE>in an appropriate place indiffmatch. For example:const strip: StripMatcherFn<typeof ARRAY_LENGTH_MATCHER_TYPE> = ( matcher: CoreArrayLengthMatcher, matchContext: MatchContext ): AnyData => // implement the strip matcher function here const check: CheckMatchFn<typeof ARRAY_LENGTH_MATCHER_TYPE> = ( matcher: CoreArrayLengthMatcher, matchContext: MatchContext, actual: unknown ): Promise<MatchResult> | MatchResult => // Implement your check here export const ArrayLengthExecutor: MatcherExecutor< typeof ARRAY_LENGTH_MATCHER_TYPE > = { check, strip };
If you need to recurse further into any children of your matchers, use
matchContext.descendAndCheck()ormatchContext.descendAndStrip()as appropriate. See the existing matchers for examples.If your matcher doesn't have enough context to strip matchers (eg, for auxillery matchers designed to be used with
and()), then throw anew StripUnsupportedError(matcher, matchContext)inside your implementation ofstrip().Note that matcher executors are not allowed to call other matcher executors - only
descendAndCheck(). If you need to combine matchers, do it at the DSL layer withand() -
Add the matcher executor to
MatcherExecutors.ts:export const MatcherExecutors: { [T in AnyCaseNodeType]: MatcherExecutor<T>; } = { // ...etc [ARRAY_LENGTH_MATCHER_TYPE]: ArrayLengthExecutor, };
Create a DSL class in packages/case-definition-dsl that creates your matcher type, for example:
/**
* Everything inside this matcher will be matched exactly, unless overridden
* with a generic matcher (eg `AnyString` or` ShapedLike`). Use this to switch
* out of `shapedLike` and back to the default exact matching.
*/
export class ExactlyLike extends CascadingContextMatcher {
/**
* @param content - The object, array, primitive or matcher to match exactly
*/
constructor(content: AnyMatcherOrData) {
super(content, { matchBy: 'exact' }, {});
}
/**
* For non-TypeScript implementations (see `AnyMatcher.toJSON`)
*
* @privateRemarks
* This comment and the implementation is boilerplate on all matchers to avoid
* outputting duplicate unimportant documentation on all matcher classes of
* the docs. Only modify this comment or the implementation via search and replace.
*/
override toJSON(): unknown {
return super.toJSON();
}
}You can also add a DSL function to the Jest boundary, for example:
/**
* Everything inside this matcher will be matched exactly, unless overridden with an `any*` matcher
*
* Use this to switch out of `shapedLike` and back to the default exact matching.
*
* @param content What
*/
export const exactlyLike = (
content: AnyCaseNodeOrData,
): CoreCascadingMatcher => ({
'case:matcher:type': CASCADING_CONTEXT_MATCHER_TYPE,
'case:matcher:child': content,
'case:context:matchBy': 'exact',
});If your matcher needs some double checking or additional processing (eg invoking other matchers to make a composite matcher), do it in the DSL layer. The matcher functions in the entities layer are intended to be data only.
Add or create additional tests at the top level (see index.*.spec.ts for examples)