diff --git a/Package.resolved b/Package.resolved index 8b5ef50..7e06ec9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "SnapshotTestingHEIC", + "repositoryURL": "https://github.com/alexey1312/SnapshotTestingHEIC.git", + "state": { + "branch": null, + "revision": "a8e6abbdde6969f0a8fb80f1ca62640824905387", + "version": "1.0.1" + } + }, { "package": "SnapshotTesting", "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git", diff --git a/Package.swift b/Package.swift index 6d54059..808ef7c 100644 --- a/Package.swift +++ b/Package.swift @@ -16,13 +16,18 @@ let package = Package( ], dependencies: [ .package(name: "SnapshotTesting", - url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.8.0"), + url: "https://github.com/pointfreeco/swift-snapshot-testing.git", + from: "1.8.0"), + .package(name: "SnapshotTestingHEIC", + url: "https://github.com/alexey1312/SnapshotTestingHEIC.git", + from: "1.0.0"), ], targets: [ .target( name: "SnapshotTestingStitch", dependencies: [ .product(name: "SnapshotTesting", package: "SnapshotTesting"), + .product(name: "SnapshotTestingHEIC", package: "SnapshotTestingHEIC"), ] ), diff --git a/Sources/SnapshotTestingStitch/ImageFormat.swift b/Sources/SnapshotTestingStitch/ImageFormat.swift new file mode 100644 index 0000000..ba6d4f8 --- /dev/null +++ b/Sources/SnapshotTestingStitch/ImageFormat.swift @@ -0,0 +1,7 @@ +import Foundation +import SnapshotTestingHEIC + +public enum ImageFormat { + case png + case heic(_ compression: CompressionQuality = .lossless) +} diff --git a/Sources/SnapshotTestingStitch/Snapshotting.swift b/Sources/SnapshotTestingStitch/Snapshotting.swift index 13aa331..cd9cf4b 100644 --- a/Sources/SnapshotTestingStitch/Snapshotting.swift +++ b/Sources/SnapshotTestingStitch/Snapshotting.swift @@ -1,57 +1,89 @@ import UIKit import SnapshotTesting +import SnapshotTestingHEIC -extension Snapshotting where Format == UIImage { - +public extension Snapshotting where Format == UIImage { /// Stitches multiple visual snapshot strategies into a single image asset. /// /// - Parameters: - /// - strategies: The unnamed tasks which should be carried out, in the order that they should be displayed. - /// Any strategy can be used as long as the output format is UIImage. - /// - style: The style configuration which allows for you to customise the appearance of the output image, including but not limited to the item - /// spacing, and optional image borders. - /// - precision: The percentage of pixels that must match in the final comparison in order for the test to successfully pass. - public static func stitch( + /// - tasks: The unnamed tasks which should be carried out, in the order that they should be displayed. + /// Any strategy can be used as long as the output format is UIImage. + /// - style: The style configuration which allows for you to customise the appearance of the output image, + /// including but not limited to the item spacing, and optional image borders. + /// - precision: The percentage of pixels that must match in the final comparison + /// in order for the test to successfully pass. + /// - format: The desired image format to use when writing to an image destination. + /// It would default to PNG for backwards compatibility. + static func stitch( strategies tasks: [Snapshotting], style: StitchStyle = .init(), - precision: Float = 1 + precision: Float = 1, + format: ImageFormat = .png ) -> Snapshotting { // Default to an empty string, if they choose not to provide one. - stitch(strategies: tasks.map { .init(name: nil, strategy: $0, configure: nil) }, style: style, precision: precision) + stitch( + strategies: tasks.map { .init(name: nil, strategy: $0, configure: nil) }, + style: style, + precision: precision, + format: format + ) } - + /// Stitches multiple visual snapshot strategies into a single image asset. /// /// - Parameters: - /// - strategies: The named tasks which should be carried out, in the order that they should be displayed. Titles will be displayed above their - /// respectful image, allowing for easier identification. Any strategy can be used as long as the output format is UIImage. - /// - style: The style configuration which allows for you to customise the appearance of the output image, including but not limited to the item - /// spacing, and optional image borders. - /// - precision: The percentage of pixels that must match in the final comparison in order for the test to successfully pass. - public static func stitch( + /// - tasks: The named tasks which should be carried out, in the order that they should be displayed. + /// Titles will be displayed above their respectful image, allowing for easier identification. + /// Any strategy can be used as long as the output format is UIImage. + /// - style: The style configuration which allows for you to customise the appearance of the output image, + /// including but not limited to the item spacing, and optional image borders. + /// - precision: The percentage of pixels that must match in the final comparison + /// in order for the test to successfully pass. + /// - format: The desired image format to use when writing to an image destination. + /// It would default to PNG for backwards compatibility. + static func stitch( strategies tasks: [(name: String, strategy: Snapshotting)], style: StitchStyle = .init(), - precision: Float = 1 + precision: Float = 1, + format: ImageFormat = .png ) -> Snapshotting { - stitch(strategies: tasks.map { .init(name: $0.name, strategy: $0.strategy, configure: nil) }, style: style, precision: precision) + stitch( + strategies: tasks.map { .init(name: $0.name, strategy: $0.strategy, configure: nil) }, + style: style, + precision: precision, + format: format + ) } - + /// Stitches multiple visual snapshot strategies into a single image asset. /// /// - Parameters: - /// - strategies: The tasks which should be carried out, in the order that they should be displayed. Tasks can include a title which will be displayed above - /// their respectful image, allowing for easier identification. Any strategy can be used as long as the output format is UIImage. Tasks can - /// can also contain a configuration block which allows for you to modify the value just before it's snapshot is taken. - /// - style: The style configuration which allows for you to customise the appearance of the output image, including but not limited to the item - /// spacing, and optional image borders. - /// - precision: The percentage of pixels that must match in the final comparison in order for the test to successfully pass. - public static func stitch( + /// - tasks: The tasks which should be carried out, in the order that they should be displayed. + /// Tasks can include a title which will be displayed above their respectful image, + /// allowing for easier identification. Any strategy can be used as long as the output format is UIImage. + /// Tasks can can also contain a configuration block which allows for you to modify + /// the value just before it's snapshot is taken. + /// - style: The style configuration which allows for you to customise the appearance of the output image, + /// including but not limited to the item spacing, and optional image borders. + /// - precision: The percentage of pixels that must match in the final comparison + /// in order for the test to successfully pass. + /// - format: The desired image format to use when writing to an image destination. + /// It would default to PNG for backwards compatibility. + static func stitch( strategies tasks: [StitchTask], style: StitchStyle = .init(), - precision: Float = 1 + precision: Float = 1, + format: ImageFormat = .png ) -> Snapshotting { - let internalStrategy: Snapshotting = .image(precision: precision) - + let internalStrategy: Snapshotting + + switch format { + case .png: + internalStrategy = .image(precision: precision) + case .heic(let compressionQuality): + internalStrategy = .imageHEIC(precision: precision, compressionQuality: compressionQuality) + } + return Snapshotting( pathExtension: internalStrategy.pathExtension, diffing: internalStrategy.diffing @@ -64,38 +96,40 @@ extension Snapshotting where Format == UIImage { callback(UIImage()) return } - + // Create a dispatch group to keep track of the remaining tasks let dispatchGroup = DispatchGroup() - + // Create an array to store the final outputs to be stitched var values = [(index: Int, title: String?, output: UIImage)]() - + // Loop over each of the user-provided strategies, snapshot them, // store the output, and update the dispatch group. tasks.enumerated().forEach { index, task in dispatchGroup.enter() - + var mutableValue = value task.configure?(&mutableValue) - + task.strategy.snapshot(mutableValue).run { output in values.append((index, task.name, output)) dispatchGroup.leave() } } - + // Once all strategies have been completed... dispatchGroup.notify(queue: .main) { // Sort values based on input order let sortedValues: [(String?, UIImage)] = values .sorted(by: { lhs, rhs in lhs.index < rhs.index }) .map { result in (result.title, result.output) } - + // Check to ensure all tasks have been returned - assert(sortedValues.count == tasks.count, - "Inconsistant number of outputted values in comparison to inputted strategies") - + assert( + sortedValues.count == tasks.count, + "Inconsistant number of outputted values in comparison to inputted strategies" + ) + // Stitch them together, and callback to the snapshot testing library. let image = ImageStitcher(inputs: sortedValues).stitch(style: style) callback(image) @@ -103,5 +137,4 @@ extension Snapshotting where Format == UIImage { } } } - } diff --git a/Tests/SnapshotTestingStitchTests/SnapshotTestingStitchHEIC.swift b/Tests/SnapshotTestingStitchTests/SnapshotTestingStitchHEIC.swift new file mode 100644 index 0000000..1f19b56 --- /dev/null +++ b/Tests/SnapshotTestingStitchTests/SnapshotTestingStitchHEIC.swift @@ -0,0 +1,130 @@ +import XCTest +import SnapshotTesting +import SnapshotTestingHEIC +@testable import SnapshotTestingStitch + +final class SnapshotTestingStitchHEICTests: XCTestCase { + + let isRecording: Bool = false + + func test_withTitles() { + assertSnapshot( + matching: createTestViewController(), + as: .stitch( + strategies: [ + ("iPhone 8", .image(on: .iPhone8)), + ("iPhone 8 Plus", .image(on: .iPhone8Plus)), + ], + format: .heic() + ), + record: isRecording + ) + } + + func test_withoutTitles() { + assertSnapshot( + matching: createTestViewController(), + as: .stitch( + strategies: [ + .image(on: .iPhone8), + .image(on: .iPhone8Plus), + ], + format: .heic() + ), + record: isRecording + ) + } + + func test_withoutBorder() { + assertSnapshot( + matching: createTestViewController(), + as: .stitch( + strategies: [ + ("iPhone 8", .image(on: .iPhone8)), + ("iPhone 8 Plus", .image(on: .iPhone8Plus)), + ], + style: .init(borderWidth: 0), + format: .heic() + ), + record: isRecording + ) + } + + func test_withManyDevices() { + assertSnapshot( + matching: createTestViewController(), + as: .stitch( + strategies: [ + ("iPhone 8", .image(on: .iPhone8)), + ("iPhone 8 Plus", .image(on: .iPhone8Plus)), + ("iPhone X", .image(on: .iPhoneX)), + ("iPhone SE", .image(on: .iPhoneSe)), + ("iPhone Xr", .image(on: .iPhoneXr)), + ("iPhone Xs Max", .image(on: .iPhoneXsMax)), + ("iPhone Xs Max (Landscape)", .image(on: .iPhoneXsMax(.landscape))), + ], + format: .heic() + ), + record: isRecording + ) + } + + func test_withView() { + assertSnapshot( + matching: createTestView(), + as: .stitch( + strategies: [ + ("100x", .image(size: CGSize(width: 100, height: 100))), + ("250x", .image(size: CGSize(width: 250, height: 250))), + ], + format: .heic() + ), + record: isRecording + ) + } + + func test_withNoStrategies() { + // You actually get a compiler warning for ambiguity by default, so you have to go through some loops to pass + // literally nothing through. + let tasks: [Snapshotting] = [] + + assertSnapshot( + matching: createTestView(), + as: .stitch(strategies: tasks, format: .heic()), + record: isRecording + ) + } + + func test_withConfigure() { + assertSnapshot( + matching: createTestViewController(), + as: .stitch(strategies: [ + .init(name: "Green", strategy: .image, configure: { $0.view.backgroundColor = .green }), + .init(name: "Pink", strategy: .image, configure: { $0.view.backgroundColor = .systemPink }), + // The input value is being manipulated, which means if you don't reconfigure it then it will be the + // same as the previous test. + .init(name: "Pink (No Configure)", strategy: .image, configure: nil), + ], + format: .heic() + ), + record: isRecording + ) + } + + // MARK: - Helpers + + func createTestViewController() -> UIViewController { + let viewController = UIViewController() + viewController.view.backgroundColor = .blue + + return viewController + } + + func createTestView() -> UIView { + let view = UIView(frame: .zero) + view.backgroundColor = .green + + return view + } + +} diff --git a/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withConfigure.1.heic b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withConfigure.1.heic new file mode 100644 index 0000000..c784593 Binary files /dev/null and b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withConfigure.1.heic differ diff --git a/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withManyDevices.1.heic b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withManyDevices.1.heic new file mode 100644 index 0000000..7abdebf Binary files /dev/null and b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withManyDevices.1.heic differ diff --git a/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withNoStrategies.1.heic b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withNoStrategies.1.heic new file mode 100644 index 0000000..096546c Binary files /dev/null and b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withNoStrategies.1.heic differ diff --git a/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withTitles.1.heic b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withTitles.1.heic new file mode 100644 index 0000000..eaf0270 Binary files /dev/null and b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withTitles.1.heic differ diff --git a/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withView.1.heic b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withView.1.heic new file mode 100644 index 0000000..ac9c317 Binary files /dev/null and b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withView.1.heic differ diff --git a/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withoutBorder.1.heic b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withoutBorder.1.heic new file mode 100644 index 0000000..97f762a Binary files /dev/null and b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withoutBorder.1.heic differ diff --git a/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withoutTitles.1.heic b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withoutTitles.1.heic new file mode 100644 index 0000000..c59b82c Binary files /dev/null and b/Tests/SnapshotTestingStitchTests/__Snapshots__/SnapshotTestingStitchHEIC/test_withoutTitles.1.heic differ