From 491ffa8ef9605c90b81567794f77118c8f6d665f Mon Sep 17 00:00:00 2001 From: Aleh Dzenisiuk Date: Wed, 10 Apr 2024 17:21:17 +0200 Subject: [PATCH] Add basic support for SwiftUI views --- MMMTestCase.podspec | 7 +-- Sources/MMMTestCase/MMMTestCase.swift | 69 ++++++++++++++++++++++++++- Sources/MMMTestCaseObjC/MMMTestCase.h | 18 +++++++ Sources/MMMTestCaseObjC/MMMTestCase.m | 17 +------ 4 files changed, 92 insertions(+), 19 deletions(-) diff --git a/MMMTestCase.podspec b/MMMTestCase.podspec index 53197d8..b79a8fa 100644 --- a/MMMTestCase.podspec +++ b/MMMTestCase.podspec @@ -1,12 +1,12 @@ # # MMMTestCase. Part of MMMTemple. -# Copyright (C) 2015-2022 MediaMonks. All rights reserved. +# Copyright (C) 2015-2024 MediaMonks. All rights reserved. # Pod::Spec.new do |s| s.name = "MMMTestCase" - s.version = "1.9.0" + s.version = "1.10.0" s.summary = "Our helpers for FBTestCase and XCTestCase" s.description = s.summary s.homepage = "https://github.com/mediamonks/#{s.name}" @@ -14,7 +14,8 @@ Pod::Spec.new do |s| s.authors = "MediaMonks" s.source = { :git => "https://github.com/mediamonks/#{s.name}.git", :tag => s.version.to_s } - s.platform = :ios, '11.0' + s.platform = :ios, '15.0' + s.swift_versions = '5.0' s.framework = 'XCTest' s.dependency 'FBSnapshotTestCase/Core' diff --git a/Sources/MMMTestCase/MMMTestCase.swift b/Sources/MMMTestCase/MMMTestCase.swift index b875e43..46f49ba 100644 --- a/Sources/MMMTestCase/MMMTestCase.swift +++ b/Sources/MMMTestCase/MMMTestCase.swift @@ -5,6 +5,7 @@ import Foundation import MMMCommonCore +import SwiftUI import UIKit #if SWIFT_PACKAGE @@ -45,7 +46,7 @@ public enum MMMTestCaseSize { return NSValue(cgSize: CGSize(width: width, height: height)) } } -}; +} extension MMMTestCase { @@ -86,7 +87,73 @@ extension MMMTestCase { identifier: identifier, backgroundColor: backgroundColor ) + } + + private class WrapperController: UIViewController { + private let viewController: UIViewController + + public init(_ viewController: UIViewController) { + self.viewController = viewController + super.init(nibName: nil, bundle: nil) + addChild(viewController) + viewController.didMove(toParent: self) + } + + public required init?(coder: NSCoder) { fatalError() } + + public var fitSize: CGSize = .init(width: 320, height: 480) + + public private(set) lazy var container = MMMTestCaseContainer() + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(container) + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + container.setChildView(viewController.view, size: fitSize) + let bounds = view.bounds.inset(by: view.safeAreaInsets) + container.frame = .init(origin: bounds.origin, size: container.sizeThatFits(.zero)) + } + } + + private func sizeForFit(_ fit: MMMTestCaseSize) -> CGSize { + switch fit { + case .natural: return self.fitSize(forPresetFit: .natural) + case .screenWidth: return self.fitSize(forPresetFit: .screenWidth) + case .screenWidthTableHeight: return self.fitSize(forPresetFit: .screenWidthTableHeight) + case let .size(width, height): return .init(width: width, height: height) + } + } + + public func verify(view: T, fit: MMMTestCaseSize = .screenWidthTableHeight, identifier: String = "", backgroundColor: UIColor? = nil) { + + let window = UIWindow() + let viewController = UIHostingController(rootView: view) + let wrapper = WrapperController(viewController) + + window.rootViewController = wrapper + window.windowLevel = .normal - 1 // It could be fun to watch snapshots, but let's keep older behavior. + window.isHidden = false + + let fitSize = sizeForFit(fit) + wrapper.fitSize = viewController.sizeThatFits(in: fitSize) + window.setNeedsLayout() + // We need the layout to happen naturally now. + pumpRunLoopABit() + + self.verifyView( + wrapper.container, + identifier: [ + identifier, + fitSize.width > 0 ? String(format: "w%.f", fitSize.width) : nil, + fitSize.height > 0 ? String(format: "h%.f", fitSize.height) : nil + ].compactMap { $0 }.joined(separator: "_"), + suffixes: self.referenceFolderSuffixes(), + tolerance: 0.05 + ) } /// Helps generating parameter dictionaries suitable for `varyParameters` from enums supporting `CaseIterable`. diff --git a/Sources/MMMTestCaseObjC/MMMTestCase.h b/Sources/MMMTestCaseObjC/MMMTestCase.h index 0d210e7..76307bd 100644 --- a/Sources/MMMTestCaseObjC/MMMTestCase.h +++ b/Sources/MMMTestCaseObjC/MMMTestCase.h @@ -145,6 +145,9 @@ typedef void (^RandomOrderBlock)(); */ - (void)flushMainQueue; +/// Pumps all events ready right now unless there are too many that we don't have time left. +- (void)pumpRunLoopABit; + @end /** @@ -180,4 +183,19 @@ NS_SWIFT_NAME(MMMTestCase.TableViewCellWrapper) @end +/** + */ +@interface MMMTestCaseContainer : UIView + +@property (nonatomic, readwrite) CGRect testViewAlignmentRect; + +- (id)init NS_DESIGNATED_INITIALIZER; + +- (id)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; +- (id)initWithFrame:(CGRect)frame NS_UNAVAILABLE; + +- (void)setChildView:(UIView *)childView size:(CGSize)size; + +@end + NS_ASSUME_NONNULL_END diff --git a/Sources/MMMTestCaseObjC/MMMTestCase.m b/Sources/MMMTestCaseObjC/MMMTestCase.m index 2d4041f..3f5eb82 100644 --- a/Sources/MMMTestCaseObjC/MMMTestCase.m +++ b/Sources/MMMTestCaseObjC/MMMTestCase.m @@ -118,19 +118,6 @@ - (NSString *)identifierForValueWithIndex:(NSInteger)index { @end -/** - */ -@interface MMMTestCaseContainer : UIView - -@property (nonatomic, readwrite) CGRect testViewAlignmentRect; - -- (id)init NS_DESIGNATED_INITIALIZER; - -- (id)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; -- (id)initWithFrame:(CGRect)frame NS_UNAVAILABLE; - -@end - @implementation MMMTestCaseContainer { UIView *_autoLayoutContainer; UIView *_childView; @@ -266,7 +253,7 @@ - (void)drawRect:(CGRect)rect { CGFloat halfLineWidth = .5; // - // Safety area backtround. + // Safety area background. // [[self safetyAreaColor] setFill]; CGContextFillRect(c, b); @@ -278,7 +265,7 @@ - (void)drawRect:(CGRect)rect { CGContextFillRect(c, [self safetyBounds]); // - // Guidlines corresponding to the alignment rectangle. + // Guidelines corresponding to the alignment rectangle. // { CGRect r = [_childView alignmentRectForFrame:[self safetyBounds]];