Skip to content

Commit 0d072cc

Browse files
committed
[#234] fastlane으로 스크린샷 자동화 스크립트 생성
1 parent 3a75340 commit 0d072cc

File tree

3 files changed

+358
-2
lines changed

3 files changed

+358
-2
lines changed

.gitignore

+1-2
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,4 @@ Tuist/.build
7474
*.plist
7575

7676
### Fastlane ###
77-
fastlane/screenshots
78-
fastlane/test_output
77+
screenshots

Snapfile

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Uncomment the lines below you want to change by removing the # in the beginning
2+
3+
# A list of devices you want to take the screenshots from
4+
devices([
5+
"iPhone 16 Pro",
6+
# "iPhone 16 Pro Max",
7+
# "iPhone 16",
8+
# "iPhone 16 Plus",
9+
"iPhone SE (3rd generation)"
10+
# "iPhone 8",
11+
# "iPhone 8 Plus",
12+
# "iPhone SE",
13+
# "iPhone X",
14+
# "iPad Pro (12.9-inch)",
15+
# "iPad Pro (9.7-inch)",
16+
# "Apple TV 1080p",
17+
# "Apple Watch Series 6 - 44mm"
18+
])
19+
20+
languages([
21+
"ko-KR",
22+
# "en-US",
23+
# "de-DE",
24+
# "it-IT",
25+
# ["pt", "pt_BR"] # Portuguese with Brazilian locale
26+
])
27+
28+
# The name of the scheme which contains the UI Tests
29+
scheme("EATSSUUITests")
30+
31+
# Where should the resulting screenshots be stored?
32+
# output_directory("./screenshots")
33+
34+
# remove the '#' to clear all previously generated screenshots before creating new ones
35+
clear_previous_screenshots(true)
36+
37+
# Remove the '#' to set the status bar to 9:41 AM, and show full battery and reception. See also override_status_bar_arguments for custom options.
38+
override_status_bar(true)
39+
40+
# Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments
41+
# launch_arguments(["-favColor red"])
42+
43+
# For more information about all available options run
44+
# fastlane action snapshot

SnapshotHelper.swift

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

0 commit comments

Comments
 (0)