Created
November 11, 2020 14:54
-
-
Save khramtsoff/4063894b02b265bebfe2c80dd5d50792 to your computer and use it in GitHub Desktop.
XCTestCase+Snapshot.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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