Mercurial > public > simoleon
comparison Simoleon/Helpers/SnapshotHelper.swift @ 171:70f0625bfcf1
Merge pull request #17 from denniscm190/development
open source project
committer: GitHub <noreply@github.com>
author | Dennis C. M. <dennis@denniscm.com> |
---|---|
date | Tue, 12 Oct 2021 16:17:35 +0200 |
parents | fastlane/SnapshotHelper.swift@05983a9275c1 fastlane/SnapshotHelper.swift@84137052813d |
children |
comparison
equal
deleted
inserted
replaced
146:f10b0e188905 | 171:70f0625bfcf1 |
---|---|
1 // | |
2 // SnapshotHelper.swift | |
3 // Example | |
4 // | |
5 // Created by Felix Krause on 10/8/15. | |
6 // | |
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 | |
15 import Foundation | |
16 import XCTest | |
17 | |
18 var deviceLanguage = "" | |
19 var locale = "" | |
20 | |
21 func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { | |
22 Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations) | |
23 } | |
24 | |
25 func snapshot(_ name: String, waitForLoadingIndicator: Bool) { | |
26 if waitForLoadingIndicator { | |
27 Snapshot.snapshot(name) | |
28 } else { | |
29 Snapshot.snapshot(name, timeWaitingForIdle: 0) | |
30 } | |
31 } | |
32 | |
33 /// - Parameters: | |
34 /// - name: The name of the snapshot | |
35 /// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. | |
36 func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { | |
37 Snapshot.snapshot(name, timeWaitingForIdle: timeout) | |
38 } | |
39 | |
40 enum SnapshotError: Error, CustomDebugStringConvertible { | |
41 case cannotFindSimulatorHomeDirectory | |
42 case cannotRunOnPhysicalDevice | |
43 | |
44 var debugDescription: String { | |
45 switch self { | |
46 case .cannotFindSimulatorHomeDirectory: | |
47 return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." | |
48 case .cannotRunOnPhysicalDevice: | |
49 return "Can't use Snapshot on a physical device." | |
50 } | |
51 } | |
52 } | |
53 | |
54 @objcMembers | |
55 open class Snapshot: NSObject { | |
56 static var app: XCUIApplication? | |
57 static var waitForAnimations = true | |
58 static var cacheDirectory: URL? | |
59 static var screenshotsDirectory: URL? { | |
60 return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) | |
61 } | |
62 | |
63 open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { | |
64 | |
65 Snapshot.app = app | |
66 Snapshot.waitForAnimations = waitForAnimations | |
67 | |
68 do { | |
69 let cacheDir = try getCacheDirectory() | |
70 Snapshot.cacheDirectory = cacheDir | |
71 setLanguage(app) | |
72 setLocale(app) | |
73 setLaunchArguments(app) | |
74 } catch let error { | |
75 NSLog(error.localizedDescription) | |
76 } | |
77 } | |
78 | |
79 class func setLanguage(_ app: XCUIApplication) { | |
80 guard let cacheDirectory = self.cacheDirectory else { | |
81 NSLog("CacheDirectory is not set - probably running on a physical device?") | |
82 return | |
83 } | |
84 | |
85 let path = cacheDirectory.appendingPathComponent("language.txt") | |
86 | |
87 do { | |
88 let trimCharacterSet = CharacterSet.whitespacesAndNewlines | |
89 deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) | |
90 app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] | |
91 } catch { | |
92 NSLog("Couldn't detect/set language...") | |
93 } | |
94 } | |
95 | |
96 class func setLocale(_ app: XCUIApplication) { | |
97 guard let cacheDirectory = self.cacheDirectory else { | |
98 NSLog("CacheDirectory is not set - probably running on a physical device?") | |
99 return | |
100 } | |
101 | |
102 let path = cacheDirectory.appendingPathComponent("locale.txt") | |
103 | |
104 do { | |
105 let trimCharacterSet = CharacterSet.whitespacesAndNewlines | |
106 locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) | |
107 } catch { | |
108 NSLog("Couldn't detect/set locale...") | |
109 } | |
110 | |
111 if locale.isEmpty && !deviceLanguage.isEmpty { | |
112 locale = Locale(identifier: deviceLanguage).identifier | |
113 } | |
114 | |
115 if !locale.isEmpty { | |
116 app.launchArguments += ["-AppleLocale", "\"\(locale)\""] | |
117 } | |
118 } | |
119 | |
120 class func setLaunchArguments(_ app: XCUIApplication) { | |
121 guard let cacheDirectory = self.cacheDirectory else { | |
122 NSLog("CacheDirectory is not set - probably running on a physical device?") | |
123 return | |
124 } | |
125 | |
126 let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") | |
127 app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] | |
128 | |
129 do { | |
130 let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) | |
131 let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) | |
132 let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) | |
133 let results = matches.map { result -> String in | |
134 (launchArguments as NSString).substring(with: result.range) | |
135 } | |
136 app.launchArguments += results | |
137 } catch { | |
138 NSLog("Couldn't detect/set launch_arguments...") | |
139 } | |
140 } | |
141 | |
142 open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { | |
143 if timeout > 0 { | |
144 waitForLoadingIndicatorToDisappear(within: timeout) | |
145 } | |
146 | |
147 NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work | |
148 | |
149 if Snapshot.waitForAnimations { | |
150 sleep(1) // Waiting for the animation to be finished (kind of) | |
151 } | |
152 | |
153 #if os(OSX) | |
154 guard let app = self.app else { | |
155 NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") | |
156 return | |
157 } | |
158 | |
159 app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) | |
160 #else | |
161 | |
162 guard self.app != nil else { | |
163 NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") | |
164 return | |
165 } | |
166 | |
167 let screenshot = XCUIScreen.main.screenshot() | |
168 #if os(iOS) | |
169 let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image | |
170 #else | |
171 let image = screenshot.image | |
172 #endif | |
173 | |
174 guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } | |
175 | |
176 do { | |
177 // The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices | |
178 let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ") | |
179 let range = NSRange(location: 0, length: simulator.count) | |
180 simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "") | |
181 | |
182 let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") | |
183 #if swift(<5.0) | |
184 UIImagePNGRepresentation(image)?.write(to: path, options: .atomic) | |
185 #else | |
186 try image.pngData()?.write(to: path, options: .atomic) | |
187 #endif | |
188 } catch let error { | |
189 NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png") | |
190 NSLog(error.localizedDescription) | |
191 } | |
192 #endif | |
193 } | |
194 | |
195 class func fixLandscapeOrientation(image: UIImage) -> UIImage { | |
196 #if os(watchOS) | |
197 return image | |
198 #else | |
199 if #available(iOS 10.0, *) { | |
200 let format = UIGraphicsImageRendererFormat() | |
201 format.scale = image.scale | |
202 let renderer = UIGraphicsImageRenderer(size: image.size, format: format) | |
203 return renderer.image { context in | |
204 image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) | |
205 } | |
206 } else { | |
207 return image | |
208 } | |
209 #endif | |
210 } | |
211 | |
212 class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { | |
213 #if os(tvOS) | |
214 return | |
215 #endif | |
216 | |
217 guard let app = self.app else { | |
218 NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") | |
219 return | |
220 } | |
221 | |
222 let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element | |
223 let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) | |
224 _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) | |
225 } | |
226 | |
227 class func getCacheDirectory() throws -> URL { | |
228 let cachePath = "Library/Caches/tools.fastlane" | |
229 // on OSX config is stored in /Users/<username>/Library | |
230 // and on iOS/tvOS/WatchOS it's in simulator's home dir | |
231 #if os(OSX) | |
232 let homeDir = URL(fileURLWithPath: NSHomeDirectory()) | |
233 return homeDir.appendingPathComponent(cachePath) | |
234 #elseif arch(i386) || arch(x86_64) || arch(arm64) | |
235 guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { | |
236 throw SnapshotError.cannotFindSimulatorHomeDirectory | |
237 } | |
238 let homeDir = URL(fileURLWithPath: simulatorHostHome) | |
239 return homeDir.appendingPathComponent(cachePath) | |
240 #else | |
241 throw SnapshotError.cannotRunOnPhysicalDevice | |
242 #endif | |
243 } | |
244 } | |
245 | |
246 private extension XCUIElementAttributes { | |
247 var isNetworkLoadingIndicator: Bool { | |
248 if hasAllowListedIdentifier { return false } | |
249 | |
250 let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) | |
251 let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) | |
252 | |
253 return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize | |
254 } | |
255 | |
256 var hasAllowListedIdentifier: Bool { | |
257 let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] | |
258 | |
259 return allowListedIdentifiers.contains(identifier) | |
260 } | |
261 | |
262 func isStatusBar(_ deviceWidth: CGFloat) -> Bool { | |
263 if elementType == .statusBar { return true } | |
264 guard frame.origin == .zero else { return false } | |
265 | |
266 let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) | |
267 let newStatusBarSize = CGSize(width: deviceWidth, height: 44) | |
268 | |
269 return [oldStatusBarSize, newStatusBarSize].contains(frame.size) | |
270 } | |
271 } | |
272 | |
273 private extension XCUIElementQuery { | |
274 var networkLoadingIndicators: XCUIElementQuery { | |
275 let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in | |
276 guard let element = evaluatedObject as? XCUIElementAttributes else { return false } | |
277 | |
278 return element.isNetworkLoadingIndicator | |
279 } | |
280 | |
281 return self.containing(isNetworkLoadingIndicator) | |
282 } | |
283 | |
284 var deviceStatusBars: XCUIElementQuery { | |
285 guard let app = Snapshot.app else { | |
286 fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") | |
287 } | |
288 | |
289 let deviceWidth = app.windows.firstMatch.frame.width | |
290 | |
291 let isStatusBar = NSPredicate { (evaluatedObject, _) in | |
292 guard let element = evaluatedObject as? XCUIElementAttributes else { return false } | |
293 | |
294 return element.isStatusBar(deviceWidth) | |
295 } | |
296 | |
297 return self.containing(isStatusBar) | |
298 } | |
299 } | |
300 | |
301 private extension CGFloat { | |
302 func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { | |
303 return numberA...numberB ~= self | |
304 } | |
305 } | |
306 | |
307 // Please don't remove the lines below | |
308 // They are used to detect outdated configuration files | |
309 // SnapshotHelperVersion [1.27] |