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