-
-
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
WIP: Implementing snapshot editor support #2629
Conversation
@@ -11,7 +11,8 @@ | |||
"typescript": "^2.0.10" | |||
}, | |||
"dependencies": { | |||
"babylon": "^6.13.0" | |||
"babylon": "^6.13.0", | |||
"astroll": "^1.0.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this? I can't even find this anywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a 10 line ast walker that keeps track of the parents:
https://github.com/kentaromiura/astroll/blob/master/index.js#L15-L32
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just put this into jest-editor-support for now until it grows too big?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
if (!SnapshotSupport.parser) { | ||
throw new Error('SnapshotSupport.parser has to exists'); | ||
} | ||
const fileNode = SnapshotSupport.parser(fileContent, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no try catch here as the it's better to move the responsibility to the caller, any throw from here means that the source is broken.
That sucks, time to take a look |
Once danger support is solid it would be nice to add it to the lint script in the package.json. |
Current coverage is 68.75% (diff: 95.65%)@@ master #2629 diff @@
==========================================
Files 140 142 +2
Lines 5060 5127 +67
Methods 0 0
Messages 0 0
Branches 0 0
==========================================
+ Hits 3459 3525 +66
- Misses 1601 1602 +1
Partials 0 0
|
|
||
test('snapshot-test.example', () => { | ||
const filePath = path.join(snapshotFixturePath, 'snapshot-test.example'); | ||
const required = require( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"required" seems a bit odd. Maybe "expected" (this is what it calls when the assertion fails) or "snapshotData"/"snapshotFile"/"snapshots"?
expect(results[0].node.loc.end.column).toBe(56); | ||
}); | ||
|
||
test('react-native.example', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this test different from the one above? It seems like there are two tests here that test the same thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In one case is describeless
, other than that is the same (apart the different AST)
|
||
// -> expect(extractSummary(info).summary).toMatchSnapshot(); | ||
expect(results[0].node.loc.end.line).toBe(111); | ||
expect(results[0].node.loc.end.column).toBe(56); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add an object on top of the test with metadata for all of the snapshots in the file and then test for all of them rather then just asserting that the first one is correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can but for .node
it's dependant of the format returned from the parser, which might be a good thing to test against as it will break in case the parser breaks back-compat
@@ -0,0 +1,105 @@ | |||
exports[`test renders correctly 1`] = ` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both the two example tests and two example snapshots are rather long. Can we condense them a little bit to be smaller in size? That'll make it easier to maintain them and it gets rid of all the things in the code that we don't need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took two already existing tests and related snapshots from other tests in Jest, yes we can make them smaller, but I rather use a real example
SnapshotSupport.plugins = [ | ||
'jsx', 'flow', 'objectRestSpread', 'classProperties', | ||
]; | ||
SnapshotSupport.parser = babylon.parse.bind(babylon); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we re-use code from here: https://github.com/cpojer/jest/blob/master/packages/jest-editor-support/src/parsers/BabylonParser.js#L46 ? We shouldn't make assumptions about the syntax that the user is using.
cc @orta
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this should be exported, especially since the only other user is within this file. I'd recommend to either remove this export or to split it up into two modules (like proposed in my previous comment) so that this is actually modular, like:
getMetadata(parse(file))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is that you can require this and override the default parser with a different one (TypeScript maybe?).
Ideally I should make the two Visitors I'm using configurable as well, despite the Visitiors I've used are pretty standard across different ASTs so it should work as it is.
I'd like suggestion for a better API, I though this was a good compromise to avoid having to pass all the configurations for the default case and still be a bit extensible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, ideally the babel lookup should work here 👍
}, | ||
}; | ||
|
||
SnapshotSupport.plugins = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should also be possible to get rid of this when we extract the parsing part in this package.
exportName: string, | ||
node: Node, | ||
content?: string, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would exists be false? Can we remove it?
- I recommend using
name
instead ofexportName
.export
doesn't add much value here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't want it to be confused with the filename, I guess if we need that I can just call it fileName.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh and yes exists can be false the first time you're writing the test as there's no snapshot yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, for exists
, that makes sense.
); | ||
|
||
const SnapshotSupport: SnapshotSupportType = { | ||
getMetadataFromTestFile(filePath: string): Array<SnapshotMetadata> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd recommend calling this something like:
Snapshot.getMetadata(…)
.
I don't think either the "fromTestFile" or "Support" need to be there. The editor support package already signals it is a "support package" and we only have one getter for metadata. If we ever add different files that may have snapshots (like regular files), then it makes sense to split it up into getMetadataFromTestFile
and getMetadataFromMockFile
for example.
|
||
const SnapshotSupport: SnapshotSupportType = { | ||
getMetadataFromTestFile(filePath: string): Array<SnapshotMetadata> { | ||
const fileContent = String(fs.readFileSync(filePath)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should already be a string, please pass utf8
as second arg instead of converting the buffer to a string.
}, | ||
}, | ||
state | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need to look for toMatchSnapshot
here? When you are calling this function below, shouldn't you have the node and be able to pass it in here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is for counting the number of toMatchSnapshots inside the parent so I can append the position
${describeLess}${fullName} ${position}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see now. You are using the walker to descend into toMatchSnapshot
first and then you are going back up to find the name, right? Is there a way we could track the parent nodes when we descend into the AST so that we don't have to go down and then back up the AST (for perf reasons)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The walker already does this, parents is an array of the parents, and I use that to select the nearest it
or test
parent.
Here though we would need to check for all toMatchSnapshots
if they have a common ancestor before starting this, and group by it.
Since I already filter the parents, it should be just a matter of moving the parents.filter
up inside the map and then comparing just the last parent (and I can even optimize this last bit by keeping a reference to the last parent and reset the index when it change or increment it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense.
// if `it` or `test` exists without a surrounding `describe` | ||
// then `test ` is prepended to the snapshot fullName | ||
describeLess = 'test '; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering if we could get rid of this in Jest but it would wipe away every single existing snapshot… :(
const parents = toMatchSnapshot.parents.filter( | ||
parent => ( | ||
parent.callee && | ||
['test', 'it', 'describe'].indexOf(parent.callee.name) !== -1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of doing an indexOf lookup here, could we use Object.create(null)
here so that lookups are O(1)? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, also, this should work for all of these here: https://github.com/cpojer/jest/blob/master/packages/eslint-plugin-jest/src/rules/no-identical-title.js#L15
Again, please use a map for these and hoist them to the top of the file :)
const innerAssertion = parents[parents.length - 1]; | ||
|
||
if (!innerAssertion || innerAssertion.callee.name === 'describe') { | ||
// dead code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you mean by this comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it means that there's a toMatchSnapshot
inside a describe
and that will never get executed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of "dead code" can you elaborate with the explanation from your comment?
const result = { | ||
content: undefined, | ||
exists: false, | ||
exportName: 'N/A', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we make this an empty string? I don't like "N/A" as a default.
// eslint-disable-next-line no-new-func | ||
const populate = new Function( | ||
'exports', | ||
String(fs.readFileSync(snapshotPath)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use 'utf8' instead of casting to string.
// reading and evaluating the snapshot | ||
// instead of just requiring to avoid filling node cache | ||
// eslint-disable-next-line no-new-func | ||
const populate = new Function( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is interesting, I'm wondering if we should just use this for snapshots as well instead of require..?
); | ||
|
||
const snapshots = {}; | ||
populate(snapshots); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this run the function once for each snapshot in the test? I feel like we should only do this once per file.
'use strict'; | ||
|
||
|
||
const excludes = excludeList => key => excludeList.indexOf(key) === -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a prototype-less object please! :)
d33cdee
to
065b59f
Compare
Should Snapshot be added to index.js exports? |
|
||
const fs = require('fs'); | ||
const walker = require('./walker'); | ||
const utils = require('jest-snapshot/build/utils'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you make it so jest-snapshot exports the utils in index.js? Packages in Jest aren't allowed to be accessed through /build/ etc.
}; | ||
|
||
// create a lookup table from an array. | ||
const lookupFromArray = array => array.reduce( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this really necessary?
const describeVariants = Object.assign(Object.create(null), {
describe: true,
fdescribe: true,
xdescribe: true,
});
seems fine to me, without additional runtime overhead of looping over an array and modifying an object.
|
||
getMetadata(filePath: string): Array<SnapshotMetadata> { | ||
const fileContent = fs.readFileSync(filePath, 'utf8'); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rm, go easy on the newlines, man!
const fileContent = fs.readFileSync(filePath, 'utf8'); | ||
|
||
const fileNode = this._parser(fileContent, this._parserOptions); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this one too. You can group all the constants together.
}, state); | ||
|
||
let lastParent = null; | ||
let count = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
put let assignments after const, if possible.
}; | ||
|
||
if (!innerAssertion || isDescribe(innerAssertion.callee)) { | ||
// an expectation inside a describe never get executed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An expectation inside describe never gets executed.
Nice! I think we are getting closer to something we can ship! :) @bookman25 is right, Snapshot should be exported in index.js. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see comments
421adaa
to
b007791
Compare
b007791
to
7caa8c0
Compare
:( |
Not sure why this PR was closed. The functionality seems super helpful. Is there something we can give a hand on or is this functionality not desirable to be included in jest editor support ? |
Feel free to bring back this pull request and address all the outstanding comments, we are more than happy to merge it! :) |
@cpojer The GitHub interface only shows outdated comments, I cannot understand which parts should be changed for this to happen. This would bring very good dev experience to editors. For example showing only the diff inline the editor and a button to accept the change. |
@vvo I'm not sure either, could you bring this PR back to life and make it work with the current codebase and we'll take it from there? :) |
Following jestjs#2629, I rebased the code on current master. I still have to actually test the feature with jest-community/vscode-jest#73. What i would like to add ultimately is the ability to: - preview the snapshot - have the snapshot diff - have a button to accept diff per snapshot (would update it) WDYT?
* first iteration * Use babel-traverse * comments on the pr * feat(jest-editor-support): Add Snapshot metadata Following #2629, I rebased the code on current master. I still have to actually test the feature with jest-community/vscode-jest#73. What i would like to add ultimately is the ability to: - preview the snapshot - have the snapshot diff - have a button to accept diff per snapshot (would update it) WDYT?
(This PR was followed and merged in #4570) |
* first iteration * Use babel-traverse * comments on the pr * feat(jest-editor-support): Add Snapshot metadata Following jestjs#2629, I rebased the code on current master. I still have to actually test the feature with jest-community/vscode-jest#73. What i would like to add ultimately is the ability to: - preview the snapshot - have the snapshot diff - have a button to accept diff per snapshot (would update it) WDYT?
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
Cross editor support for getting snapshot metadata from a test file
This will enable extending various editor to show something similar to this
Test plan
I've add a couple of tests, I might have missed something (that's why I've add WIP on the title) but seems pretty solid.
how to use it
look at SnapshotSupport-test.js
I've add a small configuration that should enable to change the parser if in the future we or someone else needs it, as well as the plugin array