From cd937bc2a36a6a34580cfdc365eb9630af3db948 Mon Sep 17 00:00:00 2001 From: Katherine Bertelsen Date: Tue, 4 Nov 2025 08:23:55 -0600 Subject: [PATCH] [PM-26060] Consolidate StackNavigator to BitwardenKit (#2095) --- AuthenticatorShared/UI/Auth/AuthModule.swift | 2 + .../UI/DebugMenu/DebugMenuModule.swift | 1 + .../Utilities/RootViewControllerTests.swift | 1 + .../Utilities/StackNavigator.swift | 294 ------------------ .../Utilities/StackNavigatorTests.swift | 63 ---- .../FileSelection/FileSelectionModule.swift | 2 + .../Settings/SettingsCoordinatorTests.swift | 1 + .../UI/Platform/Settings/SettingsModule.swift | 2 + .../UI/Platform/Tutorial/TutorialModule.swift | 2 + .../AuthenticatorItemModule.swift | 1 + .../UI/Vault/ItemList/ItemListModule.swift | 1 + .../Utilities/Mocks}/MockStackNavigator.swift | 63 ++-- .../Platform}/Utilities/StackNavigator.swift | 84 ++++- .../Utilities/StackNavigatorTests.swift | 15 +- .../Mocks/MockUINavigationController.swift | 283 +++++++++++++++++ .../Mocks/MockUIViewController.swift | 22 +- BitwardenShared/UI/Auth/AuthModule.swift | 1 + .../ProfileSwitcherModule.swift | 1 + .../Utilities/RootViewControllerTests.swift | 1 + .../Utilities/StackNavigatorHostedTests.swift | 38 +++ .../Platform/DebugMenu/DebugMenuModule.swift | 1 + .../ExtensionSetupCoordinatorTests.swift | 1 + .../ExtensionSetup/ExtensionSetupModule.swift | 1 + .../FileSelection/FileSelectionModule.swift | 2 + .../LoginRequestCoordinatorTests.swift | 1 + .../LoginRequest/LoginRequestModule.swift | 1 + .../PasswordAutoFillCoordinatorTests.swift | 1 + .../PasswordAutoFillModule.swift | 2 + .../AddEditFolderCoordinatorTests.swift | 1 + .../AddEditFolder/AddEditFolderModule.swift | 2 + .../UI/Platform/Settings/SettingsModule.swift | 2 + .../UI/Tools/ExportCXF/ExportCXFModule.swift | 2 + .../Generator/GeneratorCoordinatorTests.swift | 1 + .../UI/Tools/Generator/GeneratorModule.swift | 2 + .../UI/Tools/ImportCXF/ImportCXFModule.swift | 2 + .../PasswordHistoryCoordinatorTests.swift | 1 + .../PasswordHistoryModule.swift | 2 + .../UI/Tools/Send/Send/SendModule.swift | 2 + .../Tools/Send/SendItem/SendItemModule.swift | 2 + .../ImportLoginsCoordinatorTests.swift | 1 + .../ImportLogins/ImportLoginsModule.swift | 2 + .../UI/Vault/Vault/VaultModule.swift | 1 + .../UI/Vault/VaultItem/VaultItemModule.swift | 1 + GlobalTestHelpers-bwa/MockAppModule.swift | 2 +- .../MockStackNavigator.swift | 112 ------- GlobalTestHelpers/MockAppModule.swift | 2 +- 46 files changed, 499 insertions(+), 529 deletions(-) delete mode 100644 AuthenticatorShared/UI/Platform/Application/Utilities/StackNavigator.swift delete mode 100644 AuthenticatorShared/UI/Platform/Application/Utilities/StackNavigatorTests.swift rename {GlobalTestHelpers => BitwardenKit/Core/Platform/Utilities/Mocks}/MockStackNavigator.swift (59%) rename {BitwardenShared/UI/Platform/Application => BitwardenKit/Core/Platform}/Utilities/StackNavigator.swift (72%) rename {BitwardenShared/UI/Platform/Application => BitwardenKit/Core/Platform}/Utilities/StackNavigatorTests.swift (90%) create mode 100644 BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockUINavigationController.swift create mode 100644 BitwardenShared/UI/Platform/Application/Utilities/StackNavigatorHostedTests.swift delete mode 100644 GlobalTestHelpers-bwa/MockStackNavigator.swift diff --git a/AuthenticatorShared/UI/Auth/AuthModule.swift b/AuthenticatorShared/UI/Auth/AuthModule.swift index b15a3da90..585035429 100644 --- a/AuthenticatorShared/UI/Auth/AuthModule.swift +++ b/AuthenticatorShared/UI/Auth/AuthModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - AuthModule /// An object that builds coordinators for the auth flow. diff --git a/AuthenticatorShared/UI/DebugMenu/DebugMenuModule.swift b/AuthenticatorShared/UI/DebugMenu/DebugMenuModule.swift index 29284435d..3b6e7623e 100644 --- a/AuthenticatorShared/UI/DebugMenu/DebugMenuModule.swift +++ b/AuthenticatorShared/UI/DebugMenu/DebugMenuModule.swift @@ -1,3 +1,4 @@ +import BitwardenKit import Foundation // MARK: - DebugMenuModule diff --git a/AuthenticatorShared/UI/Platform/Application/Utilities/RootViewControllerTests.swift b/AuthenticatorShared/UI/Platform/Application/Utilities/RootViewControllerTests.swift index efc55aff4..7c1c61596 100644 --- a/AuthenticatorShared/UI/Platform/Application/Utilities/RootViewControllerTests.swift +++ b/AuthenticatorShared/UI/Platform/Application/Utilities/RootViewControllerTests.swift @@ -1,3 +1,4 @@ +import BitwardenKitMocks import XCTest @testable import AuthenticatorShared diff --git a/AuthenticatorShared/UI/Platform/Application/Utilities/StackNavigator.swift b/AuthenticatorShared/UI/Platform/Application/Utilities/StackNavigator.swift deleted file mode 100644 index e28f5ec30..000000000 --- a/AuthenticatorShared/UI/Platform/Application/Utilities/StackNavigator.swift +++ /dev/null @@ -1,294 +0,0 @@ -import BitwardenKit -import SwiftUI - -// MARK: - StackNavigator - -/// An object used to navigate between views in a stack interface. -/// -@MainActor -public protocol StackNavigator: Navigator { - /// Whether the stack of views in the navigator is empty. - var isEmpty: Bool { get } - - /// Dismisses the view that was presented modally by the navigator. - /// - /// - Parameters animated: Whether the transition should be animated. - /// - func dismiss(animated: Bool) - - /// Dismisses the view that was presented modally by the navigator - /// and executes a block of code when dismissing completes. - /// - /// - Parameters: - /// - animated: Whether the transition should be animated. - /// - completion: The block that is executed when dismissing completes. - /// - func dismiss(animated: Bool, completion: (() -> Void)?) - - /// Pushes a view onto the navigator's stack. - /// - /// - Parameters: - /// - view: The view to push onto the stack. - /// - animated: Whether the transition should be animated. - /// - hidesBottomBar: Whether the bottom bar should be hidden when the view is pushed. - /// - func push(_ view: Content, animated: Bool, hidesBottomBar: Bool) - - /// Pushes a view controller onto the navigator's stack. - /// - /// - Parameters: - /// - viewController: The view controller to push onto the stack. - /// - animated: Whether the transition should be animated. - /// - func push(_ viewController: UIViewController, animated: Bool) - - /// Pops a view off the navigator's stack. - /// - /// - Parameter animated: Whether the transition should be animated. - /// - Returns: The `UIViewController` that was popped off the navigator's stack. - /// - @discardableResult - func pop(animated: Bool) -> UIViewController? - - /// Pops all the view controllers on the stack except the root view controller. - /// - /// - Parameter animated: Whether the transition should be animated. - /// - Returns: An array of `UIViewController`s that were popped of the navigator's stack. - /// - @discardableResult - func popToRoot(animated: Bool) -> [UIViewController] - - /// Presents a view modally. - /// - /// - Parameters: - /// - view: The view to present. - /// - animated: Whether the transition should be animated. - /// - overFullscreen: Whether or not the presented modal should cover the full screen. - /// - onCompletion: A closure to call on completion. - /// - func present( - _ view: Content, - animated: Bool, - overFullscreen: Bool, - onCompletion: (() -> Void)?, - ) - - /// Presents a view controller modally. Supports presenting on top of presented modals if necessary. - /// - /// - Parameters: - /// - viewController: The view controller to present. - /// - animated: Whether the transition should be animated. - /// - overFullscreen: Whether or not the presented modal should cover the full screen. - /// - onCompletion: A closure to call on completion. - /// - func present( - _ viewController: UIViewController, - animated: Bool, - overFullscreen: Bool, - onCompletion: (() -> Void)?, - ) - - /// Replaces the stack with the specified view. - /// - /// - Parameters: - /// - view: The view that will replace the stack. - /// - animated: Whether the transition should be animated. - /// - func replace(_ view: Content, animated: Bool) -} - -extension StackNavigator { - /// Dismisses the view that was presented modally by the navigator. Animation is controlled by - /// `UI.animated`. - /// - func dismiss() { - dismiss(animated: UI.animated) - } - - /// Dismisses the view that was presented modally by the navigator. Animation is controlled by - /// `UI.animated`. Executes a block of code when dismissing completes. - /// - func dismiss(completion: (() -> Void)?) { - dismiss(animated: UI.animated, completion: completion) - } - - /// Pushes a view onto the navigator's stack. - /// - /// - Parameters: - /// - view: The view to push onto the stack. - /// - animated: Whether the transition should be animated. Defaults to `UI.animated`. - /// - func push(_ view: Content, animated: Bool = UI.animated) { - push(view, animated: animated, hidesBottomBar: false) - } - - /// Pushes a view controller onto the navigator's stack. - /// - /// - Parameters: - /// - viewController: The view controller to push onto the stack. - /// - animated: Whether the transition should be animated. Defaults to `UI.animated`. - /// - navigationTitle: The navigation title to pre-populate the navigation bar so that it doesn't flash. - /// - searchController: If non-nil, pre-populate the navigation bar with a search bar backed by the - /// supplied UISearchController. - /// Normal SwiftUI search contorls will not work if this value is supplied. Tracking the searchController - /// behavior must be done through a UISearchControllerDelegate or a UISearchResultsUpdating object. - /// - func push( - _ viewController: UIViewController, - animated: Bool = UI.animated, - navigationTitle: String? = nil, - searchController: UISearchController? = nil, - ) { - if let navigationTitle { - // Preset some navigation item values so that the navigation bar does not flash oddly once - // the view's push animation has completed. This happens because `UIHostingController` does - // not resolve its `navigationItem` properties until the view has been displayed on screen. - // In this case, that doesn't happen until the push animation has completed, which results - // in both the title and the search bar flashing into view after the push animation - // completes. This occurs on all iOS versions (tested on iOS 17). - // - // The values set here are temporary, and are overwritten once the hosting controller has - // resolved its root view's navigation bar modifiers. - viewController.navigationItem.largeTitleDisplayMode = .never - viewController.navigationItem.title = navigationTitle - if let searchController { - if #available(iOS 16.0, *) { - viewController.navigationItem.preferredSearchBarPlacement = .stacked - } - viewController.navigationItem.searchController = searchController - viewController.navigationItem.hidesSearchBarWhenScrolling = false - } - } - - push(viewController, animated: animated) - } - - /// Pops a view off the navigator's stack. Animation is controlled by `UI.animated`. - /// - /// - Returns: The `UIViewController` that was popped off the navigator's stack. - /// - @discardableResult - func pop() -> UIViewController? { - pop(animated: UI.animated) - } - - /// Pops all the view controllers on the stack except the root view controller. Animation is controlled by - /// `UI.animated`. - /// - /// - Returns: An array of `UIViewController`s that were popped of the navigator's stack. - /// - @discardableResult - func popToRoot() -> [UIViewController] { - popToRoot(animated: UI.animated) - } - - /// Presents a view modally. - /// - /// - Parameters: - /// - view: The view to present. - /// - animated: Whether the transition should be animated. Defaults to `UI.animated`. - /// - overFullscreen: Whether or not the presented modal should cover the full screen. - /// - onCompletion: The closure to call after presenting. - /// - func present( - _ view: Content, - animated: Bool = UI.animated, - overFullscreen: Bool = false, - onCompletion _: (() -> Void)? = nil, - ) { - present( - view, - animated: animated, - overFullscreen: overFullscreen, - onCompletion: nil, - ) - } - - /// Presents a view controller modally. Supports presenting on top of presented modals if necessary. Animation is - /// controlled by `UI.animated`. - /// - /// - Parameters: - /// - viewController: The view controller to present. - /// - overFullscreen: Whether or not the presented modal should cover the full screen. - /// - onCompletion: The closure to call after presenting. - /// - func present( - _ viewController: UIViewController, - overFullscreen: Bool = false, - onCompletion: (() -> Void)? = nil, - ) { - present( - viewController, - animated: UI.animated, - overFullscreen: overFullscreen, - onCompletion: onCompletion, - ) - } - - /// Replaces the stack with the specified view. Animation is controlled by `UI.animated`. - /// - /// - Parameter view: The view that will replace the stack. - /// - func replace(_ view: Content) { - replace(view, animated: UI.animated) - } -} - -// MARK: - UINavigationController - -extension UINavigationController: StackNavigator { - public var isEmpty: Bool { - viewControllers.isEmpty - } - - public var rootViewController: UIViewController? { - self - } - - public func dismiss(animated: Bool) { - dismiss(animated: animated, completion: nil) - } - - @discardableResult - public func pop(animated: Bool) -> UIViewController? { - popViewController(animated: animated) - } - - @discardableResult - public func popToRoot(animated: Bool) -> [UIViewController] { - popToRootViewController(animated: animated) ?? [] - } - - public func push(_ view: Content, animated: Bool, hidesBottomBar: Bool) { - let viewController = UIHostingController(rootView: view) - viewController.hidesBottomBarWhenPushed = hidesBottomBar - let animated = self.view.window != nil ? animated : false - push(viewController, animated: animated) - } - - public func push(_ viewController: UIViewController, animated: Bool) { - let animated = view.window != nil ? animated : false - pushViewController(viewController, animated: animated) - } - - public func present( - _ view: Content, - animated: Bool, - overFullscreen: Bool, - onCompletion: (() -> Void)? = nil, - ) { - let controller = UIHostingController(rootView: view) - controller.isModalInPresentation = true - if overFullscreen { - controller.modalPresentationStyle = .overFullScreen - controller.view.backgroundColor = .clear - } - let animated = self.view.window != nil ? animated : false - present(controller, animated: animated, onCompletion: onCompletion) - } - - public func replace(_ view: Content, animated: Bool) { - let animated = self.view.window != nil ? animated : false - setViewControllers([UIHostingController(rootView: view)], animated: animated) - } -} diff --git a/AuthenticatorShared/UI/Platform/Application/Utilities/StackNavigatorTests.swift b/AuthenticatorShared/UI/Platform/Application/Utilities/StackNavigatorTests.swift deleted file mode 100644 index 6e340b4bb..000000000 --- a/AuthenticatorShared/UI/Platform/Application/Utilities/StackNavigatorTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -import SwiftUI -import XCTest - -@testable import AuthenticatorShared - -@MainActor -class StackNavigatorTests: BitwardenTestCase { - var subject: UINavigationController! - - override func setUp() { - super.setUp() - subject = UINavigationController() - setKeyWindowRoot(viewController: subject) - } - - /// `present(_:animated:)` presents the hosted view. - func testPresent() { - subject.present(EmptyView(), animated: false) - XCTAssertTrue(subject.presentedViewController is UIHostingController) - } - - /// `present(_:animated:)` presents the hosted view on existing presented views. - func testPresentOnPresentedView() { - subject.present(EmptyView(), animated: false) - subject.present(ScrollView {}, animated: false) - XCTAssertTrue(subject.presentedViewController is UIHostingController) - waitFor(subject.presentedViewController?.presentedViewController != nil) - XCTAssertTrue( - subject.presentedViewController?.presentedViewController - is UIHostingController>, - ) - } - - /// `dismiss(animated:)` dismisses the hosted view. - func testDismiss() { - subject.present(EmptyView(), animated: false) - subject.dismiss(animated: false) - waitFor(subject.presentedViewController == nil) - } - - /// `push(_:animated:)` pushes the hosted view. - func testPush() { - subject.push(EmptyView(), animated: false) - XCTAssertTrue(subject.topViewController is UIHostingController) - } - - /// `pop(animated:)` pops the hosted view. - func testPop() { - subject.push(EmptyView(), animated: false) - subject.push(EmptyView(), animated: false) - subject.pop(animated: false) - XCTAssertEqual(subject.viewControllers.count, 1) - XCTAssertTrue(subject.topViewController is UIHostingController) - } - - /// `replace(_:animated:)` replaces the hosted view. - func testReplace() { - subject.push(EmptyView(), animated: false) - subject.replace(Text("replaced"), animated: false) - XCTAssertEqual(subject.viewControllers.count, 1) - XCTAssertTrue(subject.topViewController is UIHostingController) - } -} diff --git a/AuthenticatorShared/UI/Platform/FileSelection/FileSelectionModule.swift b/AuthenticatorShared/UI/Platform/FileSelection/FileSelectionModule.swift index dbd69672e..653a4e36c 100644 --- a/AuthenticatorShared/UI/Platform/FileSelection/FileSelectionModule.swift +++ b/AuthenticatorShared/UI/Platform/FileSelection/FileSelectionModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - FileSelectionModule /// An object that builds coordinators for the file selection flow. diff --git a/AuthenticatorShared/UI/Platform/Settings/SettingsCoordinatorTests.swift b/AuthenticatorShared/UI/Platform/Settings/SettingsCoordinatorTests.swift index 9b84a5eff..dad10ba8b 100644 --- a/AuthenticatorShared/UI/Platform/Settings/SettingsCoordinatorTests.swift +++ b/AuthenticatorShared/UI/Platform/Settings/SettingsCoordinatorTests.swift @@ -1,4 +1,5 @@ import BitwardenKit +import BitwardenKitMocks import BitwardenResources import SwiftUI import XCTest diff --git a/AuthenticatorShared/UI/Platform/Settings/SettingsModule.swift b/AuthenticatorShared/UI/Platform/Settings/SettingsModule.swift index 2504d787a..0a55886ec 100644 --- a/AuthenticatorShared/UI/Platform/Settings/SettingsModule.swift +++ b/AuthenticatorShared/UI/Platform/Settings/SettingsModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - SettingsModule /// An object that builds coordinators for the settings tab. diff --git a/AuthenticatorShared/UI/Platform/Tutorial/TutorialModule.swift b/AuthenticatorShared/UI/Platform/Tutorial/TutorialModule.swift index c18160612..7639ed731 100644 --- a/AuthenticatorShared/UI/Platform/Tutorial/TutorialModule.swift +++ b/AuthenticatorShared/UI/Platform/Tutorial/TutorialModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - TutorialModule /// An object that builds tutorial coordinators diff --git a/AuthenticatorShared/UI/Vault/AuthenticatorItem/AuthenticatorItemModule.swift b/AuthenticatorShared/UI/Vault/AuthenticatorItem/AuthenticatorItemModule.swift index c353015c8..8e565ad4b 100644 --- a/AuthenticatorShared/UI/Vault/AuthenticatorItem/AuthenticatorItemModule.swift +++ b/AuthenticatorShared/UI/Vault/AuthenticatorItem/AuthenticatorItemModule.swift @@ -1,3 +1,4 @@ +import BitwardenKit import Foundation // MARK: - AuthenticatorItemModule diff --git a/AuthenticatorShared/UI/Vault/ItemList/ItemListModule.swift b/AuthenticatorShared/UI/Vault/ItemList/ItemListModule.swift index ac70d02a6..b2adb5b55 100644 --- a/AuthenticatorShared/UI/Vault/ItemList/ItemListModule.swift +++ b/AuthenticatorShared/UI/Vault/ItemList/ItemListModule.swift @@ -1,3 +1,4 @@ +import BitwardenKit import Foundation // MARK: - ItemListModule diff --git a/GlobalTestHelpers/MockStackNavigator.swift b/BitwardenKit/Core/Platform/Utilities/Mocks/MockStackNavigator.swift similarity index 59% rename from GlobalTestHelpers/MockStackNavigator.swift rename to BitwardenKit/Core/Platform/Utilities/Mocks/MockStackNavigator.swift index 79436ee5f..5585ee8a4 100644 --- a/GlobalTestHelpers/MockStackNavigator.swift +++ b/BitwardenKit/Core/Platform/Utilities/Mocks/MockStackNavigator.swift @@ -1,19 +1,18 @@ import BitwardenKit -import BitwardenShared import SwiftUI -final class MockStackNavigator: StackNavigator { - struct NavigationAction { - var type: NavigationType - var view: Any? - var animated: Bool - var embedInNavigationController: Bool? - var hidesBottomBar: Bool? - var isModalInPresentation: Bool? - var overFullscreen: Bool? +public final class MockStackNavigator: StackNavigator { + public struct NavigationAction { + public var type: NavigationType + public var view: Any? + public var animated: Bool + public var embedInNavigationController: Bool? + public var hidesBottomBar: Bool? + public var isModalInPresentation: Bool? + public var overFullscreen: Bool? } - enum NavigationType { + public enum NavigationType { case dismissed case dismissedWithCompletionHandler case pushed @@ -24,26 +23,28 @@ final class MockStackNavigator: StackNavigator { case replaced } - var actions: [NavigationAction] = [] - var alertOnDismissed: (() -> Void)? - var alerts: [BitwardenKit.Alert] = [] - var isEmpty = true - var isNavigationBarHidden = false - var isPresenting = false - var rootViewController: UIViewController? + public var actions: [NavigationAction] = [] + public var alertOnDismissed: (() -> Void)? + public var alerts: [BitwardenKit.Alert] = [] + public var isEmpty = true + public var isNavigationBarHidden = false + public var isPresenting = false + public var rootViewController: UIViewController? - var viewControllersToPop: [UIViewController] = [] + public var viewControllersToPop: [UIViewController] = [] - func dismiss(animated: Bool) { + public init() {} + + public func dismiss(animated: Bool) { actions.append(NavigationAction(type: .dismissed, animated: animated)) } - func dismiss(animated: Bool, completion: (() -> Void)?) { + public func dismiss(animated: Bool, completion: (() -> Void)?) { completion?() actions.append(NavigationAction(type: .dismissedWithCompletionHandler, animated: animated)) } - func push(_ view: Content, animated: Bool, hidesBottomBar: Bool) { + public func push(_ view: Content, animated: Bool, hidesBottomBar: Bool) { actions.append(NavigationAction( type: .pushed, view: view, @@ -52,7 +53,7 @@ final class MockStackNavigator: StackNavigator { )) } - func push(_ viewController: UIViewController, animated: Bool) { + public func push(_ viewController: UIViewController, animated: Bool) { actions.append(NavigationAction( type: .pushed, view: viewController, @@ -61,27 +62,27 @@ final class MockStackNavigator: StackNavigator { } @discardableResult - func pop(animated: Bool) -> UIViewController? { + public func pop(animated: Bool) -> UIViewController? { actions.append(NavigationAction(type: .popped, animated: animated)) return viewControllersToPop.last } @discardableResult - func popToRoot(animated: Bool) -> [UIViewController] { + public func popToRoot(animated: Bool) -> [UIViewController] { actions.append(NavigationAction(type: .poppedToRoot, animated: animated)) return viewControllersToPop } - func present(_ alert: BitwardenKit.Alert) { + public func present(_ alert: BitwardenKit.Alert) { alerts.append(alert) } - func present(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) { + public func present(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) { alerts.append(alert) alertOnDismissed = onDismissed } - func present( // swiftlint:disable:this function_parameter_count + public func present( // swiftlint:disable:this function_parameter_count _ view: Content, animated: Bool, embedInNavigationController: Bool, @@ -102,7 +103,7 @@ final class MockStackNavigator: StackNavigator { ) } - func present( + public func present( _ viewController: UIViewController, animated: Bool, overFullscreen: Bool, @@ -119,11 +120,11 @@ final class MockStackNavigator: StackNavigator { ) } - func setNavigationBarHidden(_ hidden: Bool, animated: Bool) { + public func setNavigationBarHidden(_ hidden: Bool, animated: Bool) { isNavigationBarHidden = hidden } - func replace(_ view: Content, animated: Bool) { + public func replace(_ view: Content, animated: Bool) { actions.append(NavigationAction(type: .replaced, view: view, animated: animated)) } } diff --git a/BitwardenShared/UI/Platform/Application/Utilities/StackNavigator.swift b/BitwardenKit/Core/Platform/Utilities/StackNavigator.swift similarity index 72% rename from BitwardenShared/UI/Platform/Application/Utilities/StackNavigator.swift rename to BitwardenKit/Core/Platform/Utilities/StackNavigator.swift index a70fb01b6..e4750e7d0 100644 --- a/BitwardenShared/UI/Platform/Application/Utilities/StackNavigator.swift +++ b/BitwardenKit/Core/Platform/Utilities/StackNavigator.swift @@ -1,4 +1,3 @@ -import BitwardenKit import SwiftUI // MARK: - StackNavigator @@ -223,28 +222,74 @@ public extension StackNavigator { // MARK: - UINavigationController extension UINavigationController: StackNavigator { + /// Returns whether the navigation controller's stack is empty. + /// + /// - Returns: `true` if there are no view controllers in the stack, `false` otherwise. public var isEmpty: Bool { viewControllers.isEmpty } + /// Returns the root view controller of the navigation stack. + /// + /// For UINavigationController, this returns the navigation controller itself + /// as it serves as the root container for the navigation stack. + /// + /// - Returns: The navigation controller instance. public var rootViewController: UIViewController? { self } + /// Dismisses the modally presented view controller without a completion handler. + /// + /// This is a convenience method that calls the system's dismiss method + /// with a nil completion handler. + /// + /// - Parameters: + /// - animated: Whether the dismissal should be animated. public func dismiss(animated: Bool) { dismiss(animated: animated, completion: nil) } + /// Pops the top view controller from the navigation stack. + /// + /// Removes and returns the top view controller from the navigation stack. + /// If the stack only contains the root view controller, this method does nothing + /// and returns nil. + /// + /// - Parameters: + /// - animated: Whether the pop transition should be animated. + /// - Returns: The view controller that was popped, or nil if no controller was popped. @discardableResult public func pop(animated: Bool) -> UIViewController? { popViewController(animated: animated) } + /// Pops all view controllers except the root view controller. + /// + /// Removes all view controllers from the stack except the root view controller + /// and returns an array of the popped controllers. + /// + /// - Parameters: + /// - animated: Whether the pop transition should be animated. + /// - Returns: An array of view controllers that were popped from the stack. + /// Returns an empty array if no controllers were popped. @discardableResult public func popToRoot(animated: Bool) -> [UIViewController] { popToRootViewController(animated: animated) ?? [] } + /// Pushes a SwiftUI view onto the navigation stack. + /// + /// Wraps the provided SwiftUI view in a UIHostingController and pushes it + /// onto the navigation stack. Automatically disables animation if the + /// navigation controller is not currently in a window to prevent animation + /// issues during initial setup. + /// + /// - Parameters: + /// - view: The SwiftUI view to push onto the stack. + /// - animated: Whether the push transition should be animated. + /// - hidesBottomBar: Whether the bottom bar (tab bar) should be hidden + /// when this view controller is displayed. public func push(_ view: Content, animated: Bool, hidesBottomBar: Bool) { let viewController = UIHostingController(rootView: view) viewController.hidesBottomBarWhenPushed = hidesBottomBar @@ -252,11 +297,38 @@ extension UINavigationController: StackNavigator { push(viewController, animated: animated) } + /// Pushes a view controller onto the navigation stack. + /// + /// Adds the specified view controller to the top of the navigation stack. + /// Automatically disables animation if the navigation controller is not + /// currently in a window to prevent animation issues during initial setup. + /// + /// - Parameters: + /// - viewController: The view controller to push onto the stack. + /// - animated: Whether the push transition should be animated. public func push(_ viewController: UIViewController, animated: Bool) { let animated = view.window != nil ? animated : false pushViewController(viewController, animated: animated) } + /// Presents a SwiftUI view modally. + /// + /// Wraps the provided SwiftUI view in a UIHostingController and presents it modally. + /// Optionally embeds the view in a new navigation controller and configures + /// various presentation options. Automatically disables animation if the + /// navigation controller is not currently in a window to prevent animation issues + /// during initial setup. + /// + /// - Parameters: + /// - view: The SwiftUI view to present modally. + /// - animated: Whether the presentation should be animated. + /// - embedInNavigationController: Whether to wrap the view in a new + /// navigation controller. + /// - isModalInPresentation: Whether the modal enforces modal behavior, + /// preventing interactive dismissal. + /// - overFullscreen: Whether the modal should use full-screen presentation + /// with a clear background. + /// - onCompletion: Optional closure called after presentation completes. public func present( _ view: Content, animated: Bool, @@ -284,6 +356,16 @@ extension UINavigationController: StackNavigator { present(controller, animated: animated, onCompletion: onCompletion) } + /// Replaces the entire navigation stack with a single SwiftUI view. + /// + /// Removes all existing view controllers from the navigation stack and + /// replaces them with a single new view controller containing the specified + /// SwiftUI view. Automatically disables animation if the navigation controller + /// is not currently in a window to prevent animation issues during initial setup. + /// + /// - Parameters: + /// - view: The SwiftUI view that will become the new root of the stack. + /// - animated: Whether the replacement should be animated. public func replace(_ view: Content, animated: Bool) { let animated = self.view.window != nil ? animated : false setViewControllers([UIHostingController(rootView: view)], animated: animated) diff --git a/BitwardenShared/UI/Platform/Application/Utilities/StackNavigatorTests.swift b/BitwardenKit/Core/Platform/Utilities/StackNavigatorTests.swift similarity index 90% rename from BitwardenShared/UI/Platform/Application/Utilities/StackNavigatorTests.swift rename to BitwardenKit/Core/Platform/Utilities/StackNavigatorTests.swift index bdbe88990..7e8606554 100644 --- a/BitwardenShared/UI/Platform/Application/Utilities/StackNavigatorTests.swift +++ b/BitwardenKit/Core/Platform/Utilities/StackNavigatorTests.swift @@ -1,8 +1,8 @@ +import BitwardenKit +import BitwardenKitMocks import SwiftUI import XCTest -@testable import BitwardenShared - // MARK: - StackNavigatorTests class StackNavigatorTests: BitwardenTestCase { @@ -14,7 +14,7 @@ class StackNavigatorTests: BitwardenTestCase { override func setUp() { super.setUp() - subject = UINavigationController() + subject = MockUINavigationController() setKeyWindowRoot(viewController: subject) } @@ -63,14 +63,7 @@ class StackNavigatorTests: BitwardenTestCase { /// `present(_:animated:)` presents the hosted view on existing presented views. @MainActor func test_present_onPresentedView() { - subject.present(EmptyView(), animated: false, embedInNavigationController: false) - subject.present(ScrollView {}, animated: false, embedInNavigationController: false) - XCTAssertTrue(subject.presentedViewController is UIHostingController) - waitFor(subject.presentedViewController?.presentedViewController != nil) - XCTAssertTrue( - subject.presentedViewController?.presentedViewController - is UIHostingController>, - ) + // This test in in `BitwardenShared.StackNavigatorHostedTests` because it requires a host app. } /// `present(_:animated:)` presents the hosted view without embedding it in a navigation controller. diff --git a/BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockUINavigationController.swift b/BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockUINavigationController.swift new file mode 100644 index 000000000..a700ecea2 --- /dev/null +++ b/BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockUINavigationController.swift @@ -0,0 +1,283 @@ +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 + } +} diff --git a/BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockUIViewController.swift b/BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockUIViewController.swift index 39b00ee54..8e41a2291 100644 --- a/BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockUIViewController.swift +++ b/BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockUIViewController.swift @@ -43,17 +43,6 @@ public class MockUIViewController: UIViewController { /// The completion handler passed to the `dismiss` method. public var dismissCompletion: (() -> Void)? - // MARK: Navigation 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 `popViewController` method has been called. - public var popViewControllerCalled = false - // MARK: Navigation Controller Support /// Internal storage for a navigation controller. @@ -107,6 +96,11 @@ public class MockUIViewController: UIViewController { setUpMockHierarchy() } + /// Initializes the mock navigation controller with a nil nib name and bundle. + public init() { + super.init(nibName: nil, bundle: nil) + } + /// Initializes the mock view controller from a coder. /// /// - Parameters: @@ -164,7 +158,7 @@ public class MockUIViewController: UIViewController { } /// 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. @@ -197,9 +191,7 @@ public class MockUIViewController: UIViewController { dismissAnimated = false dismissCompletion = nil - pushViewControllerCalled = false - pushedViewController = nil - popViewControllerCalled = false + _navigationController = nil } // MARK: Mock Hierarchy diff --git a/BitwardenShared/UI/Auth/AuthModule.swift b/BitwardenShared/UI/Auth/AuthModule.swift index 8170ccea3..77a9d7acd 100644 --- a/BitwardenShared/UI/Auth/AuthModule.swift +++ b/BitwardenShared/UI/Auth/AuthModule.swift @@ -1,3 +1,4 @@ +import BitwardenKit import UIKit // MARK: - AuthModule diff --git a/BitwardenShared/UI/Auth/ProfileSwitcher/ProfileSwitcherModule.swift b/BitwardenShared/UI/Auth/ProfileSwitcher/ProfileSwitcherModule.swift index ad74c2d3a..68faca656 100644 --- a/BitwardenShared/UI/Auth/ProfileSwitcher/ProfileSwitcherModule.swift +++ b/BitwardenShared/UI/Auth/ProfileSwitcher/ProfileSwitcherModule.swift @@ -1,3 +1,4 @@ +import BitwardenKit import Foundation // MARK: - ProfileSwitcherModule diff --git a/BitwardenShared/UI/Platform/Application/Utilities/RootViewControllerTests.swift b/BitwardenShared/UI/Platform/Application/Utilities/RootViewControllerTests.swift index 86b91eb71..6a6aca6f1 100644 --- a/BitwardenShared/UI/Platform/Application/Utilities/RootViewControllerTests.swift +++ b/BitwardenShared/UI/Platform/Application/Utilities/RootViewControllerTests.swift @@ -1,3 +1,4 @@ +import BitwardenKitMocks import XCTest @testable import BitwardenShared diff --git a/BitwardenShared/UI/Platform/Application/Utilities/StackNavigatorHostedTests.swift b/BitwardenShared/UI/Platform/Application/Utilities/StackNavigatorHostedTests.swift new file mode 100644 index 000000000..b5680375e --- /dev/null +++ b/BitwardenShared/UI/Platform/Application/Utilities/StackNavigatorHostedTests.swift @@ -0,0 +1,38 @@ +import BitwardenKit +import BitwardenKitMocks +import SwiftUI +import XCTest + +// MARK: - StackNavigatorTests + +class StackNavigatorHostedTests: BitwardenTestCase { + // MARK: Properties + + var subject: UINavigationController! + + // MARK: Setup & Teardown + + override func setUp() { + super.setUp() + subject = UINavigationController() + setKeyWindowRoot(viewController: subject) + } + + // MARK: Tests + + /// `present(_:animated:)` presents the hosted view on existing presented views. + /// This is in `BitwardenSharedTests` instead of `BitwardenKitTests` because it requires a host app, + /// due to the fact that the implementation of `StackNavigator` creates a `UIHostingController`, + /// so we cannot mock it without significantly more rigamarole, which seems excessive for one test. + @MainActor + func test_present_onPresentedView() { + subject.present(EmptyView(), animated: false, embedInNavigationController: false) + subject.present(ScrollView {}, animated: false, embedInNavigationController: false) + XCTAssertTrue(subject.presentedViewController is UIHostingController) + waitFor(subject.presentedViewController?.presentedViewController != nil) + XCTAssertTrue( + subject.presentedViewController?.presentedViewController + is UIHostingController>, + ) + } +} diff --git a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuModule.swift b/BitwardenShared/UI/Platform/DebugMenu/DebugMenuModule.swift index d9c09294a..104090dd7 100644 --- a/BitwardenShared/UI/Platform/DebugMenu/DebugMenuModule.swift +++ b/BitwardenShared/UI/Platform/DebugMenu/DebugMenuModule.swift @@ -1,3 +1,4 @@ +import BitwardenKit import Foundation // MARK: - DebugMenuModule diff --git a/BitwardenShared/UI/Platform/ExtensionSetup/ExtensionActivation/ExtensionSetupCoordinatorTests.swift b/BitwardenShared/UI/Platform/ExtensionSetup/ExtensionActivation/ExtensionSetupCoordinatorTests.swift index 69973e980..a7c597f7e 100644 --- a/BitwardenShared/UI/Platform/ExtensionSetup/ExtensionActivation/ExtensionSetupCoordinatorTests.swift +++ b/BitwardenShared/UI/Platform/ExtensionSetup/ExtensionActivation/ExtensionSetupCoordinatorTests.swift @@ -1,3 +1,4 @@ +import BitwardenKitMocks import SwiftUI import XCTest diff --git a/BitwardenShared/UI/Platform/ExtensionSetup/ExtensionSetupModule.swift b/BitwardenShared/UI/Platform/ExtensionSetup/ExtensionSetupModule.swift index 43afc253d..5632973d2 100644 --- a/BitwardenShared/UI/Platform/ExtensionSetup/ExtensionSetupModule.swift +++ b/BitwardenShared/UI/Platform/ExtensionSetup/ExtensionSetupModule.swift @@ -1,3 +1,4 @@ +import BitwardenKit import UIKit // MARK: - ExtensionSetupModule diff --git a/BitwardenShared/UI/Platform/FileSelection/FileSelectionModule.swift b/BitwardenShared/UI/Platform/FileSelection/FileSelectionModule.swift index 1670d2829..169dc06ba 100644 --- a/BitwardenShared/UI/Platform/FileSelection/FileSelectionModule.swift +++ b/BitwardenShared/UI/Platform/FileSelection/FileSelectionModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - FileSelectionModule /// An object that builds coordinators for the file selection flow. diff --git a/BitwardenShared/UI/Platform/LoginRequest/LoginRequestCoordinatorTests.swift b/BitwardenShared/UI/Platform/LoginRequest/LoginRequestCoordinatorTests.swift index 8a02ef1b9..24966c1d1 100644 --- a/BitwardenShared/UI/Platform/LoginRequest/LoginRequestCoordinatorTests.swift +++ b/BitwardenShared/UI/Platform/LoginRequest/LoginRequestCoordinatorTests.swift @@ -1,3 +1,4 @@ +import BitwardenKitMocks import SwiftUI import XCTest diff --git a/BitwardenShared/UI/Platform/LoginRequest/LoginRequestModule.swift b/BitwardenShared/UI/Platform/LoginRequest/LoginRequestModule.swift index 89d978bb0..c70e7c076 100644 --- a/BitwardenShared/UI/Platform/LoginRequest/LoginRequestModule.swift +++ b/BitwardenShared/UI/Platform/LoginRequest/LoginRequestModule.swift @@ -1,3 +1,4 @@ +import BitwardenKit import Foundation // MARK: - LoginRequestModule diff --git a/BitwardenShared/UI/Platform/Settings/Settings/AutoFill/PasswordAutoFill/PasswordAutoFillCoordinatorTests.swift b/BitwardenShared/UI/Platform/Settings/Settings/AutoFill/PasswordAutoFill/PasswordAutoFillCoordinatorTests.swift index 9587d84b2..d754b1308 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/AutoFill/PasswordAutoFill/PasswordAutoFillCoordinatorTests.swift +++ b/BitwardenShared/UI/Platform/Settings/Settings/AutoFill/PasswordAutoFill/PasswordAutoFillCoordinatorTests.swift @@ -1,3 +1,4 @@ +import BitwardenKitMocks import SwiftUI import XCTest diff --git a/BitwardenShared/UI/Platform/Settings/Settings/AutoFill/PasswordAutoFill/PasswordAutoFillModule.swift b/BitwardenShared/UI/Platform/Settings/Settings/AutoFill/PasswordAutoFill/PasswordAutoFillModule.swift index 00be6d111..404548203 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/AutoFill/PasswordAutoFill/PasswordAutoFillModule.swift +++ b/BitwardenShared/UI/Platform/Settings/Settings/AutoFill/PasswordAutoFill/PasswordAutoFillModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - PasswordAutoFillModule /// An object that builds coordinators for the password autofill flow. diff --git a/BitwardenShared/UI/Platform/Settings/Settings/Vault/Folders/AddEditFolder/AddEditFolderCoordinatorTests.swift b/BitwardenShared/UI/Platform/Settings/Settings/Vault/Folders/AddEditFolder/AddEditFolderCoordinatorTests.swift index 713d77f72..7a80b1bc5 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/Vault/Folders/AddEditFolder/AddEditFolderCoordinatorTests.swift +++ b/BitwardenShared/UI/Platform/Settings/Settings/Vault/Folders/AddEditFolder/AddEditFolderCoordinatorTests.swift @@ -1,3 +1,4 @@ +import BitwardenKitMocks import BitwardenSdk import SwiftUI import XCTest diff --git a/BitwardenShared/UI/Platform/Settings/Settings/Vault/Folders/AddEditFolder/AddEditFolderModule.swift b/BitwardenShared/UI/Platform/Settings/Settings/Vault/Folders/AddEditFolder/AddEditFolderModule.swift index 821476fba..873e1fc30 100644 --- a/BitwardenShared/UI/Platform/Settings/Settings/Vault/Folders/AddEditFolder/AddEditFolderModule.swift +++ b/BitwardenShared/UI/Platform/Settings/Settings/Vault/Folders/AddEditFolder/AddEditFolderModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - AddEditFolderModule /// An object that builds coordinators for the add and edit folder view. diff --git a/BitwardenShared/UI/Platform/Settings/SettingsModule.swift b/BitwardenShared/UI/Platform/Settings/SettingsModule.swift index 8d63fbc5f..603df8e37 100644 --- a/BitwardenShared/UI/Platform/Settings/SettingsModule.swift +++ b/BitwardenShared/UI/Platform/Settings/SettingsModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - SettingsModule /// An object that builds coordinators for the settings tab. diff --git a/BitwardenShared/UI/Tools/ExportCXF/ExportCXFModule.swift b/BitwardenShared/UI/Tools/ExportCXF/ExportCXFModule.swift index 8d739437f..5bc983b64 100644 --- a/BitwardenShared/UI/Tools/ExportCXF/ExportCXFModule.swift +++ b/BitwardenShared/UI/Tools/ExportCXF/ExportCXFModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - ExportCXFModule /// An object that builds coordinators for the Credential Exchange export flow. diff --git a/BitwardenShared/UI/Tools/Generator/GeneratorCoordinatorTests.swift b/BitwardenShared/UI/Tools/Generator/GeneratorCoordinatorTests.swift index 344b60655..13e3d36d0 100644 --- a/BitwardenShared/UI/Tools/Generator/GeneratorCoordinatorTests.swift +++ b/BitwardenShared/UI/Tools/Generator/GeneratorCoordinatorTests.swift @@ -1,4 +1,5 @@ import BitwardenKit +import BitwardenKitMocks import SwiftUI import XCTest diff --git a/BitwardenShared/UI/Tools/Generator/GeneratorModule.swift b/BitwardenShared/UI/Tools/Generator/GeneratorModule.swift index bc6ca9299..9080261f7 100644 --- a/BitwardenShared/UI/Tools/Generator/GeneratorModule.swift +++ b/BitwardenShared/UI/Tools/Generator/GeneratorModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - Generator Module /// An object that builds coordinators for the generator tab. diff --git a/BitwardenShared/UI/Tools/ImportCXF/ImportCXFModule.swift b/BitwardenShared/UI/Tools/ImportCXF/ImportCXFModule.swift index f03b118eb..faae53358 100644 --- a/BitwardenShared/UI/Tools/ImportCXF/ImportCXFModule.swift +++ b/BitwardenShared/UI/Tools/ImportCXF/ImportCXFModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - ImportCXFModule /// An object that builds coordinators for the Credential Exchange import flow. diff --git a/BitwardenShared/UI/Tools/PasswordHistory/PasswordHistoryCoordinatorTests.swift b/BitwardenShared/UI/Tools/PasswordHistory/PasswordHistoryCoordinatorTests.swift index 63a3b5783..709522d8f 100644 --- a/BitwardenShared/UI/Tools/PasswordHistory/PasswordHistoryCoordinatorTests.swift +++ b/BitwardenShared/UI/Tools/PasswordHistory/PasswordHistoryCoordinatorTests.swift @@ -1,3 +1,4 @@ +import BitwardenKitMocks import SwiftUI import XCTest diff --git a/BitwardenShared/UI/Tools/PasswordHistory/PasswordHistoryModule.swift b/BitwardenShared/UI/Tools/PasswordHistory/PasswordHistoryModule.swift index 0526e4a0c..ca144f6f9 100644 --- a/BitwardenShared/UI/Tools/PasswordHistory/PasswordHistoryModule.swift +++ b/BitwardenShared/UI/Tools/PasswordHistory/PasswordHistoryModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - PasswordHistoryModule /// An object that builds coordinators for the password history view. diff --git a/BitwardenShared/UI/Tools/Send/Send/SendModule.swift b/BitwardenShared/UI/Tools/Send/Send/SendModule.swift index 3f69b9bce..b6f723e09 100644 --- a/BitwardenShared/UI/Tools/Send/Send/SendModule.swift +++ b/BitwardenShared/UI/Tools/Send/Send/SendModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - SendModule /// An object that builds coordinators for the send tab. diff --git a/BitwardenShared/UI/Tools/Send/SendItem/SendItemModule.swift b/BitwardenShared/UI/Tools/Send/SendItem/SendItemModule.swift index 1cc62284b..2410270cf 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/SendItemModule.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/SendItemModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - SendItemModule /// An object that builds coordinators for the send item flow. diff --git a/BitwardenShared/UI/Vault/ImportLogins/ImportLoginsCoordinatorTests.swift b/BitwardenShared/UI/Vault/ImportLogins/ImportLoginsCoordinatorTests.swift index 2749476eb..4d4d53c0c 100644 --- a/BitwardenShared/UI/Vault/ImportLogins/ImportLoginsCoordinatorTests.swift +++ b/BitwardenShared/UI/Vault/ImportLogins/ImportLoginsCoordinatorTests.swift @@ -1,3 +1,4 @@ +import BitwardenKitMocks import SwiftUI import XCTest diff --git a/BitwardenShared/UI/Vault/ImportLogins/ImportLoginsModule.swift b/BitwardenShared/UI/Vault/ImportLogins/ImportLoginsModule.swift index 6f6cbb203..fb810cdd3 100644 --- a/BitwardenShared/UI/Vault/ImportLogins/ImportLoginsModule.swift +++ b/BitwardenShared/UI/Vault/ImportLogins/ImportLoginsModule.swift @@ -1,3 +1,5 @@ +import BitwardenKit + // MARK: - ImportLoginsModule /// An object that builds coordinators for the import logins views. diff --git a/BitwardenShared/UI/Vault/Vault/VaultModule.swift b/BitwardenShared/UI/Vault/Vault/VaultModule.swift index 5d5ea2077..399e2eab0 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultModule.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultModule.swift @@ -1,3 +1,4 @@ +import BitwardenKit import Foundation // MARK: - VaultModule diff --git a/BitwardenShared/UI/Vault/VaultItem/VaultItemModule.swift b/BitwardenShared/UI/Vault/VaultItem/VaultItemModule.swift index 7356f6c2b..9f8431511 100644 --- a/BitwardenShared/UI/Vault/VaultItem/VaultItemModule.swift +++ b/BitwardenShared/UI/Vault/VaultItem/VaultItemModule.swift @@ -1,3 +1,4 @@ +import BitwardenKit import Foundation // MARK: - VaultModule diff --git a/GlobalTestHelpers-bwa/MockAppModule.swift b/GlobalTestHelpers-bwa/MockAppModule.swift index 6627b842d..23efb21c7 100644 --- a/GlobalTestHelpers-bwa/MockAppModule.swift +++ b/GlobalTestHelpers-bwa/MockAppModule.swift @@ -55,7 +55,7 @@ class MockAppModule: } func makeItemListCoordinator( - stackNavigator _: AuthenticatorShared.StackNavigator, + stackNavigator _: StackNavigator, ) -> AnyCoordinator { itemListCoordinator.asAnyCoordinator() } diff --git a/GlobalTestHelpers-bwa/MockStackNavigator.swift b/GlobalTestHelpers-bwa/MockStackNavigator.swift deleted file mode 100644 index 3ff9a1994..000000000 --- a/GlobalTestHelpers-bwa/MockStackNavigator.swift +++ /dev/null @@ -1,112 +0,0 @@ -import AuthenticatorShared -import BitwardenKit -import SwiftUI - -final class MockStackNavigator: StackNavigator { - struct NavigationAction { - var type: NavigationType - var view: Any? - var animated: Bool - var hidesBottomBar: Bool? - var overFullscreen: Bool? - } - - enum NavigationType { - case dismissed - case dismissedWithCompletionHandler - case pushed - case popped - case poppedToRoot - case presented - case presentedInSheet - case replaced - } - - var actions: [NavigationAction] = [] - var alerts: [BitwardenKit.Alert] = [] - var isEmpty = true - var isPresenting: Bool { actions.last?.type == .presented } - var rootViewController: UIViewController? - - var viewControllersToPop: [UIViewController] = [] - - func dismiss(animated: Bool) { - actions.append(NavigationAction(type: .dismissed, animated: animated)) - } - - func dismiss(animated: Bool, completion: (() -> Void)?) { - completion?() - actions.append(NavigationAction(type: .dismissedWithCompletionHandler, animated: animated)) - } - - func push(_ view: Content, animated: Bool, hidesBottomBar: Bool) { - actions.append(NavigationAction( - type: .pushed, - view: view, - animated: animated, - hidesBottomBar: hidesBottomBar, - )) - } - - func push(_ viewController: UIViewController, animated: Bool) { - actions.append(NavigationAction( - type: .pushed, - view: viewController, - animated: animated, - )) - } - - @discardableResult - func pop(animated: Bool) -> UIViewController? { - actions.append(NavigationAction(type: .popped, animated: animated)) - return viewControllersToPop.last - } - - @discardableResult - func popToRoot(animated: Bool) -> [UIViewController] { - actions.append(NavigationAction(type: .poppedToRoot, animated: animated)) - return viewControllersToPop - } - - func present(_ alert: BitwardenKit.Alert) { - alerts.append(alert) - } - - func present( - _ view: Content, - animated: Bool, - overFullscreen: Bool, - onCompletion: (() -> Void)?, - ) { - onCompletion?() - actions.append( - NavigationAction( - type: .presented, - view: view, - animated: animated, - overFullscreen: overFullscreen, - ), - ) - } - - func present( - _ viewController: UIViewController, - animated: Bool, - overFullscreen: Bool, - onCompletion: (() -> Void)?, - ) { - onCompletion?() - actions.append( - NavigationAction( - type: .presented, - view: viewController, - animated: animated, - overFullscreen: overFullscreen, - ), - ) - } - - func replace(_ view: Content, animated: Bool) { - actions.append(NavigationAction(type: .replaced, view: view, animated: animated)) - } -} diff --git a/GlobalTestHelpers/MockAppModule.swift b/GlobalTestHelpers/MockAppModule.swift index f42f2a26d..18677ec75 100644 --- a/GlobalTestHelpers/MockAppModule.swift +++ b/GlobalTestHelpers/MockAppModule.swift @@ -194,7 +194,7 @@ class MockAppModule: func makeVaultCoordinator( delegate _: BitwardenShared.VaultCoordinatorDelegate, - stackNavigator _: BitwardenShared.StackNavigator, + stackNavigator _: StackNavigator, ) -> BitwardenShared.AnyCoordinator { vaultCoordinator.asAnyCoordinator() }