Skip to content

Commit

Permalink
Simplifying work with swiftSnapshotTesting
Browse files Browse the repository at this point in the history
  • Loading branch information
BarredEwe committed Feb 28, 2025
1 parent 4218199 commit 80a880c
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 137 deletions.
Binary file not shown.
16 changes: 6 additions & 10 deletions Example/Shared/Examples/UIKitView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@ import SwiftUI
import Prefire

#Preview {
{
let view = UIView(frame: .init(origin: .zero, size: .init(width: 100, height: 100)))
view.backgroundColor = .red
return view
}()
let view = UIView(frame: .init(origin: .zero, size: .init(width: 100, height: 100)))
view.backgroundColor = .red
return view
}

#Preview {
{
let viewController = UIViewController()
viewController.view.backgroundColor = .green
return viewController
}()
let viewController = UIViewController()
viewController.view.backgroundColor = .green
return viewController
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ extension PreviewLoader {
guard let model = RawPreviewModel(from: body, filename: fileName) else { return nil }

let componentTestName = model.displayName.components(separatedBy: funcCharacterSet).joined()
let settingsSuffix = (model.snapshotSettings?.replacingOccurrences(of: "snapshot", with: "init")).flatMap { ", settings: \($0)" } ?? ""

let previewCode: String
let content: String
Expand All @@ -31,12 +30,10 @@ extension PreviewLoader {
content = "PreviewWrapper\(model.displayName)()"
}

let prefireSnapshot = "PrefireSnapshot(\(content), name: \"\(model.displayName)\", isScreen: \(model.isScreen), device: deviceConfig\(settingsSuffix))"

return """
func test_\(componentTestName)_Preview() {
\(previewCode)
if let failure = assertSnapshots(for: \(prefireSnapshot)) {
if let failure = assertSnapshots(for: PrefireSnapshot(\(content), name: \"\(model.displayName)\", isScreen: \(model.isScreen), device: deviceConfig)) {
XCTFail(failure)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ struct RawPreviewModel {
var traits: String
var body: String
var properties: String?
var snapshotSettings: String?

var isScreen: Bool {
traits == Constants.defaultTrait
Expand All @@ -16,7 +15,6 @@ extension RawPreviewModel {
private enum Markers {
static let previewMacro = "#Preview"
static let traits = "traits: "
static let snasphotSettings = ".snapshot("
static let previewable = "@Previewable"
}

Expand Down Expand Up @@ -54,10 +52,8 @@ extension RawPreviewModel {


for (index, line) in lines.enumerated() {
// Search for the line with snapshot settings
if snapshotSettings == nil, line.contains(Markers.snasphotSettings) {
self.snapshotSettings = line.trimmingCharacters(in: .whitespaces)
} else if line.contains(Markers.previewable) {
// Search for the line with `@Previewable` macro
if line.contains(Markers.previewable) {
lines.remove(at: index + 1)
if self.properties == nil {
self.properties = String(line.replacing("\(Markers.previewable) ", with: ""))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class RawPreviewModelTests: XCTestCase {
XCTAssertEqual(rawPreviewModel?.properties, nil)
XCTAssertEqual(rawPreviewModel?.displayName, "TestViewName")
XCTAssertEqual(rawPreviewModel?.traits, ".sizeThatFitsLayout")
XCTAssertEqual(rawPreviewModel?.snapshotSettings, nil)
}

func test_initWithoutName() {
Expand All @@ -37,6 +36,5 @@ class RawPreviewModelTests: XCTestCase {
XCTAssertEqual(rawPreviewModel?.properties, " @State var name: String = \"TestView\"")
XCTAssertEqual(rawPreviewModel?.displayName, "TestView")
XCTAssertEqual(rawPreviewModel?.traits, ".sizeThatFitsLayout")
XCTAssertEqual(rawPreviewModel?.snapshotSettings, ".snapshot(delay: 8)")
}
}
64 changes: 54 additions & 10 deletions Sources/Prefire/Preview/PrefireSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,88 @@ public struct DeviceConfig {
}

@MainActor public struct PrefireSnapshot<Content: SwiftUI.View> {
public var content: Content
private var previewContent: Content
public var name: String
public var isScreen: Bool
public var device: DeviceConfig
public var traits: UITraitCollection = .init()
public var settings: PreferenceKeys?

private var content: AnyView {
if isScreen {
AnyView(previewContent)
} else {
AnyView(
previewContent
.frame(width: device.size?.width)
.fixedSize(horizontal: false, vertical: true)
)
}
}

public init(_ preview: _Preview, testName: String = #function, device: DeviceConfig, settings: PreferenceKeys? = nil) where Content == AnyView {
content = preview.content
public init(_ preview: _Preview, testName: String = #function, device: DeviceConfig) where Content == AnyView {
previewContent = preview.content
name = preview.displayName ?? testName
isScreen = preview.layout == .device
self.device = device
self.settings = settings
}

public init(_ view: Content, name: String, isScreen: Bool, device: DeviceConfig, traits: UITraitCollection = .init(), settings: PreferenceKeys? = nil) {
content = view
public init(_ view: Content, name: String, isScreen: Bool, device: DeviceConfig, traits: UITraitCollection = .init()) {
previewContent = view
self.name = name
self.isScreen = isScreen
self.device = device
self.traits = traits
self.settings = settings
}

public init(_ view: UIView, name: String, isScreen: Bool, device: DeviceConfig, traits: UITraitCollection = .init()) where Content == ViewRepresentable<UIView> {
content = ViewRepresentable(view: view)
previewContent = ViewRepresentable(view: view)
self.name = name
self.isScreen = isScreen
self.device = device
self.traits = traits
}

public init(_ viewController: UIViewController, name: String, isScreen: Bool, device: DeviceConfig, traits: UITraitCollection = .init()) where Content == ViewControllerRepresentable<UIViewController> {
content = ViewControllerRepresentable(viewController: viewController)
previewContent = ViewControllerRepresentable(viewController: viewController)
self.name = name
self.isScreen = isScreen
self.device = device
self.traits = traits
}

public func loadViewWithPreferences() -> (AnyView, PreferenceKeys) {
let preferences = PreferenceKeys()

let view = AnyView(
content
.onPreferenceChange(DelayPreferenceKey.self) {
preferences.delay = $0
}
.onPreferenceChange(PrecisionPreferenceKey.self) {
preferences.precision = $0
}
.onPreferenceChange(PerceptualPrecisionPreferenceKey.self) {
preferences.perceptualPrecision = $0
}
.onPreferenceChange(RecordPreferenceKey.self) {
preferences.record = $0
}
)

// In order to call onPreferenceChange, render the view once
render(view: view)

return (view, preferences)
}

// MARK: - Private functions

private func render(view: AnyView) {
let hostingController = UIHostingController(rootView: view)
let window = UIWindow(frame: .init())

window.isHidden = false
window.rootViewController = hostingController
}
}
#endif
119 changes: 14 additions & 105 deletions Templates/PreviewTests.stencil
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {{ import }}
{% for import in argument.testableImports %}
@testable import {{ import }}
{% endfor %}
@testable import SnapshotTesting
import SnapshotTesting
#if canImport(AccessibilitySnapshot)
import AccessibilitySnapshot
#endif
Expand Down Expand Up @@ -92,43 +92,26 @@ import {{ import }}
}

private func assertSnapshot<Content: SwiftUI.View>(for prefireSnapshot: PrefireSnapshot<Content>) -> String? {
let preferences = prefireSnapshot.settings ?? .init()

let view = prefireSnapshot.content
.onPreferenceChange(DelayPreferenceKey.self) { preferences.delay = $0 }
.onPreferenceChange(PrecisionPreferenceKey.self) { preferences.precision = $0 }
.onPreferenceChange(PerceptualPrecisionPreferenceKey.self) { preferences.perceptualPrecision = $0 }
.onPreferenceChange(RecordPreferenceKey.self) { preferences.record = $0 }

let matchingView = prefireSnapshot.isScreen ? AnyView(view) : AnyView(view
.frame(width: prefireSnapshot.device.size?.width)
.fixedSize(horizontal: false, vertical: true)
)

// In order to call onPreferenceChange, render the view once
let hostingController = UIHostingController(rootView: matchingView)
let window = UIWindow(frame: .init())
window.isHidden = false
window.rootViewController = hostingController
hostingController.beginAppearanceTransition(true, animated: false)
hostingController.endAppearanceTransition()
hostingController.view.setNeedsLayout()
hostingController.view.layoutIfNeeded()
let (previewView, preferences) = prefireSnapshot.loadViewWithPreferences()

let failure = verifySnapshot(
of: matchingView,
as: .prefireImage(precision: { preferences.precision },
perceptualPrecision: { preferences.perceptualPrecision },
duration: { preferences.delay },
layout: prefireSnapshot.isScreen ? .device(config: prefireSnapshot.device.imageConfig) : .sizeThatFits,
traits: prefireSnapshot.traits){% if argument.file %},
record: preferences.record,
of: previewView,
as: .wait(
for: preferences.delay,
on: .image(
precision: preferences.precision,
perceptualPrecision: preferences.perceptualPrecision,
layout: prefireSnapshot.isScreen ? .device(config: prefireSnapshot.device.imageConfig) : .sizeThatFits,
traits: prefireSnapshot.traits
)
),
record: preferences.record,{% if argument.file %}
file: file{% endif %},
testName: prefireSnapshot.name
)

#if canImport(AccessibilitySnapshot)
let vc = UIHostingController(rootView: matchingView)
let vc = UIHostingController(rootView: previewView)
vc.view.frame = UIScreen.main.bounds

SnapshotTesting.assertSnapshot(
Expand Down Expand Up @@ -205,77 +188,3 @@ private extension PreviewDevice {
(self.snapshotDevice())?.deviceConfig
}
}

private extension Snapshotting where Value: SwiftUI.View, Format == UIImage {
@MainActor
static func prefireImage(
drawHierarchyInKeyWindow: Bool = false,
precision: @escaping () -> Float,
perceptualPrecision: @escaping () -> Float,
duration: @escaping () -> TimeInterval,
layout: SwiftUISnapshotLayout = .sizeThatFits,
traits: UITraitCollection = .init()
) -> Snapshotting {
let config: ViewImageConfig

switch layout {
#if os(iOS) || os(tvOS)
case let .device(config: deviceConfig):
config = deviceConfig
#endif
case .sizeThatFits:
config = .init(safeArea: .zero, size: nil, traits: traits)
case let .fixed(width: width, height: height):
let size = CGSize(width: width, height: height)
config = .init(safeArea: .zero, size: size, traits: traits)
}

return SimplySnapshotting<UIImage>(pathExtension: "png", diffing: .prefireImage(precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale))
.asyncPullback { view in
var config = config
let controller: UIViewController

if config.size != nil {
controller = UIHostingController(rootView: view)
} else {
let hostingController = UIHostingController(rootView: view)

let maxSize = CGSize.zero
config.size = hostingController.sizeThatFits(in: maxSize)

controller = hostingController
}

return Async<UIImage> { callback in
let strategy = snapshotView(
config: config,
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
traits: traits,
view: controller.view,
viewController: controller
)

let duration = duration()
if duration != .zero {
let expectation = XCTestExpectation(description: "Wait")
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
expectation.fulfill()
}
_ = XCTWaiter.wait(for: [expectation], timeout: duration + 1)
}
strategy.run(callback)
}
}
}
}

private extension Diffing where Value == UIImage {
static func prefireImage(precision: @escaping () -> Float, perceptualPrecision: @escaping () -> Float, scale: CGFloat?) -> Diffing {
lazy var originalDiffing = Diffing.image(precision: precision(), perceptualPrecision: perceptualPrecision(), scale: scale)
return Diffing(
toData: { originalDiffing.toData($0) },
fromData: { originalDiffing.fromData($0) },
diff: { originalDiffing.diff($0, $1) }
)
}
}

0 comments on commit 80a880c

Please sign in to comment.