Skip to content
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

Closed

Conversation

kentaromiura
Copy link
Contributor

Summary
Cross editor support for getting snapshot metadata from a test file

This will enable extending various editor to show something similar to this
https://twitter.com/kentaromiura/status/821459907408859136

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

@@ -11,7 +11,8 @@
"typescript": "^2.0.10"
},
"dependencies": {
"babylon": "^6.13.0"
"babylon": "^6.13.0",
"astroll": "^1.0.0"
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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?

Copy link
Contributor Author

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, {
Copy link
Contributor Author

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.

@cpojer
Copy link
Member

cpojer commented Jan 18, 2017

@orta we are still getting the missing @flow annotations on test files :(

@orta
Copy link
Member

orta commented Jan 18, 2017

That sucks, time to take a look

@kentaromiura
Copy link
Contributor Author

Once danger support is solid it would be nice to add it to the lint script in the package.json.

@codecov-io
Copy link

codecov-io commented Jan 23, 2017

Current coverage is 68.75% (diff: 95.65%)

Merging #2629 into master will increase coverage by 0.39%

@@             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          

Powered by Codecov. Last update 88d0503...2a324d1


test('snapshot-test.example', () => {
const filePath = path.join(snapshotFixturePath, 'snapshot-test.example');
const required = require(
Copy link
Member

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', () => {
Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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?

Copy link
Contributor Author

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`] = `
Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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

Copy link
Member

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))

Copy link
Contributor Author

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.

Copy link
Member

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 = [
Copy link
Member

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,
};
Copy link
Member

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 of exportName. export doesn't add much value here.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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> {
Copy link
Member

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));
Copy link
Member

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
);
Copy link
Member

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?

Copy link
Contributor Author

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}

Copy link
Member

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)?

Copy link
Contributor Author

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)

Copy link
Member

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 ';
}
Copy link
Member

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
Copy link
Member

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)? :)

Copy link
Member

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.
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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',
Copy link
Member

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))
Copy link
Member

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(
Copy link
Member

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);
Copy link
Member

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;
Copy link
Member

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! :)

@bookman25
Copy link
Contributor

Should Snapshot be added to index.js exports?


const fs = require('fs');
const walker = require('./walker');
const utils = require('jest-snapshot/build/utils');
Copy link
Member

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(
Copy link
Member

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');

Copy link
Member

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);

Copy link
Member

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;
Copy link
Member

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.
Copy link
Member

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.

@cpojer
Copy link
Member

cpojer commented Jan 26, 2017

Nice! I think we are getting closer to something we can ship! :) @bookman25 is right, Snapshot should be exported in index.js.

Copy link
Member

@cpojer cpojer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comments

@kentaromiura kentaromiura force-pushed the snapshot-editor-support branch 2 times, most recently from 421adaa to b007791 Compare March 21, 2017 18:00
@thymikee
Copy link
Collaborator

:(

@gcazaciuc
Copy link

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 ?

@cpojer
Copy link
Member

cpojer commented Apr 28, 2017

Feel free to bring back this pull request and address all the outstanding comments, we are more than happy to merge it! :)

@vvo
Copy link
Contributor

vvo commented Sep 14, 2017

@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.

@cpojer
Copy link
Member

cpojer commented Sep 15, 2017

@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? :)

vvo pushed a commit to algolia/jest that referenced this pull request Sep 30, 2017
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?
cpojer pushed a commit that referenced this pull request Sep 30, 2017
* 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?
@vvo
Copy link
Contributor

vvo commented Oct 1, 2017

(This PR was followed and merged in #4570)

tabrindle pushed a commit to tabrindle/jest that referenced this pull request Oct 2, 2017
* 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?
@github-actions
Copy link

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.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 13, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants