SnapshotHelper.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. //
  2. // SnapshotHelper.swift
  3. // Example
  4. //
  5. // Created by Felix Krause on 10/8/15.
  6. // Copyright © 2015 Felix Krause. All rights reserved.
  7. //
  8. // -----------------------------------------------------
  9. // IMPORTANT: When modifying this file, make sure to
  10. // increment the version number at the very
  11. // bottom of the file to notify users about
  12. // the new SnapshotHelper.swift
  13. // -----------------------------------------------------
  14. import Foundation
  15. import XCTest
  16. var deviceLanguage = ""
  17. var locale = ""
  18. func setupSnapshot(_ app: XCUIApplication) {
  19. Snapshot.setupSnapshot(app)
  20. }
  21. func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
  22. if waitForLoadingIndicator {
  23. Snapshot.snapshot(name)
  24. } else {
  25. Snapshot.snapshot(name, timeWaitingForIdle: 0)
  26. }
  27. }
  28. /// - Parameters:
  29. /// - name: The name of the snapshot
  30. /// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait.
  31. func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
  32. Snapshot.snapshot(name, timeWaitingForIdle: timeout)
  33. }
  34. enum SnapshotError: Error, CustomDebugStringConvertible {
  35. case cannotDetectUser
  36. case cannotFindHomeDirectory
  37. case cannotFindSimulatorHomeDirectory
  38. case cannotAccessSimulatorHomeDirectory(String)
  39. case cannotRunOnPhysicalDevice
  40. var debugDescription: String {
  41. switch self {
  42. case .cannotDetectUser:
  43. return "Couldn't find Snapshot configuration files - can't detect current user "
  44. case .cannotFindHomeDirectory:
  45. return "Couldn't find Snapshot configuration files - can't detect `Users` dir"
  46. case .cannotFindSimulatorHomeDirectory:
  47. return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable."
  48. case .cannotAccessSimulatorHomeDirectory(let simulatorHostHome):
  49. return "Can't prepare environment. Simulator home location is inaccessible. Does \(simulatorHostHome) exist?"
  50. case .cannotRunOnPhysicalDevice:
  51. return "Can't use Snapshot on a physical device."
  52. }
  53. }
  54. }
  55. @objcMembers
  56. class Snapshot: NSObject {
  57. static var app: XCUIApplication?
  58. static var cacheDirectory: URL?
  59. static var screenshotsDirectory: URL? {
  60. return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true)
  61. }
  62. class func setupSnapshot(_ app: XCUIApplication) {
  63. Snapshot.app = app
  64. do {
  65. let cacheDir = try pathPrefix()
  66. Snapshot.cacheDirectory = cacheDir
  67. setLanguage(app)
  68. setLocale(app)
  69. setLaunchArguments(app)
  70. } catch let error {
  71. print(error)
  72. }
  73. }
  74. class func setLanguage(_ app: XCUIApplication) {
  75. guard let cacheDirectory = self.cacheDirectory else {
  76. print("CacheDirectory is not set - probably running on a physical device?")
  77. return
  78. }
  79. let path = cacheDirectory.appendingPathComponent("language.txt")
  80. do {
  81. let trimCharacterSet = CharacterSet.whitespacesAndNewlines
  82. deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
  83. app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"]
  84. } catch {
  85. print("Couldn't detect/set language...")
  86. }
  87. }
  88. class func setLocale(_ app: XCUIApplication) {
  89. guard let cacheDirectory = self.cacheDirectory else {
  90. print("CacheDirectory is not set - probably running on a physical device?")
  91. return
  92. }
  93. let path = cacheDirectory.appendingPathComponent("locale.txt")
  94. do {
  95. let trimCharacterSet = CharacterSet.whitespacesAndNewlines
  96. locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
  97. } catch {
  98. print("Couldn't detect/set locale...")
  99. }
  100. if locale.isEmpty {
  101. locale = Locale(identifier: deviceLanguage).identifier
  102. }
  103. app.launchArguments += ["-AppleLocale", "\"\(locale)\""]
  104. }
  105. class func setLaunchArguments(_ app: XCUIApplication) {
  106. guard let cacheDirectory = self.cacheDirectory else {
  107. print("CacheDirectory is not set - probably running on a physical device?")
  108. return
  109. }
  110. let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt")
  111. app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"]
  112. do {
  113. let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8)
  114. let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: [])
  115. let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count))
  116. let results = matches.map { result -> String in
  117. (launchArguments as NSString).substring(with: result.range)
  118. }
  119. app.launchArguments += results
  120. } catch {
  121. print("Couldn't detect/set launch_arguments...")
  122. }
  123. }
  124. class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
  125. if timeout > 0 {
  126. waitForLoadingIndicatorToDisappear(within: timeout)
  127. }
  128. print("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work
  129. sleep(1) // Waiting for the animation to be finished (kind of)
  130. #if os(OSX)
  131. XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: [])
  132. #else
  133. guard let app = self.app else {
  134. print("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
  135. return
  136. }
  137. let screenshot = app.windows.firstMatch.screenshot()
  138. guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return }
  139. let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
  140. do {
  141. try screenshot.pngRepresentation.write(to: path)
  142. } catch let error {
  143. print("Problem writing screenshot: \(name) to \(path)")
  144. print(error)
  145. }
  146. #endif
  147. }
  148. class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) {
  149. #if os(tvOS)
  150. return
  151. #endif
  152. let networkLoadingIndicator = XCUIApplication().otherElements.deviceStatusBars.networkLoadingIndicators.element
  153. let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator)
  154. _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout)
  155. }
  156. class func pathPrefix() throws -> URL? {
  157. let homeDir: URL
  158. // on OSX config is stored in /Users/<username>/Library
  159. // and on iOS/tvOS/WatchOS it's in simulator's home dir
  160. #if os(OSX)
  161. guard let user = ProcessInfo().environment["USER"] else {
  162. throw SnapshotError.cannotDetectUser
  163. }
  164. guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else {
  165. throw SnapshotError.cannotFindHomeDirectory
  166. }
  167. homeDir = usersDir.appendingPathComponent(user)
  168. #else
  169. #if arch(i386) || arch(x86_64)
  170. guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else {
  171. throw SnapshotError.cannotFindSimulatorHomeDirectory
  172. }
  173. guard let homeDirUrl = URL(string: simulatorHostHome) else {
  174. throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome)
  175. }
  176. homeDir = URL(fileURLWithPath: homeDirUrl.path)
  177. #else
  178. throw SnapshotError.cannotRunOnPhysicalDevice
  179. #endif
  180. #endif
  181. return homeDir.appendingPathComponent("Library/Caches/tools.fastlane")
  182. }
  183. }
  184. private extension XCUIElementAttributes {
  185. var isNetworkLoadingIndicator: Bool {
  186. if hasWhiteListedIdentifier { return false }
  187. let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20)
  188. let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3)
  189. return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize
  190. }
  191. var hasWhiteListedIdentifier: Bool {
  192. let whiteListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"]
  193. return whiteListedIdentifiers.contains(identifier)
  194. }
  195. func isStatusBar(_ deviceWidth: CGFloat) -> Bool {
  196. if elementType == .statusBar { return true }
  197. guard frame.origin == .zero else { return false }
  198. let oldStatusBarSize = CGSize(width: deviceWidth, height: 20)
  199. let newStatusBarSize = CGSize(width: deviceWidth, height: 44)
  200. return [oldStatusBarSize, newStatusBarSize].contains(frame.size)
  201. }
  202. }
  203. private extension XCUIElementQuery {
  204. var networkLoadingIndicators: XCUIElementQuery {
  205. let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in
  206. guard let element = evaluatedObject as? XCUIElementAttributes else { return false }
  207. return element.isNetworkLoadingIndicator
  208. }
  209. return self.containing(isNetworkLoadingIndicator)
  210. }
  211. var deviceStatusBars: XCUIElementQuery {
  212. let deviceWidth = XCUIApplication().frame.width
  213. let isStatusBar = NSPredicate { (evaluatedObject, _) in
  214. guard let element = evaluatedObject as? XCUIElementAttributes else { return false }
  215. return element.isStatusBar(deviceWidth)
  216. }
  217. return self.containing(isStatusBar)
  218. }
  219. }
  220. private extension CGFloat {
  221. func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool {
  222. return numberA...numberB ~= self
  223. }
  224. }
  225. // Please don't remove the lines below
  226. // They are used to detect outdated configuration files
  227. // SnapshotHelperVersion [1.9]