diff --git a/Package.swift b/Package.swift index da0b2ea6..50fa8597 100644 --- a/Package.swift +++ b/Package.swift @@ -4,62 +4,109 @@ import PackageDescription let package = Package( - name: "SnapshotPreviews", - platforms: [.iOS(.v15), .macOS(.v12), .watchOS(.v9)], - products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .library( - name: "PreviewGallery", - type: .static, // Replace this to build dynamic - targets: ["PreviewGallery"]), - // Test library to import in your XCTest target. - // This is the only library that depends on XCTest.framework - .library( - name: "SnapshottingTests", - type: .static, // Replace this to build dynamic - targets: ["SnapshottingTests"]), - // Link the main app to this target to use custom snapshot settings - // This lib does not get inserted when running tests to avoid - // duplicate symbols. - .library( - name: "SnapshotPreferences", - targets: ["SnapshotPreferences"]), - // Core functionality for snapshotting exported from the internal package - .library( - name: "SnapshotPreviewsCore", - targets: ["SnapshotPreviewsCore"]), - // Dynamic library that your main app will have inserted to generate previews - .library( - name: "Snapshotting", - type: .dynamic, - targets: ["Snapshotting"]), - ], - dependencies: [ - .package(url: "https://github.com/swhitty/FlyingFox.git", exact: "0.16.0"), - .package(url: "https://github.com/EmergeTools/AccessibilitySnapshot.git", exact: "1.0.2"), - .package(url: "https://github.com/EmergeTools/SimpleDebugger.git", exact: "1.0.0"), - ], - targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - // Target that provides the XCTest - .target(name: "SnapshottingTestsObjc", dependencies: [.product(name: "SimpleDebugger", package: "SimpleDebugger", condition: .when(platforms: [.iOS, .macOS, .macCatalyst]))]), - .target(name: "SnapshottingTests", dependencies: ["SnapshotPreviewsCore", "SnapshottingTestsObjc"]), - .target(name: "SnapshotSharedModels"), - // Core functionality - .target(name: "SnapshotPreviewsCore", dependencies: ["PreviewsSupport", "SnapshotSharedModels", .product(name: "AccessibilitySnapshotCore", package: "AccessibilitySnapshot", condition: .when(platforms: [.iOS, .macCatalyst]))]), - .target(name: "SnapshotPreferences", dependencies: ["SnapshotSharedModels"]), - // Inserted dylib - .target(name: "Snapshotting", dependencies: ["SnapshottingSwift"]), - // Swift code in the inserted dylib - .target(name: "SnapshottingSwift", dependencies: ["SnapshotPreviewsCore", .product(name: "FlyingFox", package: "FlyingFox")]), - .target(name: "PreviewGallery", dependencies: ["SnapshotPreviewsCore", "SnapshotPreferences"]), - .binaryTarget( - name: "PreviewsSupport", - path: "PreviewsSupport/PreviewsSupport.xcframework"), - .testTarget( - name: "SnapshotPreviewsTests", - dependencies: ["SnapshotPreviewsCore"]), - ], - cxxLanguageStandard: .cxx11 + name: "SnapshotPreviews", + platforms: [.iOS(.v15), .macOS(.v12), .watchOS(.v9)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "PreviewGallery", + type: .static, // Replace this to build dynamic + targets: ["PreviewGallery"] + ), + // Test library to import in your XCTest target. + // This is the only library that depends on XCTest.framework + .library( + name: "SnapshottingTests", + type: .static, // Replace this to build dynamic + targets: ["SnapshottingTests"] + ), + // Link the main app to this target to use custom snapshot settings + // This lib does not get inserted when running tests to avoid + // duplicate symbols. + .library( + name: "SnapshotPreferences", + targets: ["SnapshotPreferences"] + ), + // Core functionality for snapshotting exported from the internal package + .library( + name: "SnapshotPreviewsCore", + targets: ["SnapshotPreviewsCore"] + ), + // Dynamic library that your main app will have inserted to generate previews + .library( + name: "Snapshotting", + type: .dynamic, + targets: ["Snapshotting"] + ), + ], + dependencies: [ + .package(url: "https://github.com/swhitty/FlyingFox.git", exact: "0.16.0"), + .package( + url: "https://github.com/EmergeTools/AccessibilitySnapshot.git", + exact: "1.0.2" + ), + .package( + url: "https://github.com/EmergeTools/SimpleDebugger.git", + exact: "1.0.0" + ), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + // Target that provides the XCTest + .target( + name: "SnapshottingTestsObjc", + dependencies: [ + .product( + name: "SimpleDebugger", + package: "SimpleDebugger", + condition: .when(platforms: [.iOS, .macOS, .macCatalyst]) + ) + ] + ), + .target( + name: "SnapshottingTests", + dependencies: ["SnapshotPreviewsCore", "SnapshottingTestsObjc"] + ), + .target(name: "SnapshotSharedModels"), + // Core functionality + .target( + name: "SnapshotPreviewsCore", + dependencies: [ + "PreviewsSupport", "SnapshotSharedModels", + .product( + name: "AccessibilitySnapshotCore", + package: "AccessibilitySnapshot", + condition: .when(platforms: [.iOS, .macCatalyst]) + ), + ] + ), + .target( + name: "SnapshotPreferences", + dependencies: ["SnapshotSharedModels"] + ), + // Inserted dylib + .target(name: "Snapshotting", dependencies: ["SnapshottingSwift"]), + // Swift code in the inserted dylib + .target( + name: "SnapshottingSwift", + dependencies: [ + "SnapshotPreviewsCore", + .product(name: "FlyingFox", package: "FlyingFox"), + ] + ), + .target( + name: "PreviewGallery", + dependencies: ["SnapshotPreviewsCore", "SnapshotPreferences"] + ), + .binaryTarget( + name: "PreviewsSupport", + path: "PreviewsSupport/PreviewsSupport.xcframework" + ), + .testTarget( + name: "SnapshotPreviewsTests", + dependencies: ["SnapshotPreviewsCore", "SnapshotPreferences"] + ), + ], + cxxLanguageStandard: .cxx11 ) diff --git a/Sources/SnapshotPreviewsCore/EmergeModifierView.swift b/Sources/SnapshotPreviewsCore/EmergeModifierView.swift new file mode 100644 index 00000000..a468a6da --- /dev/null +++ b/Sources/SnapshotPreviewsCore/EmergeModifierView.swift @@ -0,0 +1,64 @@ +// +// ModifierFinder.swift +// +// +// Created by Noah Martin on 7/8/24. +// + +import Foundation +import SnapshotSharedModels +import SwiftUI + +public struct EmergeModifierView: View { + + private static let modifierFinderClass = + (NSClassFromString("EmergeModifierFinder") as? NSObject.Type)?.init() + private static let finder = + modifierFinderClass != nil + ? Mirror(reflecting: modifierFinderClass!).descendant("finder") + as? (any View) -> any View : nil + private static let modifierState = + NSClassFromString("EmergeModifierState") as? NSObject.Type + private static let stateMirror = + modifierState != nil + ? Mirror( + reflecting: modifierState! + .perform(NSSelectorFromString("shared")) + .takeUnretainedValue() + ) : nil + + private let internalView: AnyView + + init(wrapped: some View) { + let rootView = Self.finder?(wrapped) + internalView = rootView != nil ? AnyView(rootView!) : AnyView(wrapped) + } + + public var body: some View { + internalView + } + + var emergeRenderingMode: EmergeRenderingMode? { + let renderingMode = + Self.stateMirror?.descendant("renderingMode") + as? EmergeRenderingMode.RawValue + return renderingMode != nil + ? EmergeRenderingMode(rawValue: renderingMode!) : nil + } + + var accessibilityEnabled: Bool? { + Self.stateMirror?.descendant("accessibilityEnabled") as? Bool + } + + var appStoreSnapshot: Bool? { + Self.stateMirror?.descendant("appStoreSnapshot") as? Bool + } + + var precision: Float? { + Self.stateMirror?.descendant("precision") as? Float + } + + var supportsExpansion: Bool { + Self.stateMirror?.descendant("expansionPreference") as? Bool ?? true + } +} diff --git a/Sources/SnapshotPreviewsCore/ModifierFinder.swift b/Sources/SnapshotPreviewsCore/ModifierFinder.swift deleted file mode 100644 index 476aada7..00000000 --- a/Sources/SnapshotPreviewsCore/ModifierFinder.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// ModifierFinder.swift -// -// -// Created by Noah Martin on 7/8/24. -// - -import Foundation -import SwiftUI -import SnapshotSharedModels - -private let modifierFinderClass = (NSClassFromString("EmergeModifierFinder") as? NSObject.Type)?.init() -private let finder = modifierFinderClass != nil ? Mirror(reflecting: modifierFinderClass!).descendant("finder") as? (any View) -> any View : nil -private let modifierState = NSClassFromString("EmergeModifierState") as? NSObject.Type -private let stateMirror = modifierState != nil ? Mirror( - reflecting: modifierState! - .perform(NSSelectorFromString("shared")) - .takeUnretainedValue()) : nil - -public struct EmergeModifierView: View { - - private let internalView: AnyView - - init(wrapped: some View) { - let rootView = finder?(wrapped) - internalView = rootView != nil ? AnyView(rootView!) : AnyView(wrapped) - } - - public var body: some View { - internalView - } - - var emergeRenderingMode: EmergeRenderingMode? { - let renderingMode = stateMirror?.descendant("renderingMode") as? EmergeRenderingMode.RawValue - return renderingMode != nil ? EmergeRenderingMode(rawValue: renderingMode!) : nil - } - - var accessibilityEnabled: Bool? { - stateMirror?.descendant("accessibilityEnabled") as? Bool - } - - var appStoreSnapshot: Bool? { - stateMirror?.descendant("appStoreSnapshot") as? Bool - } - - var precision: Float? { - stateMirror?.descendant("precision") as? Float - } - - var supportsExpansion: Bool { - stateMirror?.descendant("expansionPreference") as? Bool ?? true - } -} diff --git a/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift b/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift new file mode 100644 index 00000000..fd93d689 --- /dev/null +++ b/Tests/SnapshotPreviewsTests/ConformanceLookupTests.swift @@ -0,0 +1,17 @@ +import XCTest + +@testable import SnapshotPreviewsCore + +final class ConformanceLookupTests: XCTestCase { + func test_getPreviewTypes() throws { + let types = getPreviewTypes() + XCTAssertEqual(types.count, 1) + + let firstType = types.first! + XCTAssertEqual( + "SnapshotPreviewsTests.$s21SnapshotPreviewsTests0019TestViewswift_DJEEdfMX15_0_33_B5E96601318DE1EC85533DD88EB53190Ll7PreviewfMf_15PreviewRegistryfMu_", + firstType.name + ) + XCTAssertEqual("PreviewRegistry", firstType.proto) + } +} diff --git a/Tests/SnapshotPreviewsTests/ETBrowserTests.swift b/Tests/SnapshotPreviewsTests/ETBrowserTests.swift deleted file mode 100644 index e815f157..00000000 --- a/Tests/SnapshotPreviewsTests/ETBrowserTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import XCTest -@testable import SnapshotPreviewsCore - -final class SnapshotPreviewsTest: XCTestCase { - func testExample() throws { - // XCTest Documentation - // https://developer.apple.com/documentation/xctest - - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods - } -} diff --git a/Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift b/Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift new file mode 100644 index 00000000..e6e5b6e5 --- /dev/null +++ b/Tests/SnapshotPreviewsTests/EmergeModifierStateTests.swift @@ -0,0 +1,72 @@ +// +// EmergeModifierViewTests.swift +// SnapshotPreviews +// +// Created by Trevor Elkins on 4/30/25. +// + +import SwiftUI +import XCTest + +@testable import SnapshotPreferences +@testable import SnapshotSharedModels +@testable import SnapshotPreviewsCore + +final class EmergeModifierStateTests: XCTestCase { + + override func setUp() { + super.setUp() + EmergeModifierState.shared.reset() + } + + func test_storesRenderingMode() { + let view = makeBaseText().emergeRenderingMode(.uiView) + let state = state(for: view) + XCTAssertEqual(state.renderingMode, EmergeRenderingMode.uiView.rawValue) + } + + func test_storesPrecision() throws { + let view = makeBaseText().emergeSnapshotPrecision(0.95) + let state = state(for: view) + let precision = try XCTUnwrap(state.precision) + XCTAssertEqual(precision, 0.95, accuracy: .ulpOfOne) + } + + func test_storesExpansionPreference() { + let view = makeBaseText().emergeExpansion(false) + let state = state(for: view) + XCTAssertEqual(state.expansionPreference, false) + } + + func test_storesAccessibilityFlag() { + let view = makeBaseText().emergeAccessibility(true) + let state = state(for: view) + XCTAssertEqual(state.accessibilityEnabled, true) + } + + func test_storesAppStoreSnapshotFlag() { + let view = makeBaseText().emergeAppStoreSnapshot(true) + let state = state(for: view) + XCTAssertEqual(state.appStoreSnapshot, true) + } + + private func makeBaseText() -> Text { Text("Hello") } + + private func state( + for view: some View, + file: StaticString = #file, + line: UInt = #line + ) -> EmergeModifierState { + let wrapped = EmergeModifierView(wrapped: view) + let hosting = UIHostingController(rootView: wrapped) + + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = hosting + window.makeKeyAndVisible() + + // Give SwiftUI one tick to propagate preferences + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.01)) + + return EmergeModifierState.shared + } +} diff --git a/Tests/SnapshotPreviewsTests/ScrollExpansionTests.swift b/Tests/SnapshotPreviewsTests/ScrollExpansionTests.swift new file mode 100644 index 00000000..68167a6c --- /dev/null +++ b/Tests/SnapshotPreviewsTests/ScrollExpansionTests.swift @@ -0,0 +1,111 @@ +// +// ScrollExpansionTests.swift +// SnapshotPreviews +// +// Created by Trevor Elkins on 5/1/25. +// + +import UIKit +import XCTest + +@testable import SnapshotPreviewsCore + +final class ScrollExpansionUIKitTests: XCTestCase { + func test_expandsWhenNeeded() { + let vc = makeVC(visible: 300, content: 500) + + vc.updateHeight {} + + XCTAssertEqual( + vc.heightAnchor?.constant, + 500, + "Height should increase by diff between content & visible" + ) + XCTAssertEqual( + vc.previousHeight, + 300, + "previousHeight must capture last visible height" + ) + } + + func test_doesNotExpandWhenNotSupported() { + let vc = makeVC(visible: 300, content: 500, supportsExpansion: false) + + vc.updateHeight {} + + XCTAssertEqual( + vc.heightAnchor?.constant, + 300, + "Constraint must stay unchanged when expansion is off" + ) + XCTAssertNil( + vc.previousHeight, + "previousHeight should remain nil when no expansion happens" + ) + } + + func test_doesNotExpandAgainIfAlreadyExpanded() { + let vc = makeVC(visible: 300, content: 500) + + vc.updateHeight {} + vc.updateHeight {} // second call — should be a no-op + + XCTAssertEqual( + vc.heightAnchor?.constant, + 500, + "Second invocation must not change the constant" + ) + } + + private func makeVC( + visible: CGFloat, + content: CGFloat, + supportsExpansion: Bool = true + ) -> ExpandableVC { + let vc = ExpandableVC( + visible: visible, + content: content, + initialConstant: visible + ) + vc.supportsExpansion = supportsExpansion + return vc + } +} + +private final class ExpandableVC: UIViewController, ScrollExpansionProviding { + + var previousHeight: CGFloat? + var supportsExpansion: Bool = true + + private(set) var heightConstraint: NSLayoutConstraint? + var heightAnchor: NSLayoutConstraint? { heightConstraint } + + init( + visible: CGFloat, + content: CGFloat, + initialConstant: CGFloat? = 100 + ) { + super.init(nibName: nil, bundle: nil) + + let scroll = UIScrollView() + scroll.contentSize = CGSize(width: 320, height: content) + scroll.frame = CGRect(x: 0, y: 0, width: 320, height: visible) + scroll.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scroll) + NSLayoutConstraint.activate([ + scroll.topAnchor.constraint(equalTo: view.topAnchor), + scroll.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scroll.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scroll.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + + if let constant = initialConstant { + heightConstraint = view.heightAnchor.constraint(equalToConstant: constant) + heightConstraint?.isActive = true + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Tests/SnapshotPreviewsTests/TestView.swift b/Tests/SnapshotPreviewsTests/TestView.swift new file mode 100644 index 00000000..461d04f0 --- /dev/null +++ b/Tests/SnapshotPreviewsTests/TestView.swift @@ -0,0 +1,18 @@ +// +// TestView.swift +// SnapshotPreviews +// +// Created by Trevor Elkins on 4/30/25. +// + +import SwiftUI + +struct TestView: View { + var body: some View { + Text("Hello world!") + } +} + +#Preview { + TestView() +}