Skip to content

Commit 5f0107a

Browse files
committed
[#48] Fastlane으로 스크린샷 촬영 자동화
1 parent b900e65 commit 5f0107a

File tree

12 files changed

+977
-35
lines changed

12 files changed

+977
-35
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,7 @@ Tuist/.build
7171

7272
### Configurtion files ###
7373
*.xcconfig
74-
*.plist
74+
*.plist
75+
76+
### Fastlane ###
77+
fastlane/screenshots

EATSSU_MVC/EATSSU_MVC/UITests/SampleTests.swift

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,23 @@
88
import XCTest
99

1010
final class SampleTests: XCTestCase {
11-
12-
override func setUpWithError() throws {
13-
// Put setup code here. This method is called before the invocation of each test method in the class.
14-
15-
// In UI tests it is usually best to stop immediately when a failure occurs.
16-
continueAfterFailure = false
17-
18-
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19-
}
20-
21-
override func tearDownWithError() throws {
22-
// Put teardown code here. This method is called after the invocation of each test method in the class.
23-
}
24-
25-
func testExample() throws {
26-
// UI tests must launch the application that they test.
27-
let app = XCUIApplication()
28-
app.launch()
29-
30-
// Use XCTAssert and related functions to verify your tests produce the correct results.
31-
}
32-
33-
func testLaunchPerformance() throws {
34-
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
35-
// This measures how long it takes to launch your application.
36-
measure(metrics: [XCTApplicationLaunchMetric()]) {
37-
XCUIApplication().launch()
38-
}
39-
}
40-
}
11+
12+
@MainActor
13+
override func setUpWithError() throws {
14+
continueAfterFailure = false
15+
let app = XCUIApplication()
16+
setupSnapshot(app)
17+
app.launch()
18+
}
19+
20+
override func tearDownWithError() throws {
21+
22+
}
23+
24+
25+
/// 스크린샷 촬영 자동화 스크립트 입니다.
26+
@MainActor
27+
func testTakingSnapShot() {
28+
snapshot("01_LoginScreen")
29+
}
4130
}
Lines changed: 313 additions & 0 deletions
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+
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+
@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+
return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true)
62+
}
63+
static var deviceLanguage = ""
64+
static var currentLocale = ""
65+
66+
open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
67+
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 let error {
78+
NSLog(error.localizedDescription)
79+
}
80+
}
81+
82+
class func setLanguage(_ app: XCUIApplication) {
83+
guard let cacheDirectory = self.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 = self.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 = self.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 = self.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 let error {
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 { context 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 = self.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 self.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 self.containing(isStatusBar)
302+
}
303+
}
304+
305+
private extension CGFloat {
306+
func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool {
307+
return 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]

EATSSU_MVC/EATSSU_MVC/UITests/Sources/UITests-Bridging-Header.h

Lines changed: 0 additions & 4 deletions
This file was deleted.

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
source "https://rubygems.org"
2+
3+
gem "fastlane"

0 commit comments

Comments
 (0)