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

e2e: add FlashList tests #216

Merged
merged 13 commits into from
Mar 25, 2022
Merged

e2e: add FlashList tests #216

merged 13 commits into from
Mar 25, 2022

Conversation

ElviraBurchik
Copy link
Contributor

@ElviraBurchik ElviraBurchik commented Mar 22, 2022

Description

Adds two test cases:

  • Twitter example built with FlashList looks the same on a snapshot
  • Twitter example built with FlashList looks the same as the one with FlatList

I still need to resolve two issues:

  • saving diff if snapshots differ
  • Android snapshots doesn't look the same as emulator - created an issue to fix it later, since we don't run e2e tests on Android on CI it can wait

Reviewers’ hat-rack 🎩

  • Run test locally, confirm that it works with dev run-e2e-ios
  • See that android fails with dev run-e2e-android, but visual diff is saved to diff folder
  • Delete e2e/artifacts folde, run tests - reference artifacts should be created again

Screenshots or videos (if needed)

Checklist

@ElviraBurchik ElviraBurchik self-assigned this Mar 22, 2022
@ElviraBurchik ElviraBurchik force-pushed the e2e/add-flash-list-tests branch from 92716f9 to 3f1bc96 Compare March 23, 2022 09:57
@ElviraBurchik ElviraBurchik marked this pull request as ready for review March 23, 2022 13:59
Copy link
Contributor

@fortmarek fortmarek left a 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 Show resolved Hide resolved
Comment on lines 38 to 75
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");
Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Collaborator

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.

Comment on lines 71 to 73
function (err) {
if (err) throw err;
}
Copy link
Contributor

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

);
}
} else {
// Save reference screenshot cause it doesn't exist yet
Copy link
Contributor

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)

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

Copy link
Contributor

@fortmarek fortmarek Mar 24, 2022

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.

Copy link
Contributor Author

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:

  1. change the code to use record = true
  2. run tests, re-record snapshots
  3. 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?

Copy link
Contributor

@fortmarek fortmarek Mar 24, 2022

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

Copy link
Contributor Author

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.

Copy link
Contributor

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.


it("Twitter with FlashList looks the same", async () => {
const testArtifactsLocation = ensureArtifactsLocation(
`twitter_with_flash_list`,
Copy link
Contributor

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

Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point 👍

});
} else {
// enter demo mode
execSync("adb shell settings put global sysui_demo_allowed 1");
Copy link
Contributor

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

Comment on lines 9 to 12
export function pixelDifference(
referencePath: String,
toMatchPath: String
): PNG | null {
Copy link
Contributor

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?

Copy link
Collaborator

@naqvitalha naqvitalha left a 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.

Comment on lines 38 to 75
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");
Copy link
Collaborator

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.

@ElviraBurchik ElviraBurchik force-pushed the e2e/add-flash-list-tests branch 2 times, most recently from ba7b71e to 5d28f08 Compare March 24, 2022 14:47
Copy link
Contributor

@fortmarek fortmarek left a 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 👀

Comment on lines 34 to 35
).takeScreenshot(testName);

if (referenceExists(testName)) {
assertSnapshot(testRunScreenshotPath, testName);
} else {
saveReference(testRunScreenshotPath, testName);
}
Copy link
Contributor

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?

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

saveDiff(diffPNG, `${testName}_diff.png`);

throw new Error(
"There is difference between reference screenshot and test run screenshot"
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, done

fixture/src/Detox/SnapshotLocation.ts Outdated Show resolved Hide resolved

import { pixelDifference } from "./PixelDifference";

export const assertSnapshot = (snapshotPath: string, testName: string) => {
Copy link
Contributor

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?


it("Twitter with FlashList looks the same", async () => {
// TODOs: Get it from Jest
// expect.getState().currentTestName - doesn't work
Copy link
Contributor

Choose a reason for hiding this comment

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

is it undefined or?

Copy link
Contributor Author

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?

).takeScreenshot(flatTwitterReferenceName);

if (!referenceExists(testName)) {
saveReference(testRunScreenshotPath, testName);
Copy link
Contributor

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

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 wouldn't work for the first test case then

Copy link
Contributor

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.

Copy link
Contributor

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?

Comment on lines 58 to 56
const flashListReference = referenceExists(
"Twitter with FlashList looks the same"
);
Copy link
Contributor

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

Copy link
Contributor Author

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

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

Copy link
Contributor

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.

@ElviraBurchik ElviraBurchik force-pushed the e2e/add-flash-list-tests branch 2 times, most recently from c086039 to d28bd69 Compare March 25, 2022 08:21
Copy link
Contributor

@fortmarek fortmarek left a 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 🏁

"verbose": true,
"preset": "react-native",
"transformIgnorePatterns": [
"<rootDir>/node_modules/(?!((jest-)?react-native|@react-native(-community)?)/)"
Copy link
Contributor

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.

if (referenceExists(flashListReferenceTestName)) {
assertSnapshot(testRunScreenshotPath, flashListReferenceTestName);
} else {
saveReference(testRunScreenshotPath, flashListReferenceTestName);
Copy link
Contributor

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.

).takeScreenshot(flatTwitterReferenceName);

if (!referenceExists(testName)) {
saveReference(testRunScreenshotPath, testName);
Copy link
Contributor

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?

assertSnapshots(flatListReference, flashListReference, testName);
} else {
throw new Error(
"One of the references doesn't exist. Please run the tests again."
Copy link
Contributor

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.

Comment on lines 15 to 25
throw new Error(
`There is difference between reference screenshot and test run screenshot. See diff: ${diffPath}`
);
Copy link
Contributor

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

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

@ElviraBurchik ElviraBurchik force-pushed the e2e/add-flash-list-tests branch from e46c21f to 0e1fdc2 Compare March 25, 2022 15:02
Copy link
Contributor

@fortmarek fortmarek left a 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 👏

Comment on lines 49 to 56
assertSnapshot(testRunScreenshotPath, testName);

// Assert that FlatList reference is the same as with FlashList
assertSnapshotsEqual(
referenceExists(flashListReferenceTestName),
referenceExists(testName),
testName
);
Copy link
Contributor

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

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.

Copy link
Contributor Author

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

});

it("Twitter with FlatList looks the same as with FlashList", async () => {
// TODOs: Get it from Jest
Copy link
Contributor

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.

) => {
if (!firstPath) {
throw new Error(
"First screenshot path is null. Please make sure that you have a screenshot before running this assertion."
Copy link
Contributor

@fortmarek fortmarek Mar 25, 2022

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
@ElviraBurchik ElviraBurchik merged commit eff6d4e into main Mar 25, 2022
@ElviraBurchik ElviraBurchik deleted the e2e/add-flash-list-tests branch March 25, 2022 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants