diff --git a/Examples/MultiModuleDemo/MultiModuleDemoTests/MultiModuleDemoSnapshotTests.swift b/Examples/MultiModuleDemo/MultiModuleDemoTests/MultiModuleDemoSnapshotTests.swift index e8b94b0..e161e6a 100644 --- a/Examples/MultiModuleDemo/MultiModuleDemoTests/MultiModuleDemoSnapshotTests.swift +++ b/Examples/MultiModuleDemo/MultiModuleDemoTests/MultiModuleDemoSnapshotTests.swift @@ -7,6 +7,10 @@ final class MultiModuleDemoSnapshotSingleModuleAllowTests: SnapshotTest { override class func snapshotPreviewModules() -> [String]? { return ["ModuleA"] } + + override class func excludedSnapshotPreviews() -> [String]? { + return ["ModuleA/ModuleAViews.swift:ModuleA Button"] + } } final class MultiModuleDemoSnapshotMultipleModuleAllowTests: SnapshotTest { diff --git a/README.md b/README.md index f3bfed5..9cfbc0c 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,19 @@ xcodebuild test \ -destination 'platform=iOS Simulator,name=iPhone 15 Pro' ``` +### Environment variables + +SnapshotPreviews supports these test-runner environment variables: + +| Variable | Description | +| --- | --- | +| `TEST_RUNNER_SNAPSHOTS_EXPORT_DIR` | Writes rendered snapshot PNGs and JSON sidecars to the given directory instead of attaching PNGs to the `.xcresult` bundle. | +| `TEST_RUNNER_SNAPSHOTS_ALL_IMAGE_NAMES_FILE` | Writes all discovered logical `.png` image names to the given file, then returns without rendering previews. Used to support selective testing workflows. | + +These modes are mutually exclusive. If `TEST_RUNNER_SNAPSHOTS_ALL_IMAGE_NAMES_FILE` is set, SnapshotPreviews writes image names only and does not render or export snapshot images. + > [!NOTE] -> The `TEST_RUNNER_` prefix is how Xcode forwards an environment variable from `xcodebuild` into the test runner process. Inside the runner the variable is read as `SNAPSHOTS_EXPORT_DIR`. +> The `TEST_RUNNER_` prefix is how Xcode forwards an environment variable from `xcodebuild` into the test runner process. Inside the runner, SnapshotPreviews reads the variable without that prefix. For every rendered preview, two files are written: diff --git a/Sources/SnapshottingTests/AllSnapshotImageNamesWriter.swift b/Sources/SnapshottingTests/AllSnapshotImageNamesWriter.swift new file mode 100644 index 0000000..e05bac8 --- /dev/null +++ b/Sources/SnapshottingTests/AllSnapshotImageNamesWriter.swift @@ -0,0 +1,59 @@ +import Foundation + +final class AllSnapshotImageNamesWriter { + static let envKey = "SNAPSHOTS_ALL_IMAGE_NAMES_FILE" + + private let outputURL: URL + + static func createFromEnvironment( + environment: [String: String] = ProcessInfo.processInfo.environment, + fileManager: FileManager = .default + ) -> AllSnapshotImageNamesWriter? { + guard let outputPath = environment[envKey] else { + return nil + } + + let trimmed = outputPath.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { + preconditionFailure("\(envKey) is set but empty. Provide a valid file path.") + } + + let outputURL: URL + if trimmed.hasPrefix("/") { + outputURL = URL(fileURLWithPath: trimmed).standardizedFileURL + } else { + outputURL = URL(fileURLWithPath: fileManager.currentDirectoryPath) + .appendingPathComponent(trimmed) + .standardizedFileURL + } + + return Self(outputURL: outputURL, fileManager: fileManager) + } + + init(outputURL: URL, fileManager: FileManager = .default) { + self.outputURL = outputURL + + do { + try fileManager.createDirectory( + at: outputURL.deletingLastPathComponent(), + withIntermediateDirectories: true + ) + } catch { + preconditionFailure("Failed to create all snapshot image names directory at \(outputURL.deletingLastPathComponent().path): \(error)") + } + } + + func write(imageNames: [String]) { + let sortedImageNames = Set(imageNames).sorted() + let contents = sortedImageNames.isEmpty ? "" : "\(sortedImageNames.joined(separator: "\n"))\n" + guard let data = contents.data(using: .utf8) else { + preconditionFailure("Failed to encode all snapshot image names file at \(outputURL.path)") + } + + do { + try data.write(to: outputURL, options: .atomic) + } catch { + preconditionFailure("Failed to write all snapshot image names file at \(outputURL.path): \(error)") + } + } +} diff --git a/Sources/SnapshottingTests/FileNameUtils.swift b/Sources/SnapshottingTests/FileNameUtils.swift new file mode 100644 index 0000000..5c582ff --- /dev/null +++ b/Sources/SnapshottingTests/FileNameUtils.swift @@ -0,0 +1,35 @@ +// +// FileNameUtils.swift +// SnapshotPreviews +// +// Created by Cameron Cooke on 08/06/2026. +// + +import Foundation + +enum FileNameUtils { + + /// Accepts a pre-sanitized value, then converts it into a safe image filename with a .png extension. + static func imageFileName(from value: String) -> String { + "\(sanitize(value)).png" + } + + private static func sanitize(_ value: String) -> String { + var result = "" + var lastWasUnderscore = false + + for c in value { + if c.isLetter || c.isNumber || c == "." || c == "-" || c == "_" { + result.append(c) + lastWasUnderscore = false + } else if !lastWasUnderscore { + result.append("_") + lastWasUnderscore = true + } + } + + result = result.trimmingCharacters(in: CharacterSet(charactersIn: "_.-")) + + return result.isEmpty ? "snapshot" : result + } +} diff --git a/Sources/SnapshottingTests/PreviewBaseTest.swift b/Sources/SnapshottingTests/PreviewBaseTest.swift index fafe941..da207f3 100644 --- a/Sources/SnapshottingTests/PreviewBaseTest.swift +++ b/Sources/SnapshottingTests/PreviewBaseTest.swift @@ -72,7 +72,7 @@ open class PreviewBaseTest: XCTestCase { previews = [] var i = 0 - let currentDeviceName = ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] ?? ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] + let currentDeviceName = SnapshotPreviewDestination.currentDeviceName() for discoveredPreview in discoveredPreviews { let typeName = discoveredPreview.typeName @@ -80,12 +80,12 @@ open class PreviewBaseTest: XCTestCase { let count = discoveredPreview.numberOfPreviews for j in 0.. String { - var result = "" - var lastWasUnderscore = false - - for c in raw { - if c.isLetter || c.isNumber || c == "." || c == "-" || c == "_" { - result.append(c) - lastWasUnderscore = false - } else if !lastWasUnderscore { - result.append("_") - lastWasUnderscore = true - } - } - - result = result.trimmingCharacters(in: CharacterSet(charactersIn: "_.-")) - - return result.isEmpty ? "snapshot" : result - } - // MARK: - Export static func canonicalGroup( @@ -286,8 +270,8 @@ final class SnapshotCIExportCoordinator: NSObject, XCTestObservation { result: SnapshotResult, context: SnapshotContext ) { - let pngFileName = "\(context.baseFileName).png" - let jsonFileName = "\(context.baseFileName).json" + let pngFileName = context.imageFileName + let jsonFileName = context.sidecarFileName let displayName = Self.canonicalDisplayName(for: context) let group = Self.canonicalGroup( diff --git a/Sources/SnapshottingTests/SnapshotPreviewDestination.swift b/Sources/SnapshottingTests/SnapshotPreviewDestination.swift new file mode 100644 index 0000000..9b6ee71 --- /dev/null +++ b/Sources/SnapshottingTests/SnapshotPreviewDestination.swift @@ -0,0 +1,9 @@ +import Foundation + +struct SnapshotPreviewDestination { + static func currentDeviceName( + environment: [String: String] = ProcessInfo.processInfo.environment + ) -> String? { + environment["SIMULATOR_DEVICE_NAME"] ?? environment["SIMULATOR_MODEL_IDENTIFIER"] + } +} diff --git a/Sources/SnapshottingTests/SnapshotPreviewDeviceFilter.swift b/Sources/SnapshottingTests/SnapshotPreviewDeviceFilter.swift new file mode 100644 index 0000000..aa86356 --- /dev/null +++ b/Sources/SnapshottingTests/SnapshotPreviewDeviceFilter.swift @@ -0,0 +1,25 @@ +struct SnapshotPreviewDeviceFilter { + static func shouldInclude( + discoveredPreview: DiscoveredPreview, + index: Int, + currentDestinationDeviceName: String? + ) -> Bool { + let requestedDevice = discoveredPreview.devices.indices.contains(index) ? discoveredPreview.devices[index] : nil + return shouldInclude(requestedDeviceName: requestedDevice, currentDestinationDeviceName: currentDestinationDeviceName) + } + + static func shouldInclude( + requestedDeviceName: String?, + currentDestinationDeviceName: String? + ) -> Bool { + guard let currentDestinationDeviceName else { + return true + } + + guard let requestedDeviceName, !requestedDeviceName.isEmpty else { + return true + } + + return requestedDeviceName == currentDestinationDeviceName + } +} diff --git a/Sources/SnapshottingTests/SnapshotTest.swift b/Sources/SnapshottingTests/SnapshotTest.swift index 2c060b2..1d8c165 100644 --- a/Sources/SnapshottingTests/SnapshotTest.swift +++ b/Sources/SnapshottingTests/SnapshotTest.swift @@ -222,13 +222,19 @@ open class SnapshotTest: PreviewBaseTest, PreviewFilters { #endif private static var renderingStrategy: RenderingStrategy? = nil @MainActor private static var ciExportCoordinator: SnapshotCIExportCoordinator? + @MainActor private static var allSnapshotImageNamesWriter: AllSnapshotImageNamesWriter? static private var previews: [SnapshotPreviewsCore.PreviewType] = [] static private var fileNameResolver = FileNameResolver(previews: []) @MainActor override class func discoverPreviews() -> [DiscoveredPreview] { - ciExportCoordinator = SnapshotCIExportCoordinator.createFromEnvironment() + allSnapshotImageNamesWriter = AllSnapshotImageNamesWriter.createFromEnvironment() + if allSnapshotImageNamesWriter == nil { + ciExportCoordinator = SnapshotCIExportCoordinator.createFromEnvironment() + } else { + ciExportCoordinator = nil + } previews = FindPreviews.findPreviews( included: Self.snapshotPreviews(), @@ -237,9 +243,49 @@ open class SnapshotTest: PreviewBaseTest, PreviewFilters { excludedModules: Self.excludedSnapshotPreviewModules() ) fileNameResolver = FileNameResolver(previews: previews) + + if let allSnapshotImageNamesWriter { + allSnapshotImageNamesWriter.write( + imageNames: logicalImageNames(previews: previews, fileNameResolver: fileNameResolver) + ) + return [] + } + return previews.map { DiscoveredPreview.from(previewType: $0) } } + static func logicalImageNames( + previews: [SnapshotPreviewsCore.PreviewType], + fileNameResolver: FileNameResolver, + environment: [String: String] = ProcessInfo.processInfo.environment + ) -> [String] { + let currentDeviceName = SnapshotPreviewDestination.currentDeviceName(environment: environment) + var imageNames: [String] = [] + + for previewType in previews { + for previewIndex in previewType.previews.indices { + let requestedDeviceName = previewType.previews[previewIndex].device?.rawValue + guard SnapshotPreviewDeviceFilter.shouldInclude( + requestedDeviceName: requestedDeviceName, + currentDestinationDeviceName: currentDeviceName + ) else { + continue + } + + guard let rawBaseFileName = fileNameResolver.rawBaseFileName( + typeName: previewType.typeName, + previewIndex: previewIndex + ) else { + continue + } + + imageNames.append(FileNameUtils.imageFileName(from: rawBaseFileName)) + } + } + + return imageNames + } + /// Tests a specific preview by rendering it and generating a snapshot. Subclasses should NOT override this method. /// /// This method renders the specified preview using the appropriate rendering strategy, @@ -288,11 +334,11 @@ open class SnapshotTest: PreviewBaseTest, PreviewFilters { return } - let baseFileName = SnapshotCIExportCoordinator.sanitize(rawBaseFileName) + let imageFileName = FileNameUtils.imageFileName(from: rawBaseFileName) if let coordinator = Self.ciExportCoordinator { let colorSchemeValue = result.colorScheme.flatMap { $0.stringValue } let context = SnapshotContext( - baseFileName: baseFileName, + imageFileName: imageFileName, testName: name, typeName: previewType.typeName, typeDisplayName: previewType.displayName, @@ -312,7 +358,7 @@ open class SnapshotTest: PreviewBaseTest, PreviewFilters { } else { do { let attachment = try XCTAttachment(image: result.image.get()) - attachment.name = baseFileName + attachment.name = String(imageFileName.dropLast(".png".count)) attachment.lifetime = .keepAlways add(attachment) } catch { diff --git a/Tests/SnapshottingTestsTests/AllSnapshotImageNamesTests.swift b/Tests/SnapshottingTestsTests/AllSnapshotImageNamesTests.swift new file mode 100644 index 0000000..a0e65a7 --- /dev/null +++ b/Tests/SnapshottingTestsTests/AllSnapshotImageNamesTests.swift @@ -0,0 +1,160 @@ +import Foundation +import SwiftUI +@testable import SnapshotPreviewsCore +@testable import SnapshottingTests +import XCTest + +@MainActor +final class AllSnapshotImageNamesTests: XCTestCase { + private var tempDir: URL! + + override func setUp() { + super.setUp() + tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("AllSnapshotImageNamesTests-\(UUID().uuidString)") + } + + override func tearDown() { + try? FileManager.default.removeItem(at: tempDir) + super.tearDown() + } + + func testCurrentDestinationDeviceNamePrefersSimulatorDeviceName() { + let deviceName = SnapshotPreviewDestination.currentDeviceName( + environment: [ + "SIMULATOR_DEVICE_NAME": "iPhone 15", + "SIMULATOR_MODEL_IDENTIFIER": "iPhone16,1", + ] + ) + + XCTAssertEqual(deviceName, "iPhone 15") + } + + func testCurrentDestinationDeviceNameFallsBackToSimulatorModelIdentifier() { + let deviceName = SnapshotPreviewDestination.currentDeviceName( + environment: ["SIMULATOR_MODEL_IDENTIFIER": "iPhone16,1"] + ) + + XCTAssertEqual(deviceName, "iPhone16,1") + } + + func testDiscoveredPreviewDeviceFilterIncludesUndeclaredAndMatchingDevices() { + let preview = DiscoveredPreview( + typeName: "Module.TestView_Previews", + displayName: "Test View", + devices: ["", "iPhone 15", "iPhone 14"], + orientations: ["portrait", "portrait", "portrait"], + numberOfPreviews: 3 + ) + + XCTAssertTrue( + SnapshotPreviewDeviceFilter.shouldInclude( + discoveredPreview: preview, + index: 0, + currentDestinationDeviceName: "iPhone 15" + ) + ) + XCTAssertTrue( + SnapshotPreviewDeviceFilter.shouldInclude( + discoveredPreview: preview, + index: 1, + currentDestinationDeviceName: "iPhone 15" + ) + ) + XCTAssertFalse( + SnapshotPreviewDeviceFilter.shouldInclude( + discoveredPreview: preview, + index: 2, + currentDestinationDeviceName: "iPhone 15" + ) + ) + } + + func testWriterReplacesStaleOutputWithSortedDeduplicatedNames() throws { + let outputURL = tempDir.appendingPathComponent("all-image-names.txt") + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + try "stale.png\n".write(to: outputURL, atomically: true, encoding: .utf8) + + let writer = try XCTUnwrap( + AllSnapshotImageNamesWriter.createFromEnvironment( + environment: [AllSnapshotImageNamesWriter.envKey: outputURL.path] + ) + ) + + writer.write(imageNames: ["Beta.png", "Alpha.png", "Beta.png"]) + + XCTAssertEqual(try readAllSnapshotImageNamesFile(at: outputURL), "Alpha.png\nBeta.png\n") + } + + func testWriterWritesEmptyFileWhenThereAreNoImageNames() throws { + let outputURL = tempDir.appendingPathComponent("all-image-names.txt") + let writer = AllSnapshotImageNamesWriter(outputURL: outputURL) + + writer.write(imageNames: []) + + XCTAssertEqual(try readAllSnapshotImageNamesFile(at: outputURL), "") + } + + func testLogicalImageNamesAreSanitizedPngNamesAndPreserveDuplicateOrdinalsAfterDeviceFiltering() { + let previewType = PreviewType( + typeName: "TestModule.AllSnapshotImageNamesTestProvider", + previewProvider: AllSnapshotImageNamesTestProvider.self + ) + let resolver = SnapshotTest.FileNameResolver(previews: [previewType]) + + let imageNames = SnapshotTest.logicalImageNames( + previews: [previewType], + fileNameResolver: resolver, + environment: ["SIMULATOR_DEVICE_NAME": "iPhone 14"] + ) + + XCTAssertEqual( + imageNames, + [ + "All_Snapshot_Image_Names_Test_Provider_Duplicate_1.png", + "All_Snapshot_Image_Names_Test_Provider_Other.png", + ] + ) + } + + func testLogicalImageNamesUsePreviewTypeDeviceWhenDestinationMatches() { + let previewType = PreviewType( + typeName: "TestModule.AllSnapshotImageNamesTestProvider", + previewProvider: AllSnapshotImageNamesTestProvider.self + ) + let resolver = SnapshotTest.FileNameResolver(previews: [previewType]) + + let imageNames = SnapshotTest.logicalImageNames( + previews: [previewType], + fileNameResolver: resolver, + environment: ["SIMULATOR_DEVICE_NAME": "iPhone 15"] + ) + + XCTAssertEqual( + imageNames, + [ + "All_Snapshot_Image_Names_Test_Provider_Duplicate_1.png", + "All_Snapshot_Image_Names_Test_Provider_Duplicate_2.png", + "All_Snapshot_Image_Names_Test_Provider_Other.png", + ] + ) + } + + private func readAllSnapshotImageNamesFile(at url: URL) throws -> String { + try String(contentsOf: url, encoding: .utf8) + } +} + +private struct AllSnapshotImageNamesTestProvider: PreviewProvider { + static var previews: some View { + Group { + Text("One") + .previewDisplayName("Duplicate") + Text("Two") + .previewDisplayName("Duplicate") + .previewDevice("iPhone 15") + Text("Three") + .previewDisplayName("Other") + } + } +} diff --git a/Tests/SnapshottingTestsTests/FileNameUtilsTests.swift b/Tests/SnapshottingTestsTests/FileNameUtilsTests.swift new file mode 100644 index 0000000..4fe1c70 --- /dev/null +++ b/Tests/SnapshottingTestsTests/FileNameUtilsTests.swift @@ -0,0 +1,41 @@ +import XCTest +@testable import SnapshottingTests + +final class FileNameUtilsTests: XCTestCase { + func testImageFileNameReplacesUnsafeCharacters() { + let result = FileNameUtils.imageFileName(from: "My/View:Preview 1") + + XCTAssertEqual(result, "My_View_Preview_1.png") + } + + func testImageFileNameIsDeterministic() { + let a = FileNameUtils.imageFileName(from: "Some/View:Name") + let b = FileNameUtils.imageFileName(from: "Some/View:Name") + + XCTAssertEqual(a, b) + } + + func testImageFileNameCollapsesRepeatedUnsafeCharacters() { + let result = FileNameUtils.imageFileName(from: "A///B C") + + XCTAssertEqual(result, "A_B_C.png") + } + + func testImageFileNamePreservesExistingUnderscores() { + let result = FileNameUtils.imageFileName(from: "A___B") + + XCTAssertEqual(result, "A___B.png") + } + + func testImageFileNameFallsBackForEmptyResult() { + let result = FileNameUtils.imageFileName(from: "///") + + XCTAssertEqual(result, "snapshot.png") + } + + func testImageFileNamePreservesAlphanumericAndSafeCharacters() { + let result = FileNameUtils.imageFileName(from: "Hello_World-2.0") + + XCTAssertEqual(result, "Hello_World-2.0.png") + } +} diff --git a/Tests/SnapshottingTestsTests/SnapshotCIExportCoordinatorTests.swift b/Tests/SnapshottingTestsTests/SnapshotCIExportCoordinatorTests.swift index 05b052a..98225dc 100644 --- a/Tests/SnapshottingTestsTests/SnapshotCIExportCoordinatorTests.swift +++ b/Tests/SnapshottingTestsTests/SnapshotCIExportCoordinatorTests.swift @@ -46,39 +46,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.path)) } - // MARK: - Filename Sanitization - - func testSanitizeReplacesUnsafeCharacters() { - let result = SnapshotCIExportCoordinator.sanitize("My/View:Preview 1") - let unsafeChars = CharacterSet(charactersIn: "/\\: \"'<>|?*") - XCTAssertNil(result.rangeOfCharacter(from: unsafeChars)) - } - - func testSanitizeIsDeterministic() { - let a = SnapshotCIExportCoordinator.sanitize("Some/View:Name") - let b = SnapshotCIExportCoordinator.sanitize("Some/View:Name") - XCTAssertEqual(a, b) - } - - func testSanitizeCollapsesRepeatedUnsafeCharacters() { - let result = SnapshotCIExportCoordinator.sanitize("A///B C") - XCTAssertFalse(result.contains("__"), "Consecutive unsafe chars should collapse to a single underscore") - } - - func testSanitizePreservesExistingUnderscores() { - let result = SnapshotCIExportCoordinator.sanitize("A___B") - XCTAssertEqual(result, "A___B", "Underscores in the input should be preserved as-is") - } - - func testSanitizeFallsBackForEmptyResult() { - let result = SnapshotCIExportCoordinator.sanitize("///") - XCTAssertEqual(result, "snapshot") - } - - func testSanitizePreservesAlphanumericAndSafeChars() { - let result = SnapshotCIExportCoordinator.sanitize("Hello_World-2.0") - XCTAssertEqual(result, "Hello_World-2.0") - } + // MARK: - Raw Base File Names func testRawBaseFileNameUsesDisplayNameWhenUniqueForPreviewProvider() { let fileName = makeRawBaseFileName( @@ -184,8 +152,8 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let jsonURL = tempDir.appendingPathComponent("\(context.baseFileName).json") - let pngURL = tempDir.appendingPathComponent("\(context.baseFileName).png") + let jsonURL = tempDir.appendingPathComponent(context.sidecarFileName) + let pngURL = tempDir.appendingPathComponent(context.imageFileName) XCTAssertTrue(FileManager.default.fileExists(atPath: jsonURL.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: pngURL.path)) @@ -205,7 +173,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) XCTAssertEqual(json["display_name"] as? String, "Dark Mode") XCTAssertEqual(json["group"] as? String, "Login Screen") @@ -225,7 +193,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) XCTAssertEqual(json["group"] as? String, "Feature/LoginView.swift") } @@ -242,7 +210,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) XCTAssertEqual(json["display_name"] as? String, "At line #42") } @@ -260,7 +228,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) XCTAssertEqual(json["display_name"] as? String, "0") } @@ -279,7 +247,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) XCTAssertEqual(json["group"] as? String, "MyModule.TestView_Previews") } @@ -295,7 +263,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) let nestedContext = try XCTUnwrap(json["context"] as? [String: Any]) let preview = try XCTUnwrap(nestedContext["preview"] as? [String: Any]) @@ -327,7 +295,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) let tags = try XCTUnwrap(json["tags"] as? [String: String]) XCTAssertEqual(tags["component"], "button") @@ -352,7 +320,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) let nestedContext = try XCTUnwrap(json["context"] as? [String: Any]) let fixture = try XCTUnwrap(nestedContext["fixture"] as? [String: Any]) @@ -375,7 +343,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) let nestedContext = try XCTUnwrap(json["context"] as? [String: Any]) XCTAssertEqual(nestedContext["test_name"] as? String, "custom-test-name") @@ -397,7 +365,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeSuccessResult(), context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) let diffThreshold = try XCTUnwrap(json["diff_threshold"] as? Double) XCTAssertEqual(diffThreshold, 0.2, accuracy: 0.000_1) @@ -415,7 +383,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { context: context) coordinator.drain() - let json = try readJSON(forBaseFileName: context.baseFileName) + let json = try readJSON(forSidecarFileName: context.sidecarFileName) let diffThreshold = try XCTUnwrap(json["diff_threshold"] as? Double) XCTAssertEqual(diffThreshold, 0.2, accuracy: 0.000_1) @@ -430,8 +398,8 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.enqueueExport(result: makeFailureResult(), context: context) coordinator.drain() - XCTAssertFalse(FileManager.default.fileExists(atPath: tempDir.appendingPathComponent("\(context.baseFileName).png").path)) - XCTAssertFalse(FileManager.default.fileExists(atPath: tempDir.appendingPathComponent("\(context.baseFileName).json").path)) + XCTAssertFalse(FileManager.default.fileExists(atPath: tempDir.appendingPathComponent(context.imageFileName).path)) + XCTAssertFalse(FileManager.default.fileExists(atPath: tempDir.appendingPathComponent(context.sidecarFileName).path)) } // MARK: - Drain Semantics @@ -444,7 +412,7 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.drain() coordinator.drain() - XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appendingPathComponent("\(context.baseFileName).json").path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appendingPathComponent(context.sidecarFileName).path)) } func testDrainOnEmptyQueueDoesNotCrash() { @@ -471,8 +439,8 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { coordinator.drain() for context in contexts { - XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appendingPathComponent("\(context.baseFileName).png").path)) - XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appendingPathComponent("\(context.baseFileName).json").path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appendingPathComponent(context.imageFileName).path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: tempDir.appendingPathComponent(context.sidecarFileName).path)) } } } @@ -481,8 +449,8 @@ final class SnapshotCIExportCoordinatorTests: XCTestCase { extension SnapshotCIExportCoordinatorTests { - private func readJSON(forBaseFileName baseFileName: String) throws -> [String: Any] { - let data = try Data(contentsOf: tempDir.appendingPathComponent("\(baseFileName).json")) + private func readJSON(forSidecarFileName sidecarFileName: String) throws -> [String: Any] { + let data = try Data(contentsOf: tempDir.appendingPathComponent(sidecarFileName)) return try JSONSerialization.jsonObject(with: data) as! [String: Any] } @@ -520,7 +488,7 @@ extension SnapshotCIExportCoordinatorTests { additionalContext: [String: SnapshotMetadataValue] = [:] ) -> SnapshotContext { SnapshotContext( - baseFileName: baseFileName, + imageFileName: FileNameUtils.imageFileName(from: baseFileName), testName: "-[MyTests testPreview]", typeName: typeName, typeDisplayName: typeDisplayName,