瀏覽代碼

[fastlane] Enable `fastlane screenshots`

To take framed screenshots, run `fastlane screenshots`
Note that the list of supported devices and languages is random for now.
Mike JS. Choi 7 年之前
父節點
當前提交
975ca70552

+ 4 - 0
VLC.xcodeproj/project.pbxproj

@@ -246,6 +246,7 @@
 		9BADAF45185FBD9D00108BD8 /* VLCFrostedGlasView.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BADAF44185FBD9D00108BD8 /* VLCFrostedGlasView.m */; };
 		9BE4D1CE183D76950006346C /* VLCCloudStorageTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D3784B0183A990F009EE944 /* VLCCloudStorageTableViewCell.m */; };
 		A79246C8170F11DF0036AAF2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A79246C6170F11DF0036AAF2 /* Localizable.strings */; };
+		CAF76D8E20709BE500E2AD7B /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF76D8D20709BE400E2AD7B /* SnapshotHelper.swift */; };
 		CC1BBC461704938300A20CBF /* libiconv.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CC1BBC451704938300A20CBF /* libiconv.dylib */; };
 		CC1BBC4C1704939B00A20CBF /* libsqlite3.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CC1BBC4B1704939B00A20CBF /* libsqlite3.dylib */; };
 		CC1BBC4E170493A300A20CBF /* libbz2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CC1BBC4D170493A300A20CBF /* libbz2.dylib */; };
@@ -942,6 +943,7 @@
 		A79246C9170F11E40036AAF2 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
 		AC40202FFE42CEDCEB37E50D /* Pods-VLC-tvOS.distribution.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VLC-tvOS.distribution.xcconfig"; path = "Pods/Target Support Files/Pods-VLC-tvOS/Pods-VLC-tvOS.distribution.xcconfig"; sourceTree = "<group>"; };
 		C6872E7B396534F3DAF4E48F /* Pods-VLC-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VLC-iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-VLC-iOS/Pods-VLC-iOS.release.xcconfig"; sourceTree = "<group>"; };
+		CAF76D8D20709BE400E2AD7B /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = fastlane/SnapshotHelper.swift; sourceTree = SOURCE_ROOT; };
 		CC1BBC451704938300A20CBF /* libiconv.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libiconv.dylib; path = usr/lib/libiconv.dylib; sourceTree = SDKROOT; };
 		CC1BBC471704938B00A20CBF /* libstdc++.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libstdc++.dylib"; path = "usr/lib/libstdc++.dylib"; sourceTree = SDKROOT; };
 		CC1BBC491704939300A20CBF /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
@@ -1230,6 +1232,7 @@
 				4187112A1F78F87200317B1A /* VLC_for_iOSTestVideoCodecs.m */,
 				41B0BC911F73EF1B0063BA26 /* VLC_for_IOSTestMenu.m */,
 				41B0BC8A1F73ED7D0063BA26 /* Info.plist */,
+				CAF76D8D20709BE400E2AD7B /* SnapshotHelper.swift */,
 			);
 			path = "VLC for iOSUITests";
 			sourceTree = "<group>";
@@ -3006,6 +3009,7 @@
 				7D3E528B1BD7B5E100309D15 /* VLCCloudServicesTVViewController.m in Sources */,
 				DD9D8F6B1C01FE5B00B4060F /* VLCPlaybackInfoCollectionViewDataSource.m in Sources */,
 				7D405ED41BEA150C006ED886 /* VLCActivityManager.m in Sources */,
+				CAF76D8E20709BE500E2AD7B /* SnapshotHelper.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 90 - 0
VLC.xcodeproj/xcshareddata/xcschemes/VLC for iOSUITests.xcscheme

@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "0930"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "NO"
+            buildForArchiving = "NO"
+            buildForAnalyzing = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "41B0BC851F73ED7D0063BA26"
+               BuildableName = "VLC for iOSUITests.xctest"
+               BlueprintName = "VLC for iOSUITests"
+               ReferencedContainer = "container:VLC.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "41B0BC851F73ED7D0063BA26"
+               BuildableName = "VLC for iOSUITests.xctest"
+               BlueprintName = "VLC for iOSUITests"
+               ReferencedContainer = "container:VLC.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "41B0BC851F73ED7D0063BA26"
+            BuildableName = "VLC for iOSUITests.xctest"
+            BlueprintName = "VLC for iOSUITests"
+            ReferencedContainer = "container:VLC.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "41B0BC851F73ED7D0063BA26"
+            BuildableName = "VLC for iOSUITests.xctest"
+            BlueprintName = "VLC for iOSUITests"
+            ReferencedContainer = "container:VLC.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 6 - 0
fastlane/Fastfile

@@ -64,6 +64,12 @@ lane :ci do
   test
 end
 
+desc 'Take screenshots'
+lane :screenshots do
+  capture_screenshots(stop_after_first_error: true)
+  frameit(white: true, path: './fastlane/screenshots')
+end
+
 #### Tests ####
 
 desc 'Run Tests'

+ 8 - 0
fastlane/README.md

@@ -46,16 +46,24 @@ This action does the following:
 - Update the changelog from the NEWS file
 
 - Push the version bump
+
 ### lint
 ```
 fastlane lint
 ```
 Check style and conventions
+
 ### ci
 ```
 fastlane ci
 ```
 
+### screenshots
+```
+fastlane screenshots
+```
+Take screenshots
+
 ### test
 ```
 fastlane test

+ 13 - 0
fastlane/Snapfile

@@ -0,0 +1,13 @@
+ devices([
+   "iPhone X",
+   "iPhone 8"
+ ])
+
+ languages([
+   "en",
+   "de",
+   "fr",
+ ])
+
+clear_previous_screenshots(true)
+

+ 281 - 0
fastlane/SnapshotHelper.swift

@@ -0,0 +1,281 @@
+//
+//  SnapshotHelper.swift
+//  Example
+//
+//  Created by Felix Krause on 10/8/15.
+//  Copyright © 2015 Felix Krause. All rights reserved.
+//
+
+// -----------------------------------------------------
+// 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().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.10]