Hi!
I would like to be able to create asymmetric matchers using an api exported from jest which can automatically insert : $$typeof = Symbol.for('jest.asymmetricMatcher') as a property of the asymmetric matcher.
In turn, I would be able to get support from the pretty-format asymmetric_matcher plugin, and it would be able to print the result of calling myAsymmetricMatcher.toAsymmetricMatcher() for the diff string.
Is this a feature the Jest team would like incorporate?
I can work on a PR if someone could hint at the right place on where to make the changes. I was thinking of adding this jest asymmetric matcher factory possibly in the jest-matcher-utils. Would this be fine?
Thank you!
I'm open to this :)
cc @pedrottimark @thymikee
Although it is implied but not currently documented, Jest assertions evaluate asymmetric matcher objects as defined in Jasmine: https://jasmine.github.io/edge/introduction#section-Custom_asymmetric_equality_tester
Yes, as you suggest, if the assertion fails, the message needs to display an expected value better than {"asymmetricMatch": [Function asymmetricMatch]}
@mayank23 Here are some files to study, especially as you plan ahead for tests to add:
When I studied the code to answer your question about toAsymmetricMatcher method, my first impression is to call the method with arguments especially printer
@cpojer Can you confirm or adjust this direction:
expect as we did for addSnapshotSerializerexpect package instead of jest-matcher-utils@pedrottimark yep! Sounds great.
@pedrottimark @cpojer Awesome, thanks for the information! Will start on this soon.
@mayank23 Super. Can you link from a comment in this issue to a gist of some examples of asymmetric matchers with received and expected values so an assertion would fail?
That will help me double-check any improvement to interface between matcher instances and plugin to print expected value so EDIT expect and jest-diff display clear information.
Hi @pedrottimark , here's my gist of 3 examples.
https://gist.github.com/mayank23/4a5ac7a7cbb745aefc995777519338f5
I listed what I think would be nice to have printed when a match fails in each Asymmetric Matcher's getAsymmetricMatcher method.
Technically, this should run if all files were in a single directory, I commented the main use case for each too.
The main test cases file: https://gist.github.com/mayank23/4a5ac7a7cbb745aefc995777519338f5#file-testcases-js. Each test case name is denoted with the prefix 'PASS:' and 'FAIL:' for the passing and failing test cases respectively.
Thanks!
Hi @pedrottimark did you have any questions regarding the examples I links? Thanks!
@mayank23 You gave just what I need to stretch my own thinking. Thank you very much.
This issue will be in my mental slow cooker until Friday.
In case this helps to stretch your thinking about the developer experience we are aiming for, here is a picture of results from jest-diff when one of the built-in asymmetric matchers fails.

Forgot to ask if you can suggest any patterns you have seen to name a factory function?
EDITED AGAIN on 2017-10-28: To explore the space of possible solutions:
expect.AsymmetricMatcher base class so test code can extend it or construct from it$$typeof: Symbol.for('jest.asymmetricMatcher') property, and returns mutated objectSomething out of scope for this issue, but that we want not to make harder to do in the future:
In some cases like picture in preceding comment, results from jest-diff would be more useful if they could distinguish which asymmetric match sub-tests passed or failed.
@thymikee @mayank23 First draft of contract between matchers and plugin for y鈥檃ll to critique:
EDITED on 2017-10-25
For plugin to get name of matcher:
getName() method, return its valueval.constructor && val.constructor.name neither undefined nor Object (for class)asymmetricMatcher (for plain object as described in Jasmine docs)EDITED on 2017-10-28 For plugin to serialize matcher:
serialize method call with same args as plugin serialize method except omit val because matcher decides which of its values to printgetArgs() method, plugin uses the returned array instead of current val.sample to enclose in parentheses following name of matcher. If more than one arg and at least one arg serialized to multiple lines, then plugin calls internal helper printListItems to serialize on separate lines (as if a function by prettier). Otherwise it joins with comma-space the args mapped using printer callback function.EDITED: serialize() replaces toAsymmetricMatcher() to override default serialization:
serialize(/* ignores its arguments in this example */) {
return 'any(' + fnNameFor(this.sample) + ')';
}
| Baseline minified | Proposed minified looks like assertion |
| :--- | :--- |
| Anything | anything() |
| Any<Number> | any(Number) |
| ArrayContaining ["Alice", "Bob"] | arrayContaining(["Alice", "Bob"]) |
| ObjectContaining {"x": Any<Number>, "y": Any<Number>} | objectContaining({"x": any(Number), "y": any(Number)}) |
| StringContaining "JavaScript" | stringContaining("JavaScript") |
| StringMatching /\d{3}-\d{4}/ | stringMatching(/\d{3}-\d{4}/) |
Withdraw special case for arg of 2 built-in matchers because cannot do it for application-specific.
arrayContaining(Array [
"Alice",
"Bob",
])
objectContaining(Object {
"x": any(Number),
"y": any(Number),
})
Hypothetical examples from gist linked in earlier comment:
GreaterThan without toString nor toAsymmetricMatch but getArgs() { return [this.expected]; } would serialize as for example: GreaterThan(1) or GreaterThan("a")ActionTypeMatching might be an example of getString method:import internally as in gistTranslationKeyMatching because of translations seems similar to action types@thymikee Indirectly related to this issue, if Jest raises awareness of built-in asymmetric matchers so people (like me :) study or even copy-and-edit to make application-specific matchers, can we converge on one pattern for where matchers throw error about invalid sample?
3 in constructor function:
2 in asymmetricMatch method:
EDITED on 2017-10-28 to see developer experience from-the-outside-in:
For any which throws for undefined arg in constructor:
TypeError: any() expects to be passed a constructor function. Please pass one or use anything() to match any object.describe preceding two test whose assertions refer to it: encountered a declaration exception and so onTest suite failed to run and so onFor arrayContaining which throws in asymmetricMatch method You must provide an array to ArrayContaining, not 'string'. from each test:
describe preceding two test whose assertions refer to itTherefore, throw error about args in constructor so line number points to source of problem?
The asymmetricMatch method still might throw some errors like arg is not a constructor.
@mayank23 After thinking about this at turtle pace, here is hint where to make changes:
packages/expect/src/asymmetric_matchers.js to export AsymmetricMatcher class and make whichever adjustment to its methods that we agree onpackages/expect/src/__tests__/asymmetric_matchers.test.jspackages/expect/src/index.jsAsymmetricMatcher class at https://github.com/facebook/jest/blob/master/packages/expect/src/index.js#L26-L33expect at https://github.com/facebook/jest/blob/master/packages/expect/src/index.js#L239-L244packages/pretty-format/src/plugins/asymmetric_matcher.js make whichever adjustments that we agree on@cpojer @thymikee What do you think about adding AsymmetricMatcher class to expect API?
To get $$typeof property which the plugin tests when an assertion fails:
class MyMatcher extends AsymmetricMatcher as built-in matchersconst myMatcher = new AsymmetricMatcher() and then set properties, or:// EDITED to add `getExpectedType` and replace ES2015 arrow functions with functions :)
const myMatcher = Object.assign(new AsymmetricMatcher(), {
asymmetricMatch: function (received) { return received % modulus === 0; },
getExpectedType: function () { return 'number'; },
getName: function () { return 'modulusMatcher'; },
getArgs: function () { return [modulus]; },
});
I'm good with exposing AsymmetricMatcher class and moving the sample type check to the constructor 馃憤.
Alright, will get started on this soon. Thanks!
@mayank23 Super! To keep a short feedback loop of review, I suggest:
throw from asymmetricMatch method to constructor discussed in https://github.com/facebook/jest/issues/4711#issuecomment-339429367AsymmetricMatch class as property of expect object.While you work on code, would you like me to think about docs?
awesome, thanks for the advice! yes that would be great!
@mayank23 Happy New Year! Do you still want to make a pull request? Is it possible in January?
Hi @pedrottimark Happy new year to you too! Yes I will try to get this in this month. Sorry about the delay
I'm a total newbie but interested in helping if someone guides me around!
@aymericbouzy Welcome to Jest! I will read your recent issue and review this issue on Friday.
Here are some first steps:
https://github.com/facebook/jest/blob/master/CONTRIBUTING.mdyarn install and yarn testBonus: If you find anything in CONTRIBUTING.md that is incorrect or unclear, make a note for possible future pull request :)
Hi @pedrottimark! Thanks 馃槂 I've done all that alright 馃憤 Not exactly sure what you mean by Workflow, but I've run both commands successfully.
I must say the experience of opening the repo in VS Code and having the recommended extensions showing up is impressive 馃ぉ Especially the Jest extension looks incredible !
mh that's weird, when jest extension runs from VS Code, there are a lot of problems with the test snapshots, it says almost all of them have changed 馃
Going back to running the tests from my terminal.
Can we reopen this issue? I don't think the issue is being addressed. We still cannot provide a good error message other than
Expected value to equal:
{"asymmetricMatch": [Function asymmetricMatch]}
when our custom asymmetric matcher returns false
Please open up a new issue with a reproduction. Thanks!
Most helpful comment
I'm open to this :)
cc @pedrottimark @thymikee