diff --git a/.gitignore b/.gitignore index 3e69a10..cc74c10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +fastlane/metadata/review_information +fastlane/screenshots +fastlane/Preview.html +fastlane/report.xml + # Created by https://www.toptal.com/developers/gitignore/api/xcode,macos # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,macos diff --git a/Screenshots/Screenshots.swift b/Screenshots/Screenshots.swift new file mode 100644 index 0000000..4ca2487 --- /dev/null +++ b/Screenshots/Screenshots.swift @@ -0,0 +1,42 @@ +// +// Screenshots.swift +// Screenshots +// +// Created by Benjamin Erhart on 01.06.26. +// + +import XCTest + +final class Screenshots: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // 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. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testScreenshots() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + setupSnapshot(app) + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + // XCUIAutomation Documentation + // https://developer.apple.com/documentation/xcuiautomation + + snapshot("0-disabled") + + app.switches["dns-toggle"].tap() + + snapshot("1-enabled") + } +} diff --git a/dns.xcodeproj/project.pbxproj b/dns.xcodeproj/project.pbxproj index 534f1e4..12f178a 100644 --- a/dns.xcodeproj/project.pbxproj +++ b/dns.xcodeproj/project.pbxproj @@ -19,6 +19,13 @@ remoteGlobalIDString = A082640B2FC718790077B227; remoteInfo = Stickers; }; + A0F7AE542FCD9DF300C8CCFE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 069DCCF22F8C0DCD00F1EB16 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 069DCCF92F8C0DCE00F1EB16; + remoteInfo = dns; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -43,6 +50,7 @@ A06A74782F8E95410093A9E4 /* LICENCE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENCE; sourceTree = ""; }; A06A74792F8E95410093A9E4 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; A082640C2FC718790077B227 /* Stickers.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Stickers.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + A0F7AE4E2FCD9DF300C8CCFE /* Screenshots.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Screenshots.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -53,11 +61,28 @@ ); target = A082640B2FC718790077B227 /* Stickers */; }; + A0F7AE492FCD9C8B00C8CCFE /* Exceptions for "dns" folder in "dns" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 069DCCF92F8C0DCE00F1EB16 /* dns */; + }; + A0F7AE5D2FCD9E7F00C8CCFE /* Exceptions for "fastlane" folder in "Screenshots" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + SnapshotHelper.swift, + ); + target = A0F7AE4D2FCD9DF300C8CCFE /* Screenshots */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ 069DCCFC2F8C0DCE00F1EB16 /* dns */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + A0F7AE492FCD9C8B00C8CCFE /* Exceptions for "dns" folder in "dns" target */, + ); path = dns; sourceTree = ""; }; @@ -69,6 +94,19 @@ path = Stickers; sourceTree = ""; }; + A0F7AE4F2FCD9DF300C8CCFE /* Screenshots */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Screenshots; + sourceTree = ""; + }; + A0F7AE5B2FCD9E7000C8CCFE /* fastlane */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + A0F7AE5D2FCD9E7F00C8CCFE /* Exceptions for "fastlane" folder in "Screenshots" target */, + ); + path = fastlane; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -80,6 +118,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A0F7AE4B2FCD9DF300C8CCFE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -91,8 +136,10 @@ A06A74792F8E95410093A9E4 /* README.md */, A051B50B2FC9DA9100EACDC0 /* .bartycrouch.toml */, A051B5132FC9E7BC00EACDC0 /* Config.xcconfig */, + A0F7AE5B2FCD9E7000C8CCFE /* fastlane */, 069DCCFC2F8C0DCE00F1EB16 /* dns */, A082640D2FC718790077B227 /* Stickers */, + A0F7AE4F2FCD9DF300C8CCFE /* Screenshots */, 069DCCFB2F8C0DCE00F1EB16 /* Products */, ); sourceTree = ""; @@ -102,6 +149,7 @@ children = ( 069DCCFA2F8C0DCE00F1EB16 /* dns.app */, A082640C2FC718790077B227 /* Stickers.appex */, + A0F7AE4E2FCD9DF300C8CCFE /* Screenshots.xctest */, ); name = Products; sourceTree = ""; @@ -155,6 +203,29 @@ productReference = A082640C2FC718790077B227 /* Stickers.appex */; productType = "com.apple.product-type.app-extension.messages-sticker-pack"; }; + A0F7AE4D2FCD9DF300C8CCFE /* Screenshots */ = { + isa = PBXNativeTarget; + buildConfigurationList = A0F7AE562FCD9DF300C8CCFE /* Build configuration list for PBXNativeTarget "Screenshots" */; + buildPhases = ( + A0F7AE4A2FCD9DF300C8CCFE /* Sources */, + A0F7AE4B2FCD9DF300C8CCFE /* Frameworks */, + A0F7AE4C2FCD9DF300C8CCFE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + A0F7AE552FCD9DF300C8CCFE /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + A0F7AE4F2FCD9DF300C8CCFE /* Screenshots */, + ); + name = Screenshots; + packageProductDependencies = ( + ); + productName = Screenshots; + productReference = A0F7AE4E2FCD9DF300C8CCFE /* Screenshots.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -162,7 +233,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 2640; + LastSwiftUpdateCheck = 2650; LastUpgradeCheck = 2650; TargetAttributes = { 069DCCF92F8C0DCE00F1EB16 = { @@ -171,6 +242,10 @@ A082640B2FC718790077B227 = { CreatedOnToolsVersion = 26.5; }; + A0F7AE4D2FCD9DF300C8CCFE = { + CreatedOnToolsVersion = 26.5; + TestTargetID = 069DCCF92F8C0DCE00F1EB16; + }; }; }; buildConfigurationList = 069DCCF52F8C0DCD00F1EB16 /* Build configuration list for PBXProject "dns" */; @@ -193,6 +268,7 @@ targets = ( 069DCCF92F8C0DCE00F1EB16 /* dns */, A082640B2FC718790077B227 /* Stickers */, + A0F7AE4D2FCD9DF300C8CCFE /* Screenshots */, ); }; /* End PBXProject section */ @@ -212,6 +288,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A0F7AE4C2FCD9DF300C8CCFE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -244,6 +327,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A0F7AE4A2FCD9DF300C8CCFE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -252,6 +342,11 @@ target = A082640B2FC718790077B227 /* Stickers */; targetProxy = A08264112FC718790077B227 /* PBXContainerItemProxy */; }; + A0F7AE552FCD9DF300C8CCFE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 069DCCF92F8C0DCE00F1EB16 /* dns */; + targetProxy = A0F7AE542FCD9DF300C8CCFE /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -498,6 +593,44 @@ }; name = Release; }; + A0F7AE572FCD9DF300C8CCFE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = uk.sr2.dns.Screenshots; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = dns; + }; + name = Debug; + }; + A0F7AE582FCD9DF300C8CCFE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + PRODUCT_BUNDLE_IDENTIFIER = uk.sr2.dns.Screenshots; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = dns; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -528,6 +661,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + A0F7AE562FCD9DF300C8CCFE /* Build configuration list for PBXNativeTarget "Screenshots" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A0F7AE572FCD9DF300C8CCFE /* Debug */, + A0F7AE582FCD9DF300C8CCFE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/dns.xcodeproj/xcshareddata/xcschemes/Screenshots.xcscheme b/dns.xcodeproj/xcshareddata/xcschemes/Screenshots.xcscheme new file mode 100644 index 0000000..b0a5a4f --- /dev/null +++ b/dns.xcodeproj/xcshareddata/xcschemes/Screenshots.xcscheme @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/dns.xcodeproj/xcshareddata/xcschemes/Stickers.xcscheme b/dns.xcodeproj/xcshareddata/xcschemes/Stickers.xcscheme new file mode 100644 index 0000000..0d7e3ee --- /dev/null +++ b/dns.xcodeproj/xcshareddata/xcschemes/Stickers.xcscheme @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dns.xcodeproj/xcshareddata/xcschemes/dns.xcscheme b/dns.xcodeproj/xcshareddata/xcschemes/dns.xcscheme new file mode 100644 index 0000000..869915b --- /dev/null +++ b/dns.xcodeproj/xcshareddata/xcschemes/dns.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dns/HomeView.swift b/dns/HomeView.swift index f9295ea..eb95516 100644 --- a/dns/HomeView.swift +++ b/dns/HomeView.swift @@ -34,8 +34,13 @@ struct HomeView: View { Toggle("", isOn: $viewModel.isDnsEnabled) .labelsHidden() - .disabled(true) .tint(.green) +#if targetEnvironment(simulator) + .disabled(false) + .accessibilityIdentifier("dns-toggle") +#else + .disabled(true) +#endif } .padding(.vertical, 4) } diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..2f50720 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,8 @@ +app_identifier("uk.sr2.dns") # The bundle identifier of your app +# apple_id("[[APPLE_ID]]") # Your Apple Developer Portal username + + +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile + +itc_team_id "128809486" diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..7a0b34f --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,49 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:ios) + +platform :ios do + desc "Generate new localized screenshots" + lane :screenshots do + capture_screenshots(scheme: "Screenshots") + end + + desc "Upload App Store Metadata" + lane :upload_metadata do + deliver( + skip_binary_upload: true, + skip_app_version_update: true, + + skip_screenshots: true, + + force: true, + precheck_include_in_app_purchases: false, + ) + end + + desc "Upload App Store Screenshots" + lane :upload_screenshots do + deliver( + skip_binary_upload: true, + skip_app_version_update: true, + + skip_metadata: true, + overwrite_screenshots: true, + + force: true, + ) + end +end diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..83178c3 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,48 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios screenshots + +```sh +[bundle exec] fastlane ios screenshots +``` + +Generate new localized screenshots + +### ios upload_metadata + +```sh +[bundle exec] fastlane ios upload_metadata +``` + +Upload App Store Metadata + +### ios upload_screenshots + +```sh +[bundle exec] fastlane ios upload_screenshots +``` + +Upload App Store Screenshots + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/Snapfile b/fastlane/Snapfile new file mode 100644 index 0000000..9e29893 --- /dev/null +++ b/fastlane/Snapfile @@ -0,0 +1,29 @@ +# Uncomment the lines below you want to change by removing the # in the beginning + +# A list of devices you want to take the screenshots from +devices([ + "iPhone 17 Pro Max @ iOS 26.5", + "iPad Pro 13\"M5 @ iOS 26.5" +]) + +languages([ + "en-GB", +]) + +# The name of the scheme which contains the UI Tests +scheme("Screenshots") + +# Where should the resulting screenshots be stored? +output_directory("./fastlane/screenshots") + +# remove the '#' to clear all previously generated screenshots before creating new ones +# clear_previous_screenshots(true) + +# 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. +override_status_bar(true) + +# Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments +# launch_arguments(["-favColor red"]) + +# For more information about all available options run +# fastlane action snapshot diff --git a/fastlane/SnapshotHelper.swift b/fastlane/SnapshotHelper.swift new file mode 100644 index 0000000..6dec130 --- /dev/null +++ b/fastlane/SnapshotHelper.swift @@ -0,0 +1,313 @@ +// +// 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 + +@MainActor +func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { + Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations) +} + +@MainActor +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. +@MainActor +func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + Snapshot.snapshot(name, timeWaitingForIdle: timeout) +} + +enum SnapshotError: Error, CustomDebugStringConvertible { + case cannotFindSimulatorHomeDirectory + case cannotRunOnPhysicalDevice + + var debugDescription: String { + switch self { + case .cannotFindSimulatorHomeDirectory: + return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." + case .cannotRunOnPhysicalDevice: + return "Can't use Snapshot on a physical device." + } + } +} + +@objcMembers +@MainActor +open class Snapshot: NSObject { + static var app: XCUIApplication? + static var waitForAnimations = true + static var cacheDirectory: URL? + static var screenshotsDirectory: URL? { + return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) + } + static var deviceLanguage = "" + static var currentLocale = "" + + open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { + + Snapshot.app = app + Snapshot.waitForAnimations = waitForAnimations + + do { + let cacheDir = try getCacheDirectory() + Snapshot.cacheDirectory = cacheDir + setLanguage(app) + setLocale(app) + setLaunchArguments(app) + } catch let error { + NSLog(error.localizedDescription) + } + } + + class func setLanguage(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("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 { + NSLog("Couldn't detect/set language...") + } + } + + class func setLocale(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("locale.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + currentLocale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + } catch { + NSLog("Couldn't detect/set locale...") + } + + if currentLocale.isEmpty && !deviceLanguage.isEmpty { + currentLocale = Locale(identifier: deviceLanguage).identifier + } + + if !currentLocale.isEmpty { + app.launchArguments += ["-AppleLocale", "\"\(currentLocale)\""] + } + } + + class func setLaunchArguments(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("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 { + NSLog("Couldn't detect/set launch_arguments...") + } + } + + open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + if timeout > 0 { + waitForLoadingIndicatorToDisappear(within: timeout) + } + + NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work + + if Snapshot.waitForAnimations { + sleep(1) // Waiting for the animation to be finished (kind of) + } + + #if os(OSX) + guard let app = self.app else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) + #else + + guard self.app != nil else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let screenshot = XCUIScreen.main.screenshot() + #if os(iOS) && !targetEnvironment(macCatalyst) + let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image + #else + let image = screenshot.image + #endif + + guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } + + do { + // The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices + let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ") + let range = NSRange(location: 0, length: simulator.count) + simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "") + + let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") + #if swift(<5.0) + try UIImagePNGRepresentation(image)?.write(to: path, options: .atomic) + #else + try image.pngData()?.write(to: path, options: .atomic) + #endif + } catch let error { + NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png") + NSLog(error.localizedDescription) + } + #endif + } + + class func fixLandscapeOrientation(image: UIImage) -> UIImage { + #if os(watchOS) + return image + #else + if #available(iOS 10.0, *) { + let format = UIGraphicsImageRendererFormat() + format.scale = image.scale + let renderer = UIGraphicsImageRenderer(size: image.size, format: format) + return renderer.image { context in + image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + } + } else { + return image + } + #endif + } + + class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { + #if os(tvOS) + return + #endif + + guard let app = self.app else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element + let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) + _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) + } + + class func getCacheDirectory() throws -> URL { + let cachePath = "Library/Caches/tools.fastlane" + // on OSX config is stored in /Users//Library + // and on iOS/tvOS/WatchOS it's in simulator's home dir + #if os(OSX) + let homeDir = URL(fileURLWithPath: NSHomeDirectory()) + return homeDir.appendingPathComponent(cachePath) + #elseif arch(i386) || arch(x86_64) || arch(arm64) + guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { + throw SnapshotError.cannotFindSimulatorHomeDirectory + } + let homeDir = URL(fileURLWithPath: simulatorHostHome) + return homeDir.appendingPathComponent(cachePath) + #else + throw SnapshotError.cannotRunOnPhysicalDevice + #endif + } +} + +private extension XCUIElementAttributes { + var isNetworkLoadingIndicator: Bool { + if hasAllowListedIdentifier { 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 hasAllowListedIdentifier: Bool { + let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] + + return allowListedIdentifiers.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) + } + + @MainActor + var deviceStatusBars: XCUIElementQuery { + guard let app = Snapshot.app else { + fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + } + + let deviceWidth = app.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.30] diff --git a/fastlane/metadata/copyright.txt b/fastlane/metadata/copyright.txt new file mode 100644 index 0000000..c848182 --- /dev/null +++ b/fastlane/metadata/copyright.txt @@ -0,0 +1 @@ +2026 SR2 Communications Limited diff --git a/fastlane/metadata/en-GB/apple_tv_privacy_policy.txt b/fastlane/metadata/en-GB/apple_tv_privacy_policy.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/en-GB/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/en-GB/description.txt b/fastlane/metadata/en-GB/description.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/en-GB/description.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/en-GB/keywords.txt b/fastlane/metadata/en-GB/keywords.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/en-GB/keywords.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/en-GB/marketing_url.txt b/fastlane/metadata/en-GB/marketing_url.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/en-GB/marketing_url.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/en-GB/name.txt b/fastlane/metadata/en-GB/name.txt new file mode 100644 index 0000000..4e10c53 --- /dev/null +++ b/fastlane/metadata/en-GB/name.txt @@ -0,0 +1 @@ +SR2 Cloud DNS diff --git a/fastlane/metadata/en-GB/privacy_url.txt b/fastlane/metadata/en-GB/privacy_url.txt new file mode 100644 index 0000000..565aefd --- /dev/null +++ b/fastlane/metadata/en-GB/privacy_url.txt @@ -0,0 +1 @@ +https://www.sr2.uk/privacy diff --git a/fastlane/metadata/en-GB/promotional_text.txt b/fastlane/metadata/en-GB/promotional_text.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/en-GB/promotional_text.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/en-GB/release_notes.txt b/fastlane/metadata/en-GB/release_notes.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/en-GB/release_notes.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/en-GB/subtitle.txt b/fastlane/metadata/en-GB/subtitle.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/en-GB/subtitle.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/en-GB/support_url.txt b/fastlane/metadata/en-GB/support_url.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/en-GB/support_url.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/primary_category.txt b/fastlane/metadata/primary_category.txt new file mode 100644 index 0000000..41f44c0 --- /dev/null +++ b/fastlane/metadata/primary_category.txt @@ -0,0 +1 @@ +UTILITIES diff --git a/fastlane/metadata/primary_first_sub_category.txt b/fastlane/metadata/primary_first_sub_category.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/primary_first_sub_category.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/primary_second_sub_category.txt b/fastlane/metadata/primary_second_sub_category.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/primary_second_sub_category.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/secondary_category.txt b/fastlane/metadata/secondary_category.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/secondary_category.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/secondary_first_sub_category.txt b/fastlane/metadata/secondary_first_sub_category.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/secondary_first_sub_category.txt @@ -0,0 +1 @@ + diff --git a/fastlane/metadata/secondary_second_sub_category.txt b/fastlane/metadata/secondary_second_sub_category.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fastlane/metadata/secondary_second_sub_category.txt @@ -0,0 +1 @@ +