-
Notifications
You must be signed in to change notification settings - Fork 629
Description
Describe the bug
We observed two related issues with filename generation when using verifySnapshot (via a helper function) within @mainactor async test functions:
Unexpected .1 Suffix: When not providing an explicit named parameter to verifySnapshot, snapshot filenames were sometimes generated with a .1 suffix (e.g., testFunctionName.1.png) even though only a single snapshot assertion was made within the test function. This occurred in @mainactor async tests.
Duplicated Filename with named Parameter: After adding an explicit named parameter to work around the first issue (e.g., named: "testFunctionName" where the name matched the function name), Xcode Cloud logs reported failures looking for a snapshot file where the name appeared duplicated (e.g., testFunctionName.testFunctionName.png). Locally, this duplication was not observed, but the initial .1 issue was resolved by adding named.
To Reproduce
Setup:
Create an @mainactor async test function.
Use a helper function like assertSnapshotUIView below, which calculates the snapshot directory based on the test class and calls verifySnapshot.
Code Structure:
Swift
import Foundation
import SnapshotTesting
import UIKit
import XCTest
// MARK: - Test Class Structure
class MySnapshotTests: XCTestCase {
@MainActor
func testMyFeatureSnapshot() async {
let myView = UIView() // Replace with actual view creation
// ... setup view ...
r
assertSnapshotUIView(
matching: myView,
named: "testMyFeatureSnapshot", // Explicit name matching function
record: false
)
}
}
// MARK: - Helper Functions
public func assertSnapshotUIView(
matching view: UIView,
named name: String? = nil,
width: CGFloat = 375,
height: CGFloat? = nil,
timeout: TimeInterval = 0.5,
file: StaticString = #file,
testName: String = #function, // #function passed here
line: UInt = #line,
record recording: Bool = false
) {
precondition(UIScreen.main.scale == 3, "Snapshot tests require 3x screen scale")
let snapshotDirectory = snapshotDirectory(for: file)
let viewHeight = view
.systemLayoutSizeFitting(
CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
).height
// Call the library's core function
if let error = verifySnapshot(
of: { view }(),
as: .image(
precision: 0.98,
perceptualPrecision: 0.98,
size: .init(width: width, height: height ?? viewHeight)
),
named: name,
record: recording,
snapshotDirectory: snapshotDirectory,
timeout: timeout,
file: file,
testName: testName,
line: line
) {
XCTFail(error, file: file, line: line)
}
}
// Custom function to determine snapshot directory based on test class file
func snapshotDirectory(
for file: StaticString,
relativePathComponent: String = "TESTTARGET"
) -> String {
var sourcePathComponents = URL(fileURLWithPath: "\(file)").pathComponents
if let indexFolder = sourcePathComponents.firstIndex(of: relativePathComponent) {
sourcePathComponents = Array(sourcePathComponents.prefix(upTo: indexFolder.advanced(by: 1)))
} else {
print("Warning: Could not find relative path component '\(relativePathComponent)' in \(file)")
sourcePathComponents = URL(fileURLWithPath: "\(file)").deletingLastPathComponent().pathComponents
}
let fileUrl = URL(fileURLWithPath: "\(file)", isDirectory: false)
let folderName = fileUrl.deletingPathExtension().lastPathComponent // e.g., MySnapshotTests
sourcePathComponents.append("__Snapshots__")
sourcePathComponents.append(folderName) // Subdirectory per test class
return sourcePathComponents.joined(separator: "/")
}
Steps:
Run the test without the named parameter in assertSnapshotUIView. Observe if a .1 suffix appears on the generated filename, especially if the test function is async.
Add the named parameter with a value matching the test function name (e.g., named: "testMyFeatureSnapshot" for func testMyFeatureSnapshot()).
Record the snapshot locally (record: true). Commit the snapshot (e.g., testMyFeatureSnapshot.png).
Run the test on a CI environment like Xcode Cloud (record: false). Check the failure logs. Observe if the error message indicates it was looking for a duplicated filename like testMyFeatureSnapshot.testMyFeatureSnapshot.png
.
Expected behavior
When only one verifySnapshot call occurs within a test function (even async), no .1 suffix should be appended to the filename when named is nil. The name should be derived cleanly from #function.
When named: "some_name" is provided, the resulting snapshot filename should be some_name.png (plus any expected, non-duplicated environment identifiers), located within the specified snapshotDirectory. It should not be some_name.some_name.png.
Screenshots
Example Log Message Snippet from Xcode Cloud:
failed - No reference was found on disk. Automatically recorded snapshot: … open "file:///.../Snapshots/MySnapshotTests/testMyFeatureSnapshot.testMyFeatureSnapshot.png"
Environment
swift-snapshot-testing version: [1.18.3]
Xcode version: [16.3]
Swift version: [5.0]
OS: [Xcode Cloud Environment (CI)]
Test Execution Context: Tests are @mainactor async func