Make Fastlane Snapshot Work with React Native
fastlane snapshot
on react native app requires quite some effortsUPDATE 09/24/2021
Since we wrote this article, our code base has experienced significant change, including an almost rewrite of the app itself and upgrade of most of the tools we use (e.g. Xcode, React Native, and fastlane). Unsurprisingly, these changes break the UI Test script we wrote before. But what is surprising to us is that we could no longer get snapshot
to work using the same app.otherElements["some_id"]
trick as before. After a lot of painful trail-and-error, we document below our current UI Test set up for snapshot
.
- Dummy button
- UI Test file
- Do not use
Touchable
forsnapshot
dummy buttons. For reasons unbeknown to us,Touchable
elements do not show its accessibility identifier even if bothtestID
andaccessibilityLabel
have been specified. Without the accessibility identifier, it is very cumbersome selecting thesnapshot
dummy buttons to tap. We eventually settle for usingText
as button (yes,Text
component hasonPress
prop), according to the suggestion from here. One major benefit of usingText
is that it is recognized byXCTest
asstaticTexts
, so we can be more specific targeting our dummy buttons. The selection query becomesapp.staticTexts["accessibility_identifier"]
, where the accessibility identifier is the string assigned totestID
(NOT the actual text placed in theText
component). - Use Accessibility Inspector to check whether the designed accessibility identifier has been assigned to the dummy button. Follow this answer to start the Accessibility Inspector. Make sure the existence of the accessibility identifier is confirmed before triggering the UI Test.
- Be careful when wrapping a dummy button under a higher level
Touchable
component. According to this answer, React NativeTouchable
component hasaccessible
set to true by default. This means all the children under theTouchable
are considered one unit and none of their testIDs will be accessible as identifiers. If this happens to be the case (in our case, it is), one must explicitly set the top levelTouchable
component with propaccessible={false}
in order to obtain identifiers for its children. We have been bitten hard and wasted a lot of time by this peculiarity.
UPDATE 02/10/2021
for the Android version, i.e. fastlane screengrab
, we have shared our experience in Make Fastlane Screengrab Work with React Native.
We recently submitted an app made in react native to Apple App Store. The whole process was highly complex, but with the help of fastlane
, it was bumpy but still manageable…until we started to generate screenshots of the app. Screenshots are required by Apple App Store for two sizes of iPhone (6.5-inch and 5.5-inch) and two versions of iPad Pro (12.9-inch 2nd and 3rd generation). Although screenshots can be produced manually, it is not a scalable solution once the numbers of device types and supported languages increase. Hence, automated screenshot generation is unavoidable.
fastlane
offers a command snapshot
to automatically take screenshots of an app, leveraging the UITest capability of Xcode. Unfortunately, we encountered quite a few issues when going through the doc of fastlane snapshot
. We would like to share our experience below and show our current solutions that either resolve or work around the issues.
Incorporate SnapshotHelper.swift
Upon running fastlane snapshot init
, a SnapshotHelper.swift
file will be generated. It contains the definition of setSnapshot
and snapshot
function that are used in the UI test (i.e., if this file is not included, calling setSnapshot
or snapshot
in UI test results in error). This file must be incorporated into the UI test target. To add it to the UI test target, right click on the UI test target on the left column of Xcode, choose “Add Files to …”, select SnapshotHelper.swift
, and tick “copy item if needed”.
“Timestamped Event Matching Error”
We followed the tutorial recommended in fastlane snapshot
doc, but once the UITest started to record, tapping a button either was not recorded or faced with a“Timestamped Event Matching Error”. Often times Xcode would even hang and not respond when recording was ended. Google and SO search seemed to suggest that the problem was due to not having provided testID
to the button component. We proceeded to add testID
to the buttons, yet the problem persisted. It seemed to us that no matter what we did, we simply could not tame the beast of Xcode to record any app actions in UITest.
Fortunately, we stumbled upon this repo, which showed that by adding testID
to TouchableHighlight
, Xcode UITest was able to recognize TouchableHighlight
and execute actions associated with it. Since all our buttons were written using the react-native-elements library, we didn’t have a TouchableHighlight
laying around. So we created one for testing purpose. While tapping it still wasn’t recorded by UITest in Xcode, fastlane snapshot
was able to call XCUIApplication().otherElements[‘some_test_id’].tap()
defined in xxx_appUITests.swift
. This was a breakthrough! At least, we found a way to trigger something in the app during fastlane snapshot
.
We also found that one of the comments in an issue on fastlane
mentioned that they used dummy buttons to trigger redux state changes in order to take desired screenshots. This seemed to us a very hacky workaround and initially we didn’t want to pursue it. Yet, now it was the only option left. So we added a few dummy TouchableHighlight
buttons with unique testID
, set up their callbacks to change the screen to desired states, and viola, fastlane snapshot
worked as expected.
Hide The Dummy Buttons
The dummy button hack worked. The next problem was to hide them when screenshot was taken. Since the screenshot was taken programmatically, we thought the dummy buttons could be easily hidden using any of the following approaches:
- Not providing text in the dummy button, something like this
<View>
<TouchableHighlight
onPress={() => callback()}
testID="some_test_id">
<Text></Text>
</TouchableHighlight>
</View>
2. Place the dummy button behind the main screen via z-index
trick
3. Make the dummy button text transparent.
However, it turned out approach 1 and 2 didn’t work, because when configured like that, the dummy buttons became untappable (i.e., they were too hidden). Only approach 3 worked, provided that another tweak was also made: the dummy buttons must have the style {position: 'absolute'}
to elevate them above everything else. Otherwise, they would occupy space on the screen and mess up the layout of other visible components.
Remove The Dummy Buttons in Production
Approach 3 successfully hid the dummy buttons during screenshots, but if we released the app like that, those dummy buttons would be exposed to users. Thus, we wanted to remove the dummy buttons in the production release. This required that the rendering of the dummy buttons to be contingent on some environment variable, e.g. the env for screenshot was not production:
This approach worked. Now the production release did not contain the dummy buttons. However, it led to another problem: we had to take screenshots under dev
instead of prod
. If there is no difference between the dev
and prod
versions of the app, it is not an issue. However, if there is difference between the two versions, e.g. some immature features exist in dev
but not ready for prod
, one will have to mock the dev
version as prod
, while at the same time also specify that screenshot is available.
Unfortunately, our app fell in the second scenario, and we had to create a new environment variable screenshot
to determine whether the dummy buttons should be rendered (i.e. instead of checking if (env === 'prod')
, we checked if (screenshot === 'true')
) and use env=prod
for the development environment temporarily. Furthermore, since the screenshots were taken under dev
, we had to also disable yellow box warning when screenshot was available.
This rather convoluted approach worked. We were able to take screenshots with the dummy buttons present (but invisible and not affecting the app layout) and only showing the production ready features.
Avoid Environment Variables Cache
Since we made changes from env=dev
to env=prod
in one of our env files, we must run npx react-native start --reset-cache
manually to load such change when metro bundler was started. Otherwise, the previously cached environment variables would be used, irrespective of the change in the env file.
To automate this workflow in Fastfile
, we relied on tmux
to start the cache-free metro bundler before calling fastlane snapshot
.
Wrong Screenshot Dimension
Everything seemed to be working now. We were able to use undetectable dummy buttons to automatically generate screenshots and remove the dummy buttons in production release. The next hurdle was that the dimension of the raw screenshot did not match the requirement specified by Apple App Store. Dimension of screenshot has been an issue for fastlane
in the past, and it should be resolved by using fastlane frameit
. fastlane frameit
puts a device frame surrounding a raw screenshot and allows for additional marketing text on top or bottom of the framed screenshot. In our case, the output of fastlane frameit
seemed to meet the dimension requirements, but just to be safe, we also implemented a script to guarantee that the final screenshot output matched the dimension requirement perfectly:
This script required installation of imagemagick. It can be used like this: DEVICE="iPhone 11" resize_screenshot.sh
Missing iPad Pro (12.9-inch) (2nd generation) frame
The final obstacle was the missing iPad Pro (12.9-inch)(2nd generation) frame. Screenshots for iPad Pro (12.9-inch)(2nd generation) is required by Apple App Store, yet its frame was mysteriously missing in the latest frame folder. After some digging, we found out that the frame name for iPad Pro (12.9-inch)(2nd generation) is actually Apple iPad Pro and it lives in the 1572609390 folder .
We downloaded the “Apple iPad Pro Space Grey” frame to ~/.fastlane/frameit/latest/
and the problem was resolved.
Summary
Taking screenshots is a required step before releasing an app to Apple App Store. Although doing it manually is feasible at the beginning, it is not a scalable approach once the numbers of device types and supported languages grow in the future. fastlane snapshot
provides an automated way to generate screenshots programmatically, yet we have found that this tool does not work very well with react native apps out of the box. In this simple write up, we have documented our struggles with fastlane snapshot
, and provided our solution/work-arounds to smooth out all the kinks. We hope this write up will help some poor soul who is shaking his monitor right now and asking the ultimate question in the universe: why is this not working?! (we do not deny nor admit that we have done the same thing)