Skip to content

Instantly share code, notes, and snippets.

@khramtsoff
Created November 11, 2020 14:54
Show Gist options
  • Save khramtsoff/4063894b02b265bebfe2c80dd5d50792 to your computer and use it in GitHub Desktop.
Save khramtsoff/4063894b02b265bebfe2c80dd5d50792 to your computer and use it in GitHub Desktop.
XCTestCase+Snapshot.swift
// let sut = UIView()
// let snapshot = sut.snapshot(for: .init(size: sut.frame.size, traitCollection: .init(layoutDirection: .rightToLeft)))
// assert(snapshot: snapshot, named: "my_view")
// record(snapshot: snapshot, named: "my_view")
import UIKit
import XCTest
extension XCTestCase {
func assert(snapshot: UIImage, named name: String, file: StaticString = #filePath, line: UInt = #line) {
let snapshotURL = makeSnapshotURL(named: name, file: file)
let snapshotData = makeSnapshotData(for: snapshot, file: file, line: line)
guard let storedSnapshotData = try? Data(contentsOf: snapshotURL) else {
XCTFail("Failed to load stored snapshot at URL: \(snapshotURL). Use the `record` method to store a snapshot before asserting.", file: file, line: line)
return
}
if snapshotData != storedSnapshotData {
let temporarySnapshotURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent(snapshotURL.lastPathComponent)
try? snapshotData?.write(to: temporarySnapshotURL)
XCTFail("New snapshot does not match stored snapshot. New snapshot URL: \(temporarySnapshotURL), Stored snapshot URL: \(snapshotURL)", file: file, line: line)
}
}
func record(snapshot: UIImage, named name: String, file: StaticString = #filePath, line: UInt = #line) {
let snapshotURL = makeSnapshotURL(named: name, file: file)
let snapshotData = makeSnapshotData(for: snapshot, file: file, line: line)
do {
try FileManager.default.createDirectory(
at: snapshotURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try snapshotData?.write(to: snapshotURL)
XCTFail("Record succeeded at URL: \(snapshotURL). Use `assert` to compare the snapshot from now on.", file: file, line: line)
} catch {
XCTFail("Failed to record snapshot with error: \(error)", file: file, line: line)
}
}
private func makeSnapshotURL(named name: String, file: StaticString) -> URL {
return URL(fileURLWithPath: String(describing: file))
.deletingLastPathComponent()
.appendingPathComponent("snapshots")
.appendingPathComponent("\(name).png")
}
private func makeSnapshotData(for snapshot: UIImage, file: StaticString, line: UInt) -> Data? {
guard let data = snapshot.pngData() else {
XCTFail("Failed to generate PNG data representation from snapshot", file: file, line: line)
return nil
}
return data
}
}
extension UIView {
func snapshot(for configuration: SnapshotConfiguration) -> UIImage {
let root = UIViewController()
root.view = self
return SnapshotWindow(configuration: configuration, root: root).snapshot()
}
}
struct SnapshotConfiguration {
let size: CGSize
let safeAreaInsets: UIEdgeInsets
let layoutMargins: UIEdgeInsets
let traitCollection: UITraitCollection
init(size: CGSize = .zero, safeAreaInsets: UIEdgeInsets = .zero, layoutMargins: UIEdgeInsets = .zero, traitCollection: UITraitCollection = UITraitCollection(traitsFrom: [])) {
self.size = size
self.safeAreaInsets = safeAreaInsets
self.layoutMargins = layoutMargins
self.traitCollection = traitCollection
}
}
final class SnapshotWindow: UIWindow {
private var configuration = SnapshotConfiguration()
convenience init(configuration: SnapshotConfiguration, root: UIViewController) {
self.init(frame: CGRect(origin: .zero, size: configuration.size))
self.configuration = configuration
self.layoutMargins = configuration.layoutMargins
self.rootViewController = root
self.isHidden = false
root.view.layoutMargins = configuration.layoutMargins
}
override var safeAreaInsets: UIEdgeInsets {
return configuration.safeAreaInsets
}
override var traitCollection: UITraitCollection {
return UITraitCollection(traitsFrom: [super.traitCollection, configuration.traitCollection])
}
func snapshot() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds, format: .init(for: traitCollection))
return renderer.image { action in
layer.render(in: action.cgContext)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment