Generating App Screenshots Using Xamarin.UITest

With a white label app platform like ours at Olo, generating screenshots for every app we publish can be a real chore. It makes sense for us to use simulators/emulators to do this, given the large number of device sizes we need to generate for, so it's not something we can easily farm out to a non-engineer to do manually without them spending an obscene amount of time on it. If you haven't noticed already, we're big on automation, so I set out to see if we could automate this as well.

Xamarin.UITest is a great library from Xamarin that makes it really easy to write UI tests in C#/F# for your iOS and Android apps. Under the hood, Xamarin.UITest uses Calabash in order to facilitate this functionality. You can run these tests both locally as well as in Test Cloud, which means you can easily take your tests and run them across Test Cloud's large device farm.

For this example I'll focus on iOS, but the same code will also work for Android, minus the iOS-specific parts.

Listing all iOS Simulators

These days it's necessary to submit screenshots for your apps across many different screen sizes for iOS. To start out I wanted to just pull screenshots for every iOS simulator configuration on my system. Eventually I'll probably fine tune this to only pick the ones it absolutely needs. Here's some code in F# that returns a list of all the available iOS simulators:

module SimulatorHelpers
 
open System.IO
open System.Xml

let listAvailableSimulators = fun simulatorRoot ->
    Directory.EnumerateFiles(simulatorRoot, "device.plist", SearchOption.AllDirectories)
    |> Seq.map (fun path ->
        let plistDoc = new XmlDocument()
        plistDoc.LoadXml(File.ReadAllText(path))
        let dictNodes = plistDoc.GetElementsByTagName("dict").Item(0).ChildNodes
 
        let values = (
            seq { for i in 0..dictNodes.Count - 1 do if i % 2 = 0 then yield i }
            |> Seq.map (fun i -> (dictNodes.Item(i).InnerText, dictNodes.Item(i + 1).InnerText)))
        let getValue = fun(key) -> values |> Seq.pick (fun (k, v) -> if key = k then Some(v) else None)
 
        (getValue "name", getValue "UDID")
    )

Generating Screenshots

Now that the list of simulators is available, we can pass those to Xamarin.UITest and run our tests. In our system we have classes defined already that represent each screen in our app, in order to make it easy to write UI tests around these screens. For this example, we'll launch the app in each simulator and grab a couple screenshots while tapping through it.

If you've used Test Cloud before you're already familiar with how it takes screenshots every time you call app.Screenshot(), but what you might not have noticed is that the method actually returns a FileInfo object for the locally saved file. All we need to do is copy that file over to wherever we need it, and we have our screenshot!

Here's a console app in F# that ties this all together:

open System
open System.IO
open SimulatorHelpers
open Xamarin.UITest
open UITests.Core.Screens

[<EntryPoint>]
let main argv = 
    let screenshotFolderPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())
    Directory.CreateDirectory(screenshotFolderPath) |> ignore

    let saveScreenshot deviceFolderPath screenshotNumber (file:FileInfo) =
        let screenshotPath = Path.Combine(deviceFolderPath, (screenshotNumber.ToString() + ".png"))
        file.CopyTo(screenshotPath)

    listAvailableSimulators "/Users/gshackles/Library/Developer/CoreSimulator/Devices" 
    |> Seq.iter (fun (name, udid) ->
        Console.WriteLine("Generating screenshots for " + name)

        let deviceFolderPath = Path.Combine(screenshotFolderPath, name)
        Directory.CreateDirectory(deviceFolderPath) |> ignore

        let app = ConfigureApp
                    .Debug().EnableLocalScreenshots()
                    .iOS
                    .AppBundle("path/to/my.app")
                    .DeviceIdentifier(udid)
                    .StartApp()
        let takeScreenshot number = app.Screenshot(number.ToString()) |> saveScreenshot deviceFolderPath number |> ignore

        let loggedOutHomeScreen = LoggedOutHomeScreen(app)
        loggedOutHomeScreen.WaitUntilLoaded()
        takeScreenshot 1

		let findLocationsScreen = loggedOutHomeScreen.StartNewOrder()
        findLocationsScreen.WaitUntilLoaded()
        takeScreenshot 2
    )

    0

Once this finishes running, the temporary folder created here contains folders for each simulator, and files in each for each screenshot taken.

comments powered by Disqus
Navigation