-
Notifications
You must be signed in to change notification settings - Fork 288
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
e2e: add FlashList tests #216
Conversation
92716f9
to
3f1bc96
Compare
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.
Some initial comments. I think most importantly we should work on how to write these tests with as minimal code as possible, so we can add them easily in the future.
fixture/e2e/flashList.test.e2e.js
Outdated
const testRunScreenshotPath = await element( | ||
by.id("FlashList") | ||
).takeScreenshot(flashTwitterReferenceName); | ||
|
||
// Path where we want to save the screenshot | ||
const flatListReferencePath = path.resolve( | ||
testArtifactsLocation, | ||
flashTwitterReferenceName | ||
); | ||
|
||
// If reference is already there, compare the two screenshots | ||
if (fs.existsSync(flatListReferencePath)) { | ||
console.log(`testRunScreenshotPath ${testRunScreenshotPath}`); | ||
console.log(`flatListReferencePath ${flatListReferencePath}`); | ||
// compare screenshots, get difference | ||
const diffPNG = pixelDifference( | ||
testRunScreenshotPath, | ||
flatListReferencePath | ||
); | ||
|
||
// If there is difference, fail the test | ||
if (diffPNG !== null) { | ||
saveDiff(diffPNG, "flash_twitter_looks_the_same_diff", platform); | ||
|
||
throw new Error( | ||
"There is difference between reference screenshot and test run screenshot" | ||
); | ||
} | ||
} else { | ||
// Save reference screenshot cause it doesn't exist yet | ||
fs.renameSync( | ||
testRunScreenshotPath, | ||
flatListReferencePath, | ||
function (err) { | ||
if (err) throw err; | ||
} | ||
); | ||
console.log("Reference screenshot created"); |
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 love if this was extracted into a simple method such as:
assertSnapshot(matching: element, record: boolean) // not sure what type `element` is
We can also change it in a way to test that it's the same as before and the same as FlatList
:
assertSnapshots(elementA: element, elementB: element) // this can probably be "recorded" every time
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.
For reference, I am taking inspiration from this Swift library as I liked its API quite a lot back when I used 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.
+1
We need to provide a simple API to write new tests. assert
should only require an element and abstract out path related complexity. We should only need to worry about getting to element. Record is also helpful when we want to easily update things.
assertSnapshots
is also great and in cases where both elements are not there on the screen at the same time there could be helpers like captureElement
and it's output can also go to assertSnapshots
.
fixture/e2e/flashList.test.e2e.js
Outdated
function (err) { | ||
if (err) throw err; | ||
} |
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.
usage of arrow function would be easier to read here imho
fixture/e2e/flashList.test.e2e.js
Outdated
); | ||
} | ||
} else { | ||
// Save reference screenshot cause it doesn't exist 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.
How do you re-take screenshots? Do you need to delete the file? I think a record
boolean that tells you whether to take the snapshot again would make sense (as mentioned below)
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 created reference snapshots and committed them to the repo. Now every test run compares new screenshots with reference once and asserts if they don't match with saving a diff snapshot. There is no need to delete anything in regular cases, when do you think it makes sense to record them again?
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 need to delete anything in regular cases, when do you think it makes sense to record them again?
We will have to re-run screenshots every time we change the fixture. Additionally, we will also need to re-run them when we upgrade the simulators/emulators we run the screenshots on (it happens quite often things change with the OS version upgrade).
We should into making this as seamless as possible - deleting screenshots manually from the file system is imho tedious and error-prone. I believe we should unlock this functionality from code instead.
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.
Right, then if it's in code updating the snapshots gonna look the following:
- change the code to use
record = true
- run tests, re-record snapshots
- change the code back?
Not cool.
I can add a dev command instead to delete the artifacts and run tests to generate new snapshots instead. What do you think?
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.
Not cool.
I actually don't mind that set up. The big benefit is that you don't need to switch context when you want to re-record something. Having a dev command means going to command line, copy-paste the test name, and run the command. When you write a new test, you might want to re-record quite often which is why it's imho worth it to have it in code as it's so much easier to use. With the extra command, you would have to delete the screenshot after each run.
The biggest drawback is that you might forget to delete it - we can create an extra lint rule if that becomes a problem. But I think the pros outweigh the cons in this case. But I'm sure we're not the first creating such snapshot tests via detox - are there any other opensource or Shopify alternatives that we can inspire ourselves from?
I can add a dev command instead to delete the artifacts and run tests to generate new snapshots instead. What do you think?
What do I do if I want to delete a specific artifact? I suppose we could add possibility to specify the file name but if we abstract the artifact name to be derived from the test name, it might be tedious.
Additionally, we should probably avoid writing custom dev
commands as we will most likely move away from dev.yml
in the future as outside contributors won't be able to use it once we opensource the repo (that being said, we can still define custom commands in package.json
).
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.
Honestly, modifying the code only to make it re-record snapshot and then changing back is longer then manually deleting the folder corresponding to the test case. I will take a look into existing solutions, but having a record options doesn't thrill me for now.
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.
Honestly, modifying the code only to make it re-record snapshot and then changing back is longer then manually deleting the folder corresponding to the test case
Could you explain why it is longer? But yeah, if you find some other examples for this, let me know! Curious to hear what @naqvitalha thinks about this, too.
fixture/e2e/flashList.test.e2e.js
Outdated
|
||
it("Twitter with FlashList looks the same", async () => { | ||
const testArtifactsLocation = ensureArtifactsLocation( | ||
`twitter_with_flash_list`, |
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 optionally infer the name from the test name as mentioned 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.
additionally, I don't think this needs to be in backticks or?
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.
Good point 👍
fixture/e2e/DetoxHelpers.js
Outdated
}); | ||
} else { | ||
// enter demo mode | ||
execSync("adb shell settings put global sysui_demo_allowed 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.
I would personally prefer await exec
since the method is async anyway but probably won't make much difference
fixture/e2e/DetoxHelpers.js
Outdated
export function pixelDifference( | ||
referencePath: String, | ||
toMatchPath: String | ||
): PNG | null { |
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 this file be in typescript as I see you are declaring the types 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.
We need an easier API and that's all that I see that we need to really change.
fixture/e2e/flashList.test.e2e.js
Outdated
const testRunScreenshotPath = await element( | ||
by.id("FlashList") | ||
).takeScreenshot(flashTwitterReferenceName); | ||
|
||
// Path where we want to save the screenshot | ||
const flatListReferencePath = path.resolve( | ||
testArtifactsLocation, | ||
flashTwitterReferenceName | ||
); | ||
|
||
// If reference is already there, compare the two screenshots | ||
if (fs.existsSync(flatListReferencePath)) { | ||
console.log(`testRunScreenshotPath ${testRunScreenshotPath}`); | ||
console.log(`flatListReferencePath ${flatListReferencePath}`); | ||
// compare screenshots, get difference | ||
const diffPNG = pixelDifference( | ||
testRunScreenshotPath, | ||
flatListReferencePath | ||
); | ||
|
||
// If there is difference, fail the test | ||
if (diffPNG !== null) { | ||
saveDiff(diffPNG, "flash_twitter_looks_the_same_diff", platform); | ||
|
||
throw new Error( | ||
"There is difference between reference screenshot and test run screenshot" | ||
); | ||
} | ||
} else { | ||
// Save reference screenshot cause it doesn't exist yet | ||
fs.renameSync( | ||
testRunScreenshotPath, | ||
flatListReferencePath, | ||
function (err) { | ||
if (err) throw err; | ||
} | ||
); | ||
console.log("Reference screenshot created"); |
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.
+1
We need to provide a simple API to write new tests. assert
should only require an element and abstract out path related complexity. We should only need to worry about getting to element. Record is also helpful when we want to easily update things.
assertSnapshots
is also great and in cases where both elements are not there on the screen at the same time there could be helpers like captureElement
and it's output can also go to assertSnapshots
.
ba7b71e
to
5d28f08
Compare
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.
Looking much better 👏 I have not done much research whether such a library exists but seems like it's general enough that we can potentially think of extracting it out in the future 👀
fixture/e2e/flashList.test.e2e.js
Outdated
).takeScreenshot(testName); | ||
|
||
if (referenceExists(testName)) { | ||
assertSnapshot(testRunScreenshotPath, testName); | ||
} else { | ||
saveReference(testRunScreenshotPath, testName); | ||
} |
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.
much better 👏
Can we extract this into a separate and reusable method as well?
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 I don't want to do it for now - I'm not 100% happy with this approach it might be changed soon. Right now I'd prefer it to be super obvious what's happening for a person reading the test case.
fixture/src/Detox/SnapshotAsserts.ts
Outdated
saveDiff(diffPNG, `${testName}_diff.png`); | ||
|
||
throw new Error( | ||
"There is difference between reference screenshot and test run screenshot" |
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 output the path of the 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.
Good idea, done
|
||
import { pixelDifference } from "./PixelDifference"; | ||
|
||
export const assertSnapshot = (snapshotPath: string, testName: 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.
I'd probably change the snapshotPath
to element
and take the snapshot inside the file. I don't see a need to enforce users of the API to do so or?
fixture/e2e/flashList.test.e2e.js
Outdated
|
||
it("Twitter with FlashList looks the same", async () => { | ||
// TODOs: Get it from Jest | ||
// expect.getState().currentTestName - doesn't work |
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 it undefined
or?
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.
getState
is undefined.
Based on this Github issue expect.getState().currentTestName
is not supposed to work at all. It has been working, but it's accidental?
fixture/e2e/flashList.test.e2e.js
Outdated
).takeScreenshot(flatTwitterReferenceName); | ||
|
||
if (!referenceExists(testName)) { | ||
saveReference(testRunScreenshotPath, testName); |
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 put the referenceExists
check into the saveReference
method
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 wouldn't work for the first test case then
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 so? I don't see why it should not work for the first test case, too.
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 see now - then we can run saveReference
inside assertSnapshot
instead?
fixture/e2e/flashList.test.e2e.js
Outdated
const flashListReference = referenceExists( | ||
"Twitter with FlashList looks the same" | ||
); |
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 makes this test dependent on the Twitter with FlashList looks the same
test - I don't think that's the best practice.
I'd either:
- combine these two tests into one, so you can ensure
FlashList
screenshot is always taken - keep these tests separate and extract taking the
FlashList
screenshot into a helper method. We can then run it in both tests
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, I don't like either
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'm not fond of the first one, because in this case we will always have a fresh screenshot of FlashList which might differ from reference and we might not know about it. The reference is supposed to be source of truth.
With the second one, getting the FlashList reference is already a one line
const reference = referenceExists("Twitter with FlashList looks the same");
the best I can do here is to move the test name constant out.
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.
because in this case we will always have a fresh screenshot of FlashList which might differ from reference and we might not know about it
Why is that the case? You can always check whether the reference exists and only take a new screenshot when it does not.
With the second one, getting the FlashList reference is already a one line
const reference = referenceExists("Twitter with FlashList looks the same");
Yeah, but the issue is you still take the screenshot in a different test, coupling those tests together. We should avoid that.
c086039
to
d28bd69
Compare
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.
Just minor comments. I think we can make the API to assert snapshots just a little bit 🤏 better but it's already pretty convenient. Let me know @ElviraBurchik if you wanted to pair on it to finish it off 🏁
fixture/e2e/config.json
Outdated
"verbose": true, | ||
"preset": "react-native", | ||
"transformIgnorePatterns": [ | ||
"<rootDir>/node_modules/(?!((jest-)?react-native|@react-native(-community)?)/)" |
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.
Based on our pairing session, I don't think this is necessary anymore.
fixture/e2e/flashList.test.e2e.js
Outdated
if (referenceExists(flashListReferenceTestName)) { | ||
assertSnapshot(testRunScreenshotPath, flashListReferenceTestName); | ||
} else { | ||
saveReference(testRunScreenshotPath, flashListReferenceTestName); |
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 remember from swift-snapshot-testing
library that they failed the test when you were creating a new test. It does make kind of sense but you might want to check out the approach of jest-image-snapshot
, too.
fixture/e2e/flashList.test.e2e.js
Outdated
).takeScreenshot(flatTwitterReferenceName); | ||
|
||
if (!referenceExists(testName)) { | ||
saveReference(testRunScreenshotPath, testName); |
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 see now - then we can run saveReference
inside assertSnapshot
instead?
fixture/e2e/flashList.test.e2e.js
Outdated
assertSnapshots(flatListReference, flashListReference, testName); | ||
} else { | ||
throw new Error( | ||
"One of the references doesn't exist. Please run the tests again." |
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 also probably extract this out to assertSnapshots
. It would be also helpful to know which references don't exist.
fixture/src/Detox/SnapshotAsserts.ts
Outdated
throw new Error( | ||
`There is difference between reference screenshot and test run screenshot. See diff: ${diffPath}` | ||
); |
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.
wouldn't it be better to fail the test instead of throwing an error?
testName: string | ||
) => { | ||
if (!firstPath) { | ||
throw new Error( |
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.
Throwing an error here, because Jest's fail
function is not available - jestjs/jest#11698
e46c21f
to
0e1fdc2
Compare
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.
Just a couple of nits from my side, but I think we can more-or-less merge it now ✅ well done, Elvira, the API is almost perfect now 👏
fixture/e2e/flashList.test.e2e.js
Outdated
assertSnapshot(testRunScreenshotPath, testName); | ||
|
||
// Assert that FlatList reference is the same as with FlashList | ||
assertSnapshotsEqual( | ||
referenceExists(flashListReferenceTestName), | ||
referenceExists(testName), | ||
testName | ||
); |
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 API is really nice ❤️
console.log(`Reference screenshot for test name "${testName}" was created`); | ||
}; | ||
|
||
export const referenceExists = (testName: string): string | null => { |
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 find this misleading - we can just call it reference
instead? referenceExists
implies to me it will return a boolean
instead of path to the image.
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.
fair, it used to return boolean
thus the name, but now it's indeed misleading
fixture/e2e/flashList.test.e2e.js
Outdated
}); | ||
|
||
it("Twitter with FlatList looks the same as with FlashList", async () => { | ||
// TODOs: Get it from Jest |
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 think we can just remove this.
fixture/src/Detox/SnapshotAsserts.ts
Outdated
) => { | ||
if (!firstPath) { | ||
throw new Error( | ||
"First screenshot path is null. Please make sure that you have a screenshot before running this assertion." |
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.
Let's put the actual firstPath
value here, too (same for secondPath
)
- Remove TODO - Update errors to include invalid path
Description
Adds two test cases:
FlashList
looks the same on a snapshotFlashList
looks the same as the one withFlatList
I still need to resolve two issues:
Reviewers’ hat-rack 🎩
dev run-e2e-ios
dev run-e2e-android
, but visual diff is saved todiff
foldere2e/artifacts
folde, run tests - reference artifacts should be created againScreenshots or videos (if needed)
Checklist