-
-
Save jaanus/7e14b31f7f445435aadac09d24397da8 to your computer and use it in GitHub Desktop.
#if os(iOS) | |
import SnapshotTesting | |
import SwiftUI | |
import XCTest | |
public extension XCTestCase { | |
/// Test the layout of a full-screen SwiftUI view. | |
/// | |
/// Currently, this is hardcoded to logical width and height of iPhone 14 Pro screen. It assumes that tests are ran only on iPhone 14 Pro simulator, | |
/// so you’ll need to change this also in the Xcode Cloud workflow. Change the device from “recommended iPhones” to “iPhone 14 Pro”. | |
/// | |
/// This currently tests the EN and ET languages, to visually spot any problems in language localization. | |
/// | |
/// - Parameters: | |
/// - view: The view to test. | |
/// - testBundleResourceURL: Resource URL that contains a folder with the reference screenshots. | |
/// For SPM module tests, the folder will be named `__Snapshots__/TestClassName`. | |
/// For top-level app target tests, the folder will be named simply `TestClassName`. | |
/// - file: The test file calling this function. No need to pass it, this is determined automatically. | |
/// - testName: Test function that is calling this function. No need to pass it, this is determined automatically. | |
/// - line: Line in the test file calling this function. No need to pass it, this is determined automatically. | |
func assertSnapshot( | |
view: some View, | |
testBundleResourceURL: URL, | |
file: StaticString = #file, | |
testName: String = #function, | |
line: UInt = #line | |
) { | |
let locales = [Locale(identifier: "en_US"), Locale(identifier: "et_EE")] | |
let testClassFileURL = URL(fileURLWithPath: "\(file)", isDirectory: false) | |
let testClassName = testClassFileURL.deletingPathExtension().lastPathComponent | |
let folderCandidates = [ | |
// For SPM modules. | |
testBundleResourceURL.appending(path: "__Snapshots__").appending(path: testClassName), | |
// For top-level xcodeproj app target. | |
testBundleResourceURL.appending(path: testClassName) | |
] | |
for locale in locales { | |
// Default case: snapshots are not present in test bundle. This will fall back to standard SnapshotTesting behavior, | |
// where the snapshots live in `__Snapshots__` folder that is adjacent to the test class. | |
var snapshotDirectory: String? = nil | |
for folder in folderCandidates { | |
let referenceSnapshotURLInTestBundle = folder.appending(path: "\(sanitizePathComponent(testName)).\(locale.identifier).png") | |
if FileManager.default.fileExists(atPath: referenceSnapshotURLInTestBundle.path(percentEncoded: false)) { | |
// The snapshot file is present in the test bundle, so we will instruct snapshot-testing to use the folder | |
// pointing to the snapshots in the test bundle, instead of the default. | |
// This is the code path that Xcode Cloud will follow, if everything is set up correctly. | |
snapshotDirectory = folder.path(percentEncoded: false) | |
} | |
} | |
let failure = SnapshotTesting.verifySnapshot( | |
matching: view | |
.frame(width: 393, height: 852) | |
.environment(\.locale, locale), | |
// When precision is the default 100%, some snapshot tests on Xcode Cloud fail, | |
// even though there is no visible difference between reference and test result images, | |
// and the difference image is completely black (does not indicate any different pixels). | |
// 🤷 Just lowering the tolerance a bit seems to make it more resilient. | |
as: .image(precision: 0.98), | |
named: locale.identifier, | |
record: false, | |
snapshotDirectory: snapshotDirectory, | |
file: file, | |
testName: testName, | |
line: line | |
) | |
if let message = failure { | |
XCTFail(message, file: file, line: line) | |
} | |
} | |
} | |
// Copied from swift-snapshot-testing | |
func sanitizePathComponent(_ string: String) -> String { | |
return string | |
.replacingOccurrences(of: "\\W+", with: "-", options: .regularExpression) | |
.replacingOccurrences(of: "^-|-$", with: "", options: .regularExpression) | |
} | |
} | |
#endif |
First of all: Great approach and great that you confronted Apple about this.
Second:
It seems like the match-function you are using in your code example here is deprecated. There is a concrete, but bit different looking example, in the swift-snapshot-testing assertSnapshot() sourcecode.
Still, I can't for the love of me get Xcode Cloud to find my snapshots attached to my testing target.
Lastly:
This works when you create new tests. If the snapshot doesn't exist, it's recorded locally.
After this you have to add the file to the test target manually - you won't know if you forgot this until your tests fail in the cloud. It could be nice with automatic adding the file to the test target or a compiler warning if you forgot it.
The problem arises if you want to re-record the snapshot for an existing test. Let's say that you changed your app and want to update the snapshot. If you delete the local snapshot int the Snapshots folder, the test target will not compile anymore because of a dead reference in the project file. You will have to manually un-tick it in the thest target, to get the target to compile and run tests again and create the new snapshot.
The manual quirks with this makes it a work-around more than a streamlined solution with a good developer expereience.
Have you had time to look into how these issues could be addressed?
Just as a note, in my case, I had to add the counter on the test name: testView.1.png
, testView.2.png
, etc.
To achieve this, I've modified the definition of the referencSnapshotURLInTestBundle
variable as follows:
let counter = counterQueue.sync { () -> Int in
let key = folder.appendingPathComponent(sanitizePathComponent(testName))
counterMap[key, default: 0] += 1
return counterMap[key]!
}
// Note I'm not including the locale just because I did not need it
let referencSnapshotURLInTestBundle = folder.appending(path: "\(sanitizePathComponent(testName)).\(counter).png")
I'm also declaring these two variables globally:
private let counterQueue = DispatchQueue(label: "com.bundle.identifier.counter")
private var counterMap: [URL: Int] = [:]
Blog post with longer discussion: https://jaanus.com/snapshot-testing-xcode-cloud/