Last active
July 13, 2022 13:26
-
-
Save Cosmo/bd7720bf42e3bc5e7822010ba8aa350c to your computer and use it in GitHub Desktop.
Take screenshot with Xcode Test Plans
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env xcrun swift | |
import Foundation | |
// Todos | |
// [ ] Create missing simulators if needed | |
// [ ] Test on fresh new install of macOS / Xcode | |
// [ ] Get scheme and testPlan from command line argument | |
// Ideas | |
// [ ] What about tvOS, watchOS, ..? | |
// Add devices for your screenshots. | |
let devices: [Device] = [ | |
Device(name: "iPhone 11 Pro Max", | |
idiom: .phone, | |
homeStyle: .indicator, | |
displaySize: "6.5"), | |
Device(name: "iPhone 8 Plus", | |
idiom: .phone, | |
homeStyle: .button, | |
displaySize: "5.5"), | |
Device(name: "iPad Pro (12.9-inch) (5th generation)", | |
idiom: .pad, | |
homeStyle: .indicator, | |
displaySize: "12.9"), | |
Device(name: "iPad Pro (12.9-inch) (2nd generation)", | |
idiom: .pad, | |
homeStyle: .button, | |
displaySize: "12.9"), | |
] | |
// Replace the placeholders with your scheme and test plan. | |
let scheme = "SCHEME_NAME" | |
let testPlan = "TESTPLAN_NAME" | |
// Do not edit anything starting from here. | |
func start(devices: [Device], scheme: String, testPlan: String) { | |
let targetFolder = "\(Project.path)/Screenshots" | |
let derivedDataPath = "/private/tmp/\(Project.name)/DerivedData" | |
for device in devices { | |
let screenshotsPath = "\(targetFolder)/\(device.description)/\(device.name)" | |
print("Starting Simulator: \(device.name)") | |
Shell.run("xcrun simctl boot \(device.name.quoted())") | |
print("Simulator started.") | |
print("Setting Status Bar …") | |
device.setStatusBar() | |
print("Status Bar set.") | |
print("Deleting Derived Data …") | |
Shell.run("rm -rf \(derivedDataPath.quoted())") | |
print("Creating Screenshots Directory …") | |
Shell.run("mkdir -p \(screenshotsPath.quoted())") | |
print("Running Tests …") | |
Shell.run("xcodebuild test -scheme \(scheme) -destination \"platform=iOS Simulator,name=\(device.name)\" -testPlan \(testPlan) -derivedDataPath \(derivedDataPath.quoted())") | |
print("Copying Screenshots …") | |
Shell.run("find \"\(derivedDataPath)/Logs/Test\" -maxdepth 1 -type d -exec xcparse screenshots {} \(screenshotsPath.quoted()) \\;") | |
// run("xcrun simctl shutdown \"\(device.name)\"") | |
} | |
} | |
extension String { | |
func quoted() -> String { | |
return "\u{22}\(self)\u{22}" | |
} | |
} | |
struct Shell { | |
@discardableResult | |
static func safeShell(_ command: String) throws -> String { | |
let task = Process() | |
let pipe = Pipe() | |
task.standardOutput = pipe | |
task.standardError = pipe | |
task.arguments = ["-c", command] | |
task.executableURL = URL(fileURLWithPath: "/bin/zsh") | |
task.standardInput = nil | |
try task.run() | |
let data = pipe.fileHandleForReading.readDataToEndOfFile() | |
let output = String(data: data, encoding: .utf8)! | |
return output.replacingOccurrences(of: "\n", with: "") | |
} | |
@discardableResult | |
static func run(_ command: String) -> String? { | |
do { | |
return try safeShell(command) | |
} | |
catch { | |
print("\(error)") | |
} | |
return nil | |
} | |
} | |
struct Project { | |
static var path: String { | |
return Shell.run("pwd") ?? "" | |
} | |
static var name: String { | |
return String(path.split(separator: "/").last ?? "project") | |
} | |
} | |
struct Device: CustomStringConvertible { | |
let name: String | |
let idiom: Idiom | |
let homeStyle: HomeStyle | |
let displaySize: String | |
enum Idiom: CustomStringConvertible { | |
case pad | |
case phone | |
var description: String { | |
switch self { | |
case .pad: | |
return "iPad" | |
case .phone: | |
return "iPhone" | |
} | |
} | |
} | |
enum HomeStyle: CustomStringConvertible { | |
case indicator | |
case button | |
var description: String { | |
switch self { | |
case .indicator: | |
return "Home Indicator" | |
case .button: | |
return "Home Button" | |
} | |
} | |
} | |
func setStatusBar() { | |
switch self.idiom { | |
case .pad: | |
Device.setStatusBarPad(name: self.name) | |
case .phone: | |
switch self.homeStyle { | |
case .button: | |
Device.setStatusBarPhoneWithHomeButton(name: self.name) | |
case .indicator: | |
Device.setStatusBarPhoneWithHomeIndicator(name: self.name) | |
} | |
} | |
} | |
static func setStatusBarPhoneWithHomeButton(name: String) { | |
Shell.run("xcrun simctl status_bar \"\(name)\" clear") | |
Shell.run("xcrun simctl status_bar \"\(name)\" override --time \"9:41 AM\" --wifiBars 3 --cellularBars 4 --operatorName \"\"") | |
Shell.run("xcrun simctl spawn \"\(name)\" defaults write com.apple.springboard SBShowBatteryPercentage 1") | |
} | |
static func setStatusBarPhoneWithHomeIndicator(name: String) { | |
Shell.run("xcrun simctl status_bar \"\(name)\" clear") | |
Shell.run("xcrun simctl status_bar \"\(name)\" override --time \"9:41\" --wifiBars 3 --cellularBars 4") | |
} | |
static func setStatusBarPad(name: String) { | |
Shell.run("xcrun simctl status_bar \"\(name)\" clear") | |
Shell.run("xcrun simctl status_bar \"\(name)\" override --time \"9:41 AM\" --wifiBars 3 --wifiMode active") | |
Shell.run("xcrun simctl spawn \"\(name)\" defaults write com.apple.springboard SBShowBatteryPercentage 1") | |
Shell.run("xcrun simctl spawn \"\(name)\" defaults write com.apple.UIKit StatusBarHidesDate 1") | |
} | |
var description: String { | |
return "\(idiom.description) \(displaySize)-inch with \(homeStyle.description)" | |
} | |
} | |
start(devices: devices, scheme: scheme, testPlan: testPlan) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment