Storm Sim Requires 180 Screenshots
or how not to cry yourself to sleep
Storm Sim has a free and paid version. It supports six languages. Two devices at three resolutions. If the UI changes in any significant way, it needs 180 screenshots. Uploading them in iTunes Connect is bad enough, but navigating through the app in each language is just killer. I find myself not wanting to make a change because I know it will require updating the screenshots. That's bad.
So here's another Pro Tip for you: Use Instruments to automate your screenshot taking. Would you like to know more?
First, create a UtilityScripts.js file in your Xcode workspace (so it will get checked into source control and versioned; you will cry bitter, bitter tears if all this hard work gets eaten). Here is a good basic set of utility functions:
var target = UIATarget.localTarget();
var app = target.frontMostApp();
var mainWindow = app.mainWindow();
var tabBar = mainWindow.tabBar();
function withOrientation(orientation, func) {
target.delay(2.0);
var oldOrientation = target.deviceOrientation();
UIALogger.logMessage("current device orientation: " + oldOrientation);
target.setDeviceOrientation(orientation);
target.delay(4.0);
try {
func();
}
finally {
target.delay(2.0);
target.setDeviceOrientation(oldOrientation);
target.delay(2.0);
}
}
function runTest(doRun, name, func, options) {
if(!doRun) return;
if(options == null) {
options = {
logTree: true
};
}
UIALogger.logStart(name);
try {
func(name, options);
UIALogger.logPass(name);
}
catch(e) {
UIALogger.logError(e);
if(options.logTree)
target.logElementTree();
UIALogger.logFail(name);
}
}
First we setup some shortcut vars to common things, then we setup a helper function to change orientations, then finally a test runner method that will log the tree of UI elements on failure, as well as indicate success/fail to UIAutomation
so we get fancy green/red indicators for pass/fail in the Instruments UI.
Now go to Xcode, Product, Profile (or Command-I). Select the Automator template. Save the Instruments setup or you'll regret it; I also recommend checking "Continuously Log Results" and make sure the folder you select is where you placed your Instruments file. Screenshots will appear inside Instruments but if you attempt to open them in Preview or do anything with them they'll show as unreadable/corrupted unless you follow this advice. Wherever you save this, there will be subfolders created for each Instruments run and that's where you will find the screenshots written as PNGs, so make sure you create separate folders for different projects or they'll end up all mixed up.
The good news is you can use the Simulator in Retina mode and it will grab high-res screenshots, even if the simulator is showing up as scaled. This is really helpful for iPad Retina screenshots as almost no one can fit that whole thing on the screen. It also doesn't matter if the simulator window is covered up - it grabs the backing buffer directly.
On the left, under Scripts, click Add, then Create Script. Give it a name like Screenshots.js. Note: Instruments will continuously save this file without prompting so don't open it in another editor while Instruments is open. If you import a script it gets copied over; so far I haven't found a way to reference a file in source control without copying it so watch out for backing up your scripts. You can certainly add your tests to another file in Xcode, then have your Instruments script just call one main() function in there.
What, pray tell, will we do with this script?
#import "/full/path/to/xcode/project/TestScripts/Utilities.js"
var CLOCK = 0;
var NOWPLAYING = CLOCK + 1;
var LIBRARY = CLOCK + 2;
var OPTIONS = CLOCK + 3;
var STORE = CLOCK + 4;
target.delay(4.0);
The first thing is to include Utility.js from your workspace in this script, which confusingly has nothing to do with the import command in the Instruments UI. Next I setup constants for my UI tabs. It's a good idea to have each test/screenshot function explicitly navigate to the place it wants to be in the UI, then navigate back to the root so you can keep things sensible... otherwise you will have very painful order-of-execution issues without realizing it.
Now one important thing to remember for screenshots is that UIAutomation
doesn't necessarily wait for animations to complete, so you may need to insert delays to prevent your screenshots from being jacked up. I've got a target.delay(4.0)
item in there to pause and wait for my app to truly finish starting up. UIAutomation tries to wait for some things automatically, e.g.: if you tell a table cell button to "tap" then try to grab the new table view sliding onto screen, it will wait for that new view controller to setup, then display it's table view, but in my experience anything involving screenshots just required me to manually inject delays, otherwise the screenshots were a mess.
runTest(true, "clockShot", function() {
withOrientation(UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT, function() {
tabBar.buttons()[CLOCK].tap();
target.delay(2.0);
target.captureScreenWithName("001-ClockScreenshot");
});
});
Ah, finally! Here is our first test. The first parameter lets me quickly disable a test without having to comment out the code. "clockShot" is the name of the test displayed in Instruments.
I'm using the orientation helper function to make sure we switch to landscape orientation for the duration of the test, then switch back to whatever it was originally (portrait) when done. Then I find the clock button on the tab bar and tap it. This selects the clock view, and I wait 2 seconds to make sure any animations are done. Then I capture a screenshot which will be named "001-ClockScreenshot.png" and placed in the /path/to/instruments/file/Run XYZ
folder. Note: Orientation stuff is sometimes flaky so watch out, you may need to re-run the test or manually fuss with the simulator to get it to work properly.
Now let's get into something a bit more interesting:
runTest(false, "optionsWakeTimer", function() {
tabBar.buttons()[OPTIONS].tap();
target.delay(1.0);
var table = mainWindow.tableViews()[0];
var cell = table.cells()[2];
cell.tap();
target.delay(1.0);
table = mainWindow.tableViews()[0];
cell = table.cells()[0];
cell.switches()[0].tap();
target.delay(1.0);
target.captureScreenWithName("005-WakeTimer");
});
Here you can see how you can manipulate the UI element tree to locate controls. I grab the first available table view (usually only one unless on iPad), then grab the third cell (index 2) and tap it. Then I re-grab the table view because the view hierarchy has changed. I locate the first cell, grab the first switch control, then tap it.
Notice something? Almost all the element traversal is accomplished with a JS function that returns a UIAElementArray
containing any elements that matched the request. There are a variety of predicates you can use to do all sorts of fancy things, for example table.cells().firstWithName("xyz")
which uses the Accessibility name to locate the first cell matching xyz
. You can also do things like withPredicate("someprop begins with 'x'")
which would return all controls where a property some prop
is a string starting with the character x
. You can discover the types of the elements (typeof(cell) == UIATableCell)
, etc.
Since I am switching the simulator language to capture the localized screenshots, I can't use the name because that is also localized, so I navigate by indexes. It's definitely more brittle but so far I haven't found a better solution.
Once you stop recording in Instruments it kills the app, then you can switch the simulator hardware (Target, Options, Simulator Hardware to switch iOS versions or iPhone vs iPad... retina vs non-retina just use the Simulator Hardware menu) and hit Record again no problem. Same when switching languages: stop recording, go to Settings, General, International, Language in the simulator and it will reboot the simulator in another language. Now hit Record and Bam! localized screenshots. *With this setup I only have to do 36 things and the screenshots all turn out identical with no mistakes (6 language changes, three hardware changes, two apps) and those things are considerably faster. *
You can find the complete JS reference here: UIAutomation JS Reference. Most of the actual UI elements like tables, buttons, etc derive from UIAElement
so start there for some interesting things. UIAElementArray
is another good one and has all the find/firstWith/etc methods.
I've included a screenshot of what this looks like after running; the green indicates pass, but the first one shows red because of a failure. Notice how you can navigate the UI hierarchy and see just the listed control on the right under "Screenshot".
This blog represents my own personal opinion and is not endorsed by my employer.