mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-12 07:43:01 -06:00
284 lines
9.1 KiB
Swift
284 lines
9.1 KiB
Swift
import BitwardenKit
|
|
import UIKit
|
|
import XCTest
|
|
|
|
// MARK: - MockUINavigationController
|
|
|
|
public class MockUINavigationController: UINavigationController {
|
|
// MARK: Static properties
|
|
|
|
/// A size for the `mockWindow` and `mockView` objects to have.
|
|
/// This happens to be the size of the iPhone 5, 5C, 5S, and SE.
|
|
private static var mockWindowSize = CGRect(x: 0, y: 0, width: 320, height: 568)
|
|
|
|
// MARK: Presentation Tracking
|
|
|
|
/// Indicates whether the `present` method has been called.
|
|
public var presentCalled = false
|
|
|
|
/// The view controller that was presented, if any.
|
|
public var presentedView: UIViewController?
|
|
|
|
/// Indicates whether the presentation was animated.
|
|
public var presentAnimated = false
|
|
|
|
/// The completion handler passed to the `present` method.
|
|
public var presentCompletion: (() -> Void)?
|
|
|
|
/// Returns the currently presented view controller.
|
|
override public var presentedViewController: UIViewController? {
|
|
presentedView
|
|
}
|
|
|
|
// MARK: Dismissal Tracking
|
|
|
|
/// Indicates whether the `dismiss` method has been called.
|
|
public var dismissCalled = false
|
|
|
|
/// Indicates whether the dismissal was animated.
|
|
public var dismissAnimated = false
|
|
|
|
/// The completion handler passed to the `dismiss` method.
|
|
public var dismissCompletion: (() -> Void)?
|
|
|
|
// MARK: Push/Pop Tracking
|
|
|
|
/// Indicates whether the `pushViewController` method has been called.
|
|
public var pushViewControllerCalled = false
|
|
|
|
/// The view controller that was pushed, if any.
|
|
public var pushedViewController: UIViewController?
|
|
|
|
/// Indicates whether the push operation was animated.
|
|
public var pushAnimated = false
|
|
|
|
/// Indicates whether the `popViewController` method has been called.
|
|
public var popViewControllerCalled = false
|
|
|
|
/// The view controller that was popped, if any.
|
|
var poppedViewController: UIViewController?
|
|
|
|
/// Indicates whether the pop operation was animated.
|
|
var popAnimated = false
|
|
|
|
// MARK: Navigation Controller Support
|
|
|
|
/// Internal storage for a navigation controller.
|
|
private var _navigationController: UINavigationController?
|
|
|
|
/// Returns the internally stored navigation controller, bypassing the superclass one.
|
|
override public var navigationController: UINavigationController? {
|
|
get { _navigationController }
|
|
set { _navigationController = newValue }
|
|
}
|
|
|
|
// MARK: Mock Window and View Hierarchy
|
|
|
|
/// The mock window used for testing view hierarchy.
|
|
private var mockWindow: UIWindow?
|
|
|
|
/// The mock view used as the main view.
|
|
private var mockView: UIView?
|
|
|
|
/// Returns the mock view or the default view if no mock view is set.
|
|
override public var view: UIView! {
|
|
get {
|
|
mockView ?? super.view
|
|
}
|
|
set {
|
|
mockView = newValue
|
|
super.view = newValue
|
|
}
|
|
}
|
|
|
|
/// Returns whether the mock view or default view is loaded.
|
|
override public var isViewLoaded: Bool {
|
|
mockView != nil || super.isViewLoaded
|
|
}
|
|
|
|
// MARK: Initialization
|
|
|
|
/// Initializes the mock navigation controller with the specified nib name and bundle.
|
|
///
|
|
/// - Parameters:
|
|
/// - nibNameOrNil: The name of the nib file to load, or nil if no nib should be loaded.
|
|
/// - nibBundleOrNil: The bundle containing the nib file, or nil for the main bundle.
|
|
override public init(
|
|
nibName nibNameOrNil: String?,
|
|
bundle nibBundleOrNil: Bundle?,
|
|
) {
|
|
super.init(
|
|
nibName: nibNameOrNil,
|
|
bundle: nibBundleOrNil,
|
|
)
|
|
setUpMockHierarchy()
|
|
}
|
|
|
|
/// Initializes the mock navigation controller with a nil nib name and bundle.
|
|
public init() {
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
/// Initializes the mock navigation controller with a root view controller.
|
|
///
|
|
/// - Parameters:
|
|
/// - rootViewController: The view controller to use as the root of the navigation stack.
|
|
override public init(rootViewController: UIViewController) {
|
|
super.init(nibName: nil, bundle: nil)
|
|
// Set viewControllers array directly to avoid hierarchy issues
|
|
viewControllers = [rootViewController]
|
|
}
|
|
|
|
/// Initializes the mock view controller from a coder.
|
|
///
|
|
/// - Parameters:
|
|
/// - coder: The coder to initialize from.
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
setUpMockHierarchy()
|
|
}
|
|
|
|
// MARK: View Life Cycle Methods
|
|
|
|
/// Called after the view controller's view is loaded into memory.
|
|
/// Ensures that a mock view exists even if `loadView` wasn't called.
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
// Ensure we have a view even if loadView wasn't called
|
|
if view == nil {
|
|
view = UIView(frame: MockUINavigationController.mockWindowSize)
|
|
}
|
|
}
|
|
|
|
/// Creates the view controller's view programmatically.
|
|
/// Sets up a mock view with the predefined mock window size.
|
|
override public func loadView() {
|
|
if mockView == nil {
|
|
mockView = UIView(frame: MockUINavigationController.mockWindowSize)
|
|
}
|
|
view = mockView
|
|
}
|
|
|
|
// MARK: UIViewController Overrides
|
|
|
|
/// Presents a view controller modally and tracks the presentation details for testing.
|
|
///
|
|
/// - Parameters:
|
|
/// - viewControllerToPresent: The view controller to present.
|
|
/// - animated: Whether to animate the presentation.
|
|
/// - completion: A completion handler to call after the presentation finishes.
|
|
override public func present(
|
|
_ viewControllerToPresent: UIViewController,
|
|
animated: Bool,
|
|
completion: (() -> Void)? = nil,
|
|
) {
|
|
presentCalled = true
|
|
presentedView = viewControllerToPresent
|
|
presentAnimated = animated
|
|
presentCompletion = completion
|
|
|
|
// Set up the presented view controller's hierarchy
|
|
viewControllerToPresent.beginAppearanceTransition(true, animated: animated)
|
|
viewControllerToPresent.endAppearanceTransition()
|
|
|
|
// Call completion if provided
|
|
completion?()
|
|
}
|
|
|
|
/// Dismisses the currently presented view controller and tracks the dismissal details for testing.
|
|
///
|
|
/// - Parameters:
|
|
/// - animated: Whether to animate the dismissal.
|
|
/// - completion: A completion handler to call after the dismissal finishes.
|
|
override public func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
|
|
dismissCalled = true
|
|
dismissAnimated = animated
|
|
dismissCompletion = completion
|
|
|
|
if let presentedView {
|
|
presentedView.beginAppearanceTransition(false, animated: animated)
|
|
presentedView.endAppearanceTransition()
|
|
}
|
|
|
|
// Clear the presented view controller
|
|
presentedView = nil
|
|
|
|
completion?()
|
|
}
|
|
|
|
override public func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
|
pushViewControllerCalled = true
|
|
pushedViewController = viewController
|
|
pushAnimated = animated
|
|
|
|
// Add to view controllers array
|
|
var controllers = viewControllers
|
|
controllers.append(viewController)
|
|
viewControllers = controllers
|
|
|
|
// Simulate appearance transitions safely
|
|
DispatchQueue.main.async {
|
|
viewController.beginAppearanceTransition(true, animated: animated)
|
|
viewController.endAppearanceTransition()
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
override public func popViewController(animated: Bool) -> UIViewController? {
|
|
popViewControllerCalled = true
|
|
popAnimated = animated
|
|
|
|
guard viewControllers.count > 1 else { return nil }
|
|
|
|
var controllers = viewControllers
|
|
let poppedVC = controllers.removeLast()
|
|
poppedViewController = poppedVC
|
|
viewControllers = controllers
|
|
|
|
// Simulate appearance transitions safely
|
|
DispatchQueue.main.async {
|
|
poppedVC.beginAppearanceTransition(false, animated: animated)
|
|
poppedVC.endAppearanceTransition()
|
|
}
|
|
|
|
return poppedVC
|
|
}
|
|
|
|
// MARK: Helper Methods
|
|
|
|
/// Resets and clears all local variables, to prepare the mock for reuse.
|
|
public func reset() {
|
|
presentCalled = false
|
|
presentedView = nil
|
|
presentAnimated = false
|
|
presentCompletion = nil
|
|
|
|
dismissCalled = false
|
|
dismissAnimated = false
|
|
dismissCompletion = nil
|
|
|
|
pushViewControllerCalled = false
|
|
pushedViewController = nil
|
|
pushAnimated = false
|
|
|
|
popViewControllerCalled = false
|
|
poppedViewController = nil
|
|
popAnimated = false
|
|
|
|
_navigationController = nil
|
|
}
|
|
|
|
// MARK: Mock Hierarchy
|
|
|
|
/// Sets up a `UIWindow` and `UIView` to use as mocks in the view hierarchy.
|
|
private func setUpMockHierarchy() {
|
|
// Create a mock window to avoid issues with view hierarchy
|
|
mockWindow = UIWindow(frame: MockUINavigationController.mockWindowSize)
|
|
mockWindow?.rootViewController = self
|
|
|
|
// Create a mock view
|
|
mockView = UIView(frame: mockWindow?.frame ?? .zero)
|
|
view = mockView
|
|
}
|
|
}
|