-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Correct highlighting for asymmetric matchers #7893
Correct highlighting for asymmetric matchers #7893
Conversation
Yes, thank you for these snapshot tests as baseline so we can discuss desired report. What do you think about moving jestExpect({type: 'whatever', payload: {a: 'a', b: 'b', c: 'c', d: 'd'}}).toEqual(
{
type: 'whatever',
payload: expect.objectContaining({
a: 'x',
b: expect.any(String),
}),
},
) For your info, a solution to this problem will probably have to be powerful enough to solve another problem of deleted or especially inserted properties:
Even though we mitigated the problem with a more efficient diff algorithm, I think Jest can do even better by understanding when properties are “equal” versus deleted or inserted. Here are some important special cases when changes include both deleted and inserted props:
Although object properties are the first priority, here is an open-ended thought question for more meaningful diff: realistic scenarios when array items don’t match up. For example, React has |
Here is a small update for now. To get some inspiration and make sure that I am not gonna try to reinvent the wheel, I have been reading several codebases for the past week( Since yesterday I started designing a solution. I mostly use objects to reason about the problem, but I want to make sure that in the future the solution will be able to handle other data structures. So far, I think I am on track, but I might bother you for some advice later this week. |
Fantastic @grosto! Looking forward to it 🙂 |
Hey @grosto, anything we can help with to get this over the line? :) |
Hey @thymikee, |
@grosto no worries, same for me. |
Hey, I have some idea to solve this problem. When Example: const copyB = {...b};
Object.keys(a).forEach(key => {
if (a[key].$$typeof === Symbol.for('jest.asymmetricMatcher')) {
if (a[key].asymmetricMatch(copyB[key])) {
copyB[key] = Object.assign(a[key], {});
}
}
}); |
I am back on this! Sorry, I started a new job and really could not find a spare second. :) I will need a day or two to remember the codebases and gather my thoughts again. I will get back with some ideas after that. |
After having a fresh look at the problem, I was not really fond of my ideas from a month ago 😅. So I decided to think a bit more about it. In order to provide a better visual representation of data differences, we have to change our approach from serialize-then-diff to diff-then-serialize. As already mentioned by @pedrottimark. Below is some idea(In a very very naive Typescript): const INSERTED = 'INSERTED';
const DELETED = 'DELETED';
const EQUAL = 'EQUAL';
const UPDATED = 'UPDATED';
const TYPE_EQUAL = 'TYPE_EQUAL';
type DiffResult =
| typeof INSERTED
| typeof DELETED
| typeof EQUAL
| typeof UPDATED
| typeof TYPE_EQUAL;
interface PrimitiveValuesDiff {
diffResult: DiffResult;
path?: string;
rhsValue?: PrimitiveValue;
lhsValue?: PrimitiveValue;
// there will be more fields needed for sure
}
interface ComplexValuesDiff {
lhsConstructorName?: string;
rhsConstructorName?: string;
path?: string;
diffResult: DiffResult;
propertyDiffs: (ComplexValuesDiff | PrimitiveValuesDiff)[];
// there will be more fields needed for sure
}
// example of two object diff
const expected = {
a: 1,
b: {
c: expect.any(Number),
d: 2,
e: 3,
},
};
const received = {
a: 1,
b: {
c: 1,
e: 4,
},
};
diffComplexValues(expected, received, options);
{
lhsConstructorName: 'Object',
rhsConstructorName: 'Object',
diffResult: TYPE_EQUAL,
/* type equality is sufficient for complex values */
propertyDiffs: [
{
path: 'a',
rhsValue: 1,
lhsValue: 1,
diffResult: EQUAL,
},
{
path: 'b',
lhsConstructorName: 'Object',
rhsConstructorName: 'Object',
diffResult: UPDATED,
propertyDiffs: [
{
path: 'c',
diffResult: EQUAL,
lhsValue: expect.any(Number),
rhsValue: 1,
},
{
path: 'd',
diffResult: DELETED,
lhsValue: 2,
},
{
path: 'e',
diffResult: UPDATED,
lhsValue: 3,
rhsValue: 4,
},
],
},
],
};
`
- Expected
+ Received
Object {
"a": 1,
"b": {
"c": 1,
- "d": 2,
- "e": 3,
+ "e": 4,
}
}
` The ComplexValuesDiff will be sufficient to produce a string representation of data differences/commonalities. With some extra fields, this data structure can be also used to describe diffs of Arrays, Maps, and Sets. Important Note: I am not suggesting to create a big diff object and then pass it to another function to generate a diff string. We are probably going to create a diff string as we are traversing an object. The only reason why I made these types is that it's easier for me to illustrate the idea. It is crucial that diff function is consistent with the equality one. We can extract the smaller functions from current I am still trying to solve Glad to hear any feedback :) |
@grosto Super excited that you're working on tackling this long-standing design limitation! Quick question: What's the difference between
|
Thank you for sharing your thoughts!
Great minds think alike: Scott Hovestadt is working on streaming interface for Jest reporters
Yes, asymmetric matchers were a puzzle to me. Lack of API came up recently #8295 (comment)
Yes indeed, this paragraph is brilliant Yes, let’s start with descriptive data structure to prototype enough parts to illustrate a few of the most desired improvements. I experimented with tuples, but hard to debug when I messed up.
A question in my mind is how closely to couple the small functions to compare and format:
I think the solution will be looser, but a vague idea for asymmetric matchers is to be able to provide formatter as optional property of the comparison data structure. Which reminds me to throw out for discussion possible end goal for packaging:
|
Forgot to say about consistent functions, as comparison corresponds to equality, format functions borrow the best from In your example, be able to display the change lines for updated primitive value:
- "e": 3,
+ "e": 4, Also I think default |
@jeysal Thank you for your input!
@pedrottimark Thank you for your feedback. I am glad that we have the same vision and thank you for providing more problems to keep in mind while solving this problem.
I have been thinking about an interface to create a subset diff for Currently, I am working on extracting smaller functions from |
While trying to write a minimal formatter, here are thoughts about the descriptive data structure:
|
But do you not want to mark them properly once we've finished recursing into them? If |
These cases are evidence against what I suggested about absence of When no relevant prop diffs for complex value, possible formatting convention could include ellipsis:
|
Is this fixed now that we've landed #9257? |
Hi, The scope of this PR was intended to be larger than just asymmetric matchers, it's more about changing the jest's diff solution. Is this still something that jest's team considers doing? I have not had free time to contribute to open source for the past months, but I would love to come back to this problem. Also, I think this PR contains valuable context if somebody else would pick it up. |
If it's improved/more usable, we're always open to it 🙂 It's at least been documented now: https://github.com/facebook/jest/blob/master/packages/jest-diff/README.md Happy to keep this open, I just wondered if it might have been solved 👍 |
52468e1
to
d181360
Compare
Hello, I have finally found some time for this. Sorry for keeping it for so long. I have pushed partial implementation with tests and README.md. Will be waiting for the feedback. |
To clarify I am waiting for a feedback on this one to continue. |
I'm so sorry you haven't received feedback on this yet :( |
basic format add support for strings copied test cases from matchers.tests added perf benchmarks refactored objects refactored formatter changed the directory structure added asymmetric matcher added printer
Now that architecture is somewhat stable(at least API of diff object), I wanted to implement some new thing and also fix some skipped tests. I also observed some interesting properties/tradeoffs of diff and then serialize approach. Let's start with the pros: const a = {
payload: {a: 'a', b: 'b', c: 'c', d: 'd'},
type: 'whatever'
};
const b = {
payload: expect.objectContaining({
a: 'x',
b: expect.any(String),
}),
type: 'whatever',
}; This is field updated example. Inserted, Deleted and Unequal Type fields all have big improvements too. We can also correctly understand inverse matches const a = {
payload: {a: 'a', b: 'b', c: 'c', d: 'd'},
type: 'whatever'
};
const b = {
payload: expect.not.objectContaining({
a: 'x',
b: expect.any(String),
}),
type: 'whatever',
}; const a = {
payload: expect.not.objectContaining({
a: 'a',
b: expect.any(String),
}),
type: 'whatever',
};
const b = {
payload: {a: 'a', b: 'b', c: 'c', d: 'd'},
type: 'whatever',
}; I also expect to see the big improvements on arrayContaining too. Have to implement it first. Downsides: const a = new Map([[{a: 1}, 2]]);
const b = new Map([[{a: 2}, 2]]); Currently, I am not traversing the complex keys of the map, so formatter cannot display them properly. That's easily solvable. Bigger complexity is the diff algorithm. The second downside is easier to see than to explain. I am not even sure if this is a downside, but there are some similarities that old diff picks up that we cannot pick up(easily at least) const a = {
searching: '',
whatever: {
a: 1,
},
};
const b = {
searching: '',
whatever: [
{
a: 1,
},
],
}; Let me know what you think and I will continue working on this. |
0e0aa43
to
5b1daef
Compare
Codecov Report
@@ Coverage Diff @@
## master #7893 +/- ##
==========================================
+ Coverage 64.19% 64.79% +0.59%
==========================================
Files 308 323 +15
Lines 13519 14033 +514
Branches 3293 3443 +150
==========================================
+ Hits 8678 9092 +414
- Misses 4126 4191 +65
- Partials 715 750 +35
Continue to review full report at Codecov.
|
They are equal so there is no difference to show. Wording could be changed. I copied it the old diff, I think. |
Are they? It's expecting an object not containing |
I even check in the test that they are equal. I get where you are coming from. I don't think that's how objectContaining behaves. It just checks if one object is subset of another. |
Ah right yeah. Sorry. We even had a debate or some changes around how |
This PR is stale because it has been open 1 year with no activity. Remove stale label or comment or this will be closed in 30 days. |
As you know @SimenB I've been completely inactive on Jest this entire year, and aside from a blog post mostly inactive in 2021 too. I'm currently taking a couple months break from work traveling etc. I've been thinking about what I want to do next. One of my thoughts was whether we could utilize some of that OpenCollective money to actually "employ" me, either just for a while until I start a more "classic" job again, or part-time next to some other job, so that we can get some of these big projects moving, get the issue tracker under control, etc. Some food for thought...in about a month or so I will probably have to decide what to do next. |
Oooh, I think it's a great idea if we could get somebody on top off the repo on a more consistent basis than my "I do what seems interesting, sometimes"! Let's discuss on discord |
This PR is stale because it has been open 1 year with no activity. Remove stale label or comment or this will be closed in 30 days. |
This PR was closed because it has been stalled for 30 days with no activity. Please open a new PR if the issue is still relevant, linking to this one. |
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Summary
I've opened this PR for #6184 and #6170.
From what I could find, there are two cases where Jest shows a confusing diff:
objectContaining
orarrayContaining
, are fully shown in diff instead of only showing the subset of an object.@pedrottimark I will be waiting for your input regarding what and how :)
I have already read your comment in #7027 (comment).
Test plan
I have added several snapshot tests. I am not sure if this is the best way to illustrate the issue with tests, so the suggestions are highly welcome :)