[PM-26060] Consolidate Alert to BitwardenKit (#2081)

This commit is contained in:
Katherine Bertelsen 2025-10-30 09:43:05 -05:00 committed by GitHub
parent 87e3a223a8
commit f90ffd8b16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
74 changed files with 510 additions and 821 deletions

View File

@ -1,42 +0,0 @@
import BitwardenKit
import BitwardenResources
import Foundation
// MARK: - Alert+Error
extension Alert {
// MARK: Methods
/// The default alert style with a standard ok button to dismiss.
///
/// - Parameters:
/// - title: The alert's title.
/// - message: The alert's message.
///
/// - Returns a default styled alert.
///
static func defaultAlert(
title: String? = nil,
message: String? = nil,
alertActions: [AlertAction]? = nil,
) -> Alert {
Alert(
title: title,
message: message,
alertActions: alertActions ?? [AlertAction(title: Localizations.ok, style: .cancel)],
)
}
/// Creates an alert for an `InputValidationError`.
///
/// - Parameter error: The error to create the alert for.
/// - Returns: An alert that can be displayed to the user for an `InputValidationError`.
///
static func inputValidationAlert(error: InputValidationError) -> Alert {
Alert(
title: Localizations.anErrorHasOccurred,
message: error.message,
alertActions: [AlertAction(title: Localizations.ok, style: .default)],
)
}
}

View File

@ -1,168 +0,0 @@
import UIKit
// MARK: - Alert
/// A helper class that can create a `UIAlertController`.
/// This allows for easier testing of alert controllers and actions.
///
public class Alert {
// MARK: Properties
/// A list of actions that the user can tap on in the alert.
var alertActions: [AlertAction] = []
/// A list of text fields that the user can use to enter text.
var alertTextFields: [AlertTextField] = []
/// The message that is displayed in the alert.
let message: String?
/// The preferred action for the user to take in the alert, which emphasis is given.
var preferredAction: AlertAction?
/// The alert's style.
let preferredStyle: UIAlertController.Style
/// The title of the message that is displayed at the top of the alert.
let title: String?
// MARK: Initialization
/// Initializes an `Alert`.
///
/// - Parameters:
/// - title: The title of the message that is displayed at the top of the alert.
/// - message: The message that is displayed in the alert.
/// - preferredStyle: The alert's style.
/// - alertActions: A list of actions that the user can tap on in the alert.
/// - alertTextFields: A list of text fields that the user can enter text into.
///
public init(
title: String?,
message: String?,
preferredStyle: UIAlertController.Style = .alert,
alertActions: [AlertAction] = [],
alertTextFields: [AlertTextField] = [],
) {
self.title = title
self.message = message
self.preferredStyle = preferredStyle
self.alertActions = alertActions
self.alertTextFields = alertTextFields
}
// MARK: Methods
/// Adds an `AlertAction` to the `Alert`.
///
/// - Parameter action: The `AlertAction` to add to the `Alert`.
///
/// - Returns: `self` to allow `add(_:)` methods to be chained.
///
@discardableResult
func add(_ action: AlertAction) -> Self {
alertActions.append(action)
return self
}
/// Adds an `AlertTextField` to the `Alert`.
///
/// - Parameter textField: The `AlertTextField` to add to the `Alert`.
///
/// - Returns: `self` to allow `add(_:)` methods to be chained.
///
@discardableResult
func add(_ textField: AlertTextField) -> Self {
alertTextFields.append(textField)
return self
}
/// Adds a preferred `AlertAction` to the `Alert`. The preferred action is the action that the
/// user should take and is given emphasis. This replaces an existing preferred action, if one
/// exists.
///
/// - Parameter action: The preferred `AlertAction` to add to the `Alert`.
///
/// - Returns: `self` to allow `add(_:)` methods to be chained.
///
@discardableResult
func addPreferred(_ action: AlertAction) -> Self {
alertActions.append(action)
preferredAction = action
return self
}
/// Creates a `UIAlertController` from the `Alert` that can be presented in the view.
///
/// - Returns An initialized `UIAlertController` that has the `AlertAction`s added.
///
@MainActor
func createAlertController() -> UIAlertController {
let alert = UIAlertController(title: title, message: message, preferredStyle: preferredStyle)
alertTextFields.forEach { alertTextField in
alert.addTextField { textField in
textField.placeholder = alertTextField.placeholder
textField.tintColor = Asset.Colors.primaryBitwarden.color
textField.keyboardType = alertTextField.keyboardType
textField.isSecureTextEntry = alertTextField.isSecureTextEntry
textField.autocapitalizationType = alertTextField.autocapitalizationType
textField.autocorrectionType = alertTextField.autocorrectionType
textField.text = alertTextField.text
textField.addTarget(
alertTextField,
action: #selector(AlertTextField.textChanged(in:)),
for: .editingChanged,
)
}
}
alertActions.forEach { alertAction in
let action = UIAlertAction(title: alertAction.title, style: alertAction.style) { _ in
Task {
await alertAction.handler?(alertAction, self.alertTextFields)
}
}
alert.addAction(action)
if let preferredAction, preferredAction === alertAction {
alert.preferredAction = action
}
}
alert.view.tintColor = Asset.Colors.primaryBitwarden.color
return alert
}
}
// swiftlint:disable line_length
extension Alert: CustomDebugStringConvertible {
public var debugDescription: String {
"""
Alert(title: \(title ?? "nil"), message: \(message ?? "nil"), alertActions: \(alertActions), alertTextFields: \(alertTextFields))
"""
}
}
// swiftlint:enable line_length
extension Alert: Equatable {
public static func == (lhs: Alert, rhs: Alert) -> Bool {
lhs.alertActions == rhs.alertActions
&& lhs.message == rhs.message
&& lhs.preferredAction == rhs.preferredAction
&& lhs.preferredStyle == rhs.preferredStyle
&& lhs.title == rhs.title
}
}
extension Alert: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(alertActions)
hasher.combine(message)
hasher.combine(preferredAction)
hasher.combine(preferredStyle)
hasher.combine(title)
}
}

View File

@ -1,78 +0,0 @@
import UIKit
// MARK: - AlertAction
/// An action that can be added to an `Alert`. This is modeled after `UIAlertAction`
/// and allows the handler to be invoked from tests.
///
public class AlertAction {
// MARK: Properties
/// An optional handler that is called when the user taps on the action from the alert.
let handler: ((AlertAction, [AlertTextField]) async -> Void)?
/// The style of the action.
let style: UIAlertAction.Style
/// The title of the alert action to display in the alert.
let title: String
// MARK: Initialization
/// Initializes an `AlertAction` with a title, style and optional handler.
///
/// - Parameters:
/// - title: The title of the alert action.
/// - style: The style of the alert action to use when creating a `UIAlertAction`.
/// - handler: The handler that is called when the user taps on the action in the alert.
///
public init(
title: String,
style: UIAlertAction.Style,
handler: ((AlertAction, [AlertTextField]) async -> Void)? = nil,
) {
self.title = title
self.style = style
self.handler = handler
}
/// Initializes an `AlertAction` with a title, style and optional handler.
///
/// - Parameters:
/// - title: The title of the alert action.
/// - style: The style of the alert action to use when creating a `UIAlertAction`.
/// - handler: The handler that is called when the user taps on the action in the alert.
///
public init(
title: String,
style: UIAlertAction.Style,
handler: @escaping (AlertAction) async -> Void,
) {
self.title = title
self.style = style
self.handler = { action, _ in
await handler(action)
}
}
}
extension AlertAction: Equatable {
public static func == (lhs: AlertAction, rhs: AlertAction) -> Bool {
guard lhs.style == rhs.style, lhs.title == rhs.title else { return false }
switch (lhs.handler, rhs.handler) {
case (.none, .none),
(.some, .some):
return true
case (_, .some),
(.some, _):
return false
}
}
}
extension AlertAction: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(title)
hasher.combine(style)
}
}

View File

@ -1,29 +0,0 @@
import BitwardenKit
import BitwardenResources
import XCTest
@testable import AuthenticatorShared
class AlertErrorTests: BitwardenTestCase {
/// `defaultAlert(title:message:)` constructs an `Alert` with the title, message, and an OK button.
func test_defaultAlert() {
let subject = Alert.defaultAlert(title: "title", message: "message")
XCTAssertEqual(subject.title, "title")
XCTAssertEqual(subject.message, "message")
XCTAssertEqual(subject.alertActions, [AlertAction(title: Localizations.ok, style: .cancel)])
}
/// `inputValidationAlert(error:)` creates an `Alert` for an input validation error.
func test_inputValidationAlert() {
let subject = Alert.inputValidationAlert(
error: InputValidationError(
message: Localizations.validationFieldRequired(Localizations.accountName),
),
)
XCTAssertEqual(subject.title, Localizations.anErrorHasOccurred)
XCTAssertEqual(subject.message, Localizations.validationFieldRequired(Localizations.accountName))
XCTAssertEqual(subject.alertActions, [AlertAction(title: Localizations.ok, style: .default)])
}
}

View File

@ -1,46 +0,0 @@
import BitwardenKit
import UIKit
// MARK: - AlertPresentable
/// Protocol for creating and presenting a `UIAlertController` from an `Alert`.
///
@MainActor
public protocol AlertPresentable {
/// The root view controller that the alert should be presented on.
var rootViewController: UIViewController? { get }
/// Presents a `UIAlertController` created from the `Alert` on the provided `rootViewController`.
///
/// - Parameter alert: The `Alert` used to create a `UIAlertController` to present.
///
func present(_ alert: Alert)
}
public extension AlertPresentable {
/// Presents a `UIAlertController` created from the `Alert` on the provided `rootViewController`.
///
/// - Parameter alert: The `Alert` used to create a `UIAlertController` to present.
///
func present(_ alert: Alert) {
let alertController = alert.createAlertController()
guard let parent = rootViewController?.topmostViewController() else { return }
if alert.preferredStyle == .actionSheet {
// iPadOS requires an anchor for action sheets. This solution keeps the iPad app from crashing, and centers
// the presentation of the action sheet.
alertController.popoverPresentationController?.sourceView = parent.view
alertController.popoverPresentationController?.sourceRect = CGRect(
x: parent.view.bounds.midX,
y: parent.view.bounds.midY,
width: 0,
height: 0,
)
alertController.popoverPresentationController?.permittedArrowDirections = []
}
parent.present(alertController, animated: UI.animated)
}
}
extension UIWindow: AlertPresentable {}

View File

@ -1,41 +0,0 @@
import XCTest
@testable import AuthenticatorShared
// MARK: - AlertPresentableTests
class AlertPresentableTests: BitwardenTestCase {
// MARK: Properties
var rootViewController: UIViewController!
var subject: AlertPresentableSubject!
// MARK: Setup and Teardown
override func setUp() {
super.setUp()
rootViewController = UIViewController()
subject = AlertPresentableSubject()
subject.rootViewController = rootViewController
setKeyWindowRoot(viewController: rootViewController)
}
override func tearDown() {
super.tearDown()
rootViewController = nil
subject = nil
}
// MARK: Tests
/// `present(_:)` presents a `UIAlertController` on the root view controller.
@MainActor
func test_present() {
subject.present(Alert(title: "🍎", message: "🥝", preferredStyle: .alert))
XCTAssertNotNil(rootViewController.presentedViewController as? UIAlertController)
}
}
class AlertPresentableSubject: AlertPresentable {
var rootViewController: UIViewController?
}

View File

@ -1,117 +0,0 @@
import BitwardenSdk
import XCTest
@testable import AuthenticatorShared
// MARK: - AlertTests
class AlertTests: BitwardenTestCase {
// MARK: Properties
var subject: Alert!
// MARK: Setup and Teardown
override func setUp() {
super.setUp()
subject = Alert(title: "🍎", message: "🥝", preferredStyle: .alert)
.add(AlertAction(title: "Cancel", style: .cancel))
.addPreferred(AlertAction(title: "OK", style: .default))
.add(AlertTextField(
id: "field",
autocapitalizationType: .allCharacters,
autocorrectionType: .no,
isSecureTextEntry: true,
keyboardType: .numberPad,
placeholder: "placeholder",
text: "value",
))
}
override func tearDown() {
super.tearDown()
subject = nil
}
// MARK: Tests
/// `createAlertController` returns a `UIAlertController` based on the alert details.
@MainActor
func test_createAlertController() throws {
let alertController = subject.createAlertController()
XCTAssertEqual(alertController.title, "🍎")
XCTAssertEqual(alertController.message, "🥝")
XCTAssertEqual(alertController.preferredStyle, .alert)
XCTAssertEqual(alertController.actions.count, 2)
XCTAssertEqual(alertController.actions[0].title, "Cancel")
XCTAssertEqual(alertController.actions[0].style, .cancel)
XCTAssertEqual(alertController.actions[1].title, "OK")
XCTAssertEqual(alertController.actions[1].style, .default)
XCTAssertEqual(alertController.textFields?.count, 1)
let textField = try XCTUnwrap(alertController.textFields?.first)
XCTAssertEqual(textField.text, "value")
XCTAssertEqual(textField.placeholder, "placeholder")
XCTAssertEqual(textField.autocapitalizationType, .allCharacters)
XCTAssertEqual(textField.autocorrectionType, .no)
XCTAssertEqual(textField.isSecureTextEntry, true)
XCTAssertEqual(textField.keyboardType, .numberPad)
XCTAssertEqual(alertController.preferredAction?.title, "OK")
}
/// `debugDescription` contains the alert's properties
func test_debugDescription() {
XCTAssertEqual(
subject!.debugDescription,
// swiftlint:disable:next line_length
"Alert(title: 🍎, message: 🥝, alertActions: [AuthenticatorShared.AlertAction, AuthenticatorShared.AlertAction],"
+ " alertTextFields: [AuthenticatorShared.AlertTextField])",
)
}
/// Alert conforms to `Equatable`.
func test_equatable() {
XCTAssertEqual(subject, Alert(title: "🍎", message: "🥝", preferredStyle: .alert)
.add(AlertAction(title: "Cancel", style: .cancel))
.addPreferred(AlertAction(title: "OK", style: .default))
.add(AlertTextField(
id: "field",
autocapitalizationType: .allCharacters,
autocorrectionType: .yes,
isSecureTextEntry: true,
keyboardType: .numberPad,
placeholder: "placeholder",
text: "value",
)))
XCTAssertNotEqual(subject, Alert(title: "🍎", message: "🥝", preferredStyle: .alert)
.add(AlertAction(title: "Cancel", style: .destructive))
.addPreferred(AlertAction(title: "OK", style: .default))
.add(AlertTextField(
id: "field",
autocapitalizationType: .allCharacters,
autocorrectionType: .yes,
isSecureTextEntry: true,
keyboardType: .numberPad,
placeholder: "placeholder",
text: "value",
)))
XCTAssertNotEqual(subject, Alert(title: "🍎", message: "🥝", preferredStyle: .alert))
XCTAssertNotEqual(subject, Alert(title: "🍎", message: "🥝", preferredStyle: .alert)
.add(AlertAction(title: "Cancel", style: .cancel))
.addPreferred(AlertAction(title: "OK", style: .default) { _ in })
.add(AlertTextField(
id: "field",
autocapitalizationType: .allCharacters,
autocorrectionType: .yes,
isSecureTextEntry: true,
keyboardType: .numberPad,
placeholder: "placeholder",
text: "value",
)))
XCTAssertEqual(subject, Alert(title: "🍎", message: "🥝", preferredStyle: .alert)
.add(AlertAction(title: "Cancel", style: .cancel))
.addPreferred(AlertAction(title: "OK", style: .default)))
}
}

View File

@ -1,87 +0,0 @@
import UIKit
// MARK: - AlertTextField
/// A text field that can be added to an `Alert`. This class allows an `AlertAction` to retrieve a
/// value entered by the user when executing its handler.
///
public class AlertTextField {
/// The identifier for this text field.
public var id: String
/// The placeholder for this text field.
public var placeholder: String?
/// The text value entered by the user.
public private(set) var text: String
/// How the text should be autocapitalized in this field.
public let autocapitalizationType: UITextAutocapitalizationType
/// How the text should be autocorrected in this field.
public let autocorrectionType: UITextAutocorrectionType
/// A flag indicating if this text field's contents should be masked.
public let isSecureTextEntry: Bool
/// The keyboard type for this text field.
public let keyboardType: UIKeyboardType
/// Creates a new `AlertTextField`.
///
/// - Parameters:
/// - id: The identifier for this text field. Defaults to a new UUID.
/// - autocapitalizationType: How the text should be autocapitalized in this field. Defaults to `.sentences`.
/// - autocorrectionType: How the text should be autocorrected in this field. Defaults to `.default`.
/// - isSecureTextEntry: A flag indicating if this text field's content should be masked.
/// - keyboardType: The keyboard type for this text field. Defaults to `.default`.
/// - placeholder: The optional placeholder for this text field. Defaults to `nil`.
/// - text: An optional initial value to pre-fill the text field with.
///
public init(
id: String = UUID().uuidString,
autocapitalizationType: UITextAutocapitalizationType = .sentences,
autocorrectionType: UITextAutocorrectionType = .default,
isSecureTextEntry: Bool = false,
keyboardType: UIKeyboardType = .default,
placeholder: String? = nil,
text: String? = nil,
) {
self.id = id
self.autocapitalizationType = autocapitalizationType
self.autocorrectionType = autocorrectionType
self.isSecureTextEntry = isSecureTextEntry
self.keyboardType = keyboardType
self.placeholder = placeholder
self.text = text ?? ""
}
@objc
func textChanged(in textField: UITextField) {
text = textField.text ?? ""
}
}
extension AlertTextField: Equatable {
public static func == (lhs: AlertTextField, rhs: AlertTextField) -> Bool {
lhs.autocapitalizationType == rhs.autocapitalizationType
&& lhs.autocorrectionType == rhs.autocorrectionType
&& lhs.id == rhs.id
&& lhs.isSecureTextEntry == rhs.isSecureTextEntry
&& lhs.keyboardType == rhs.keyboardType
&& lhs.placeholder == rhs.placeholder
&& lhs.text == rhs.text
}
}
extension AlertTextField: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(autocapitalizationType)
hasher.combine(autocorrectionType)
hasher.combine(id)
hasher.combine(isSecureTextEntry)
hasher.combine(keyboardType)
hasher.combine(placeholder)
hasher.combine(text)
}
}

View File

@ -1,28 +0,0 @@
import Foundation
@testable import AuthenticatorShared
enum AlertError: LocalizedError {
case alertActionNotFound(title: String)
var errorDescription: String? {
switch self {
case let .alertActionNotFound(title):
"Unable to locate an alert action for the title: \(title)"
}
}
}
extension Alert {
/// Simulates a user interaction with the alert action that matches the provided title.
///
/// - Parameter title: The title of the alert action to trigger.
/// - Throws: Throws an `AlertError` if the alert action cannot be found.
///
func tapAction(title: String) async throws {
guard let alertAction = alertActions.first(where: { $0.title == title }) else {
throw AlertError.alertActionNotFound(title: title)
}
await alertAction.handler?(alertAction, alertTextFields)
}
}

View File

@ -60,7 +60,7 @@ open class AnyCoordinator<Route, Event>: Coordinator {
doNavigate(route, context)
}
open func showAlert(_ alert: Alert) {
open func showAlert(_ alert: BitwardenKit.Alert) {
doShowAlert(alert)
}

View File

@ -34,7 +34,7 @@ public protocol Coordinator<Route, Event>: AnyObject {
///
/// - Parameter alert: The alert to show.
///
func showAlert(_ alert: Alert)
func showAlert(_ alert: BitwardenKit.Alert)
/// Shows the loading overlay view.
///
@ -141,7 +141,7 @@ extension Coordinator where Self: HasNavigator {
///
/// - Parameter alert: The alert to show.
///
func showAlert(_ alert: Alert) {
func showAlert(_ alert: BitwardenKit.Alert) {
navigator?.present(alert)
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
// MARK: - Alert + Settings

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import XCTest

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import SwiftUI
@ -9,7 +10,7 @@ enum AuthenticatorItemRoute: Equatable {
///
/// - Parameter alert: The alert to display.
///
case alert(_ alert: Alert)
case alert(_ alert: BitwardenKit.Alert)
/// A route to dismiss the screen currently presented modally.
///

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import UIKit

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import UIKit

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
extension Alert {

View File

@ -1,9 +1,29 @@
import BitwardenKit
import TestHelpers
import XCTest
open class BitwardenTestCase: BaseBitwardenTestCase {
@MainActor
override open class func setUp() {
// Apply default appearances for snapshot tests.
UI.applyDefaultAppearances()
TestDataHelpers.defaultBundle = Bundle(for: Self.self)
}
/// Executes any logic that should be applied before each test runs.
///
@MainActor
override open func setUp() {
super.setUp()
UI.animated = false
UI.sizeCategory = .large
}
/// Executes any logic that should be applied after each test runs.
///
override open func tearDown() {
super.tearDown()
UI.animated = false
}
}

View File

@ -1,10 +1,9 @@
import BitwardenKit
import BitwardenResources
import Foundation
// MARK: - Alert+Error
extension Alert {
public extension Alert {
// MARK: Methods
/// The default alert style for a given error with a standard ok button to dismiss.

View File

@ -9,22 +9,22 @@ public class Alert {
// MARK: Properties
/// A list of actions that the user can tap on in the alert.
var alertActions: [AlertAction] = []
public var alertActions: [AlertAction] = []
/// A list of text fields that the user can use to enter text.
var alertTextFields: [AlertTextField] = []
public var alertTextFields: [AlertTextField] = []
/// The message that is displayed in the alert.
let message: String?
public let message: String?
/// The preferred action for the user to take in the alert, which emphasis is given.
var preferredAction: AlertAction?
public var preferredAction: AlertAction?
/// The alert's style.
let preferredStyle: UIAlertController.Style
public let preferredStyle: UIAlertController.Style
/// The title of the message that is displayed at the top of the alert.
let title: String?
public let title: String?
// MARK: Initialization
@ -60,7 +60,7 @@ public class Alert {
/// - Returns: `self` to allow `add(_:)` methods to be chained.
///
@discardableResult
func add(_ action: AlertAction) -> Self {
public func add(_ action: AlertAction) -> Self {
alertActions.append(action)
return self
}
@ -72,7 +72,7 @@ public class Alert {
/// - Returns: `self` to allow `add(_:)` methods to be chained.
///
@discardableResult
func add(_ textField: AlertTextField) -> Self {
public func add(_ textField: AlertTextField) -> Self {
alertTextFields.append(textField)
return self
}
@ -86,7 +86,7 @@ public class Alert {
/// - Returns: `self` to allow `add(_:)` methods to be chained.
///
@discardableResult
func addPreferred(_ action: AlertAction) -> Self {
public func addPreferred(_ action: AlertAction) -> Self {
alertActions.append(action)
preferredAction = action
return self
@ -98,7 +98,7 @@ public class Alert {
/// - Returns An initialized `UIAlertController` that has the `AlertAction`s added.
///
@MainActor
func createAlertController(onDismissed: (() -> Void)? = nil) -> UIAlertController {
public func createAlertController(onDismissed: (() -> Void)? = nil) -> UIAlertController {
let alertController = AlertController(title: title, message: message, preferredStyle: preferredStyle)
alertController.onDismissed = onDismissed

View File

@ -9,16 +9,16 @@ public class AlertAction {
// MARK: Properties
/// An optional handler that is called when the user taps on the action from the alert.
let handler: ((AlertAction, [AlertTextField]) async -> Void)?
public let handler: ((AlertAction, [AlertTextField]) async -> Void)?
/// Condition that determines if the action should be enabled. Defaults to always enabled.
var shouldEnableAction: (([AlertTextField]) -> Bool)?
public var shouldEnableAction: (([AlertTextField]) -> Bool)?
/// The style of the action.
let style: UIAlertAction.Style
public let style: UIAlertAction.Style
/// The title of the alert action to display in the alert.
let title: String
public let title: String
// MARK: Initialization

View File

@ -1,8 +1,8 @@
import BitwardenKit
import BitwardenResources
import TestHelpers
import XCTest
@testable import BitwardenShared
@testable import BitwardenKit
class AlertErrorTests: BitwardenTestCase {
/// `defaultAlert(title:message:)` constructs an `Alert` with the title, message, and an OK button.
@ -17,10 +17,10 @@ class AlertErrorTests: BitwardenTestCase {
/// `defaultAlert(error:)` constructs an `Alert` with the title and message based on the error,
/// and an OK button.
func test_defaultAlertError() {
let subject = Alert.defaultAlert(error: StateServiceError.noActiveAccount)
let subject = Alert.defaultAlert(error: BitwardenTestError.example)
XCTAssertEqual(subject.title, Localizations.anErrorHasOccurred)
XCTAssertEqual(subject.message, StateServiceError.noActiveAccount.errorDescription)
XCTAssertEqual(subject.message, BitwardenTestError.example.errorDescription)
XCTAssertEqual(subject.alertActions, [AlertAction(title: Localizations.ok, style: .cancel)])
}

View File

@ -1,6 +1,7 @@
import BitwardenKitMocks
import XCTest
@testable import BitwardenShared
@testable import BitwardenKit
// MARK: - AlertPresentableTests
@ -14,7 +15,7 @@ class AlertPresentableTests: BitwardenTestCase {
override func setUp() {
super.setUp()
rootViewController = UIViewController()
rootViewController = MockUIViewController()
subject = AlertPresentableSubject()
subject.rootViewController = rootViewController
setKeyWindowRoot(viewController: rootViewController)

View File

@ -0,0 +1,92 @@
import BitwardenKitMocks
import BitwardenResources
import XCTest
@testable import BitwardenKit
// MARK: - AlertTests
class AlertTests: BitwardenTestCase {
// MARK: Properties
var subject: Alert!
// MARK: Setup and Teardown
override func setUp() {
super.setUp()
subject = Alert.fixture(alertActions: [AlertAction.cancel()],
alertTextFields: [AlertTextField.fixture(autocorrectionType: .no)])
.addPreferred(AlertAction.ok())
}
override func tearDown() {
super.tearDown()
subject = nil
}
// MARK: Tests
/// `createAlertController` returns a `UIAlertController` based on the alert details.
@MainActor
func test_createAlertController() throws {
let alertController = subject.createAlertController()
XCTAssertEqual(alertController.title, "🍎")
XCTAssertEqual(alertController.message, "🥝")
XCTAssertEqual(alertController.preferredStyle, .alert)
XCTAssertEqual(alertController.actions.count, 2)
XCTAssertEqual(alertController.actions[0].title, "Cancel")
XCTAssertEqual(alertController.actions[0].style, .cancel)
XCTAssertEqual(alertController.actions[1].title, "OK")
XCTAssertEqual(alertController.actions[1].style, .default)
XCTAssertEqual(alertController.textFields?.count, 1)
let textField = try XCTUnwrap(alertController.textFields?.first)
XCTAssertEqual(textField.text, "value")
XCTAssertEqual(textField.placeholder, "placeholder")
XCTAssertEqual(textField.autocapitalizationType, .allCharacters)
XCTAssertEqual(textField.autocorrectionType, .no)
XCTAssertEqual(textField.isSecureTextEntry, true)
XCTAssertEqual(textField.keyboardType, .numberPad)
XCTAssertEqual(alertController.preferredAction?.title, "OK")
}
/// `createAlertController` sets an `onDismissed` closure that's called when the alert is dismissed.
@MainActor
func test_createAlertController_onDismissed() {
var dismissedCalled = false
let alertController = subject.createAlertController { dismissedCalled = true }
let rootViewController = MockUIViewController()
setKeyWindowRoot(viewController: rootViewController)
rootViewController.present(alertController, animated: false)
XCTAssertFalse(dismissedCalled)
rootViewController.dismiss(animated: false)
waitFor(rootViewController.presentedViewController == nil)
XCTAssertTrue(dismissedCalled)
}
/// `debugDescription` contains the alert's properties
func test_debugDescription() {
XCTAssertEqual(
subject!.debugDescription,
"Alert(title: 🍎, message: 🥝, alertActions: [BitwardenKit.AlertAction, BitwardenKit.AlertAction],"
+ " alertTextFields: [BitwardenKit.AlertTextField])",
)
}
/// Alert conforms to `Equatable`.
func test_equatable() {
XCTAssertEqual(subject, Alert.fixture(alertActions: [AlertAction.cancel()])
.addPreferred(AlertAction.ok()))
XCTAssertNotEqual(subject, Alert.fixture(alertActions: [AlertAction.cancel(style: .destructive)])
.addPreferred(AlertAction.ok()))
XCTAssertNotEqual(subject, Alert(title: "🍎", message: "🥝", preferredStyle: .alert))
XCTAssertNotEqual(subject, Alert.fixture(alertActions: [AlertAction.cancel()])
.addPreferred(AlertAction.ok { _, _ in }))
XCTAssertEqual(subject, Alert.fixture(alertActions: [AlertAction.cancel()])
.addPreferred(AlertAction.ok()))
}
}

View File

@ -59,8 +59,16 @@ public class AlertTextField {
self.text = text ?? ""
}
/// Updates the text field's internal text value when the UITextField content changes.
///
/// This method is typically connected as a target-action for the `.editingChanged`
/// control event of a `UITextField`. It synchronizes the text field's content with
/// the internal `text` property and triggers the optional `onTextChanged` callback.
///
/// - Parameters:
/// - textField: The `UITextField` whose content has changed.
@objc
func textChanged(in textField: UITextField) {
public func textChanged(in textField: UITextField) {
text = textField.text ?? ""
onTextChanged?()
}

View File

@ -1,7 +1,7 @@
import BitwardenResources
import UIKit
@testable import BitwardenShared
@testable import BitwardenKit
enum AlertError: LocalizedError {
case alertActionNotFound(title: String)
@ -17,7 +17,7 @@ enum AlertError: LocalizedError {
}
}
extension Alert {
public extension Alert {
/// Simulates tapping the cancel button of the alert.
func tapCancel() async throws {
try await tapAction(title: Localizations.cancel)

View File

@ -0,0 +1,217 @@
import BitwardenKit
import UIKit
import XCTest
// MARK: - MockUIViewController
/// A mock UIViewController that can be used in tests that normally rely on the existence of a host app
/// because of details about how UIViewControllers present/dismiss other UIViewControllers.
public class MockUIViewController: UIViewController {
// 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: 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.
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 view 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 init(
nibName nibNameOrNil: String?,
bundle nibBundleOrNil: Bundle?,
) {
super.init(
nibName: nibNameOrNil,
bundle: nibBundleOrNil,
)
setUpMockHierarchy()
}
/// 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: MockUIViewController.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: MockUIViewController.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?()
}
// 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
popViewControllerCalled = false
}
// 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: MockUIViewController.mockWindowSize)
mockWindow?.rootViewController = self
// Create a mock view
mockView = UIView(frame: mockWindow?.frame ?? .zero)
view = mockView
}
}

View File

@ -1,4 +1,3 @@
import BitwardenKit
import Combine
import Foundation

View File

@ -3,7 +3,7 @@ import SnapshotTesting
import TestHelpers
import XCTest
@testable import BitwardenShared
@testable import BitwardenKit
class LoadingOverlayViewTests: BitwardenTestCase {
// MARK: Tests

View File

@ -2,7 +2,7 @@
import SnapshotTesting
import XCTest
@testable import BitwardenShared
@testable import BitwardenKit
class SectionViewTests: BitwardenTestCase {
// MARK: Tests

View File

@ -0,0 +1,73 @@
import UIKit
#if DEBUG
public extension Alert {
static func fixture(
title: String = "🍎",
message: String? = "🥝",
preferredStyle: UIAlertController.Style = .alert,
alertActions: [AlertAction] = [.ok()],
alertTextFields: [AlertTextField] = [.fixture()],
) -> Alert {
Alert(
title: title,
message: message,
preferredStyle: preferredStyle,
alertActions: alertActions,
alertTextFields: alertTextFields,
)
}
}
public extension AlertAction {
static func ok(
title: String = "OK",
style: UIAlertAction.Style = .default,
handler: ((AlertAction, [AlertTextField]) async -> Void)? = nil,
shouldEnableAction: (([AlertTextField]) -> Bool)? = nil,
) -> AlertAction {
AlertAction(
title: title,
style: style,
handler: handler,
shouldEnableAction: shouldEnableAction,
)
}
static func cancel(
title: String = "Cancel",
style: UIAlertAction.Style = .cancel,
handler: ((AlertAction, [AlertTextField]) async -> Void)? = nil,
shouldEnableAction: (([AlertTextField]) -> Bool)? = nil,
) -> AlertAction {
AlertAction(
title: title,
style: style,
handler: handler,
shouldEnableAction: shouldEnableAction,
)
}
}
public extension AlertTextField {
static func fixture(
id: String = "field",
autocapitalizationType: UITextAutocapitalizationType = .allCharacters,
autocorrectionType: UITextAutocorrectionType = .yes,
isSecureTextEntry: Bool = true,
keyboardType: UIKeyboardType = .numberPad,
placeholder: String? = "placeholder",
text: String = "value",
) -> AlertTextField {
AlertTextField(
id: id,
autocapitalizationType: autocapitalizationType,
autocorrectionType: autocorrectionType,
isSecureTextEntry: isSecureTextEntry,
keyboardType: keyboardType,
placeholder: placeholder,
text: text,
)
}
}
#endif

View File

@ -191,7 +191,7 @@ extension LandingProcessor: ProfileSwitcherHandler {
// No-Op for the landing processor.
}
func showAlert(_ alert: Alert) {
func showAlert(_ alert: BitwardenKit.Alert) {
coordinator.showAlert(alert)
}

View File

@ -68,7 +68,7 @@ protocol ProfileSwitcherHandler: AnyObject { // sourcery: AutoMockable
///
/// - Parameter alert: The alert to show.
///
func showAlert(_ alert: Alert)
func showAlert(_ alert: BitwardenKit.Alert)
/// Shows the profile switcher; this is used on iOS >=26 for displaying the sheet;
/// on iOS <26, `profileSwitcherState.isVisible` is used instead.

View File

@ -5,7 +5,7 @@ import BitwardenKitMocks
class MockProfileSwitcherHandlerProcessor:
MockProcessor<ProfileSwitcherState, ProfileSwitcherAction, ProfileSwitcherEffect>,
ProfileSwitcherHandler {
var alertsShown = [BitwardenShared.Alert]()
var alertsShown = [BitwardenKit.Alert]()
var allowLockAndLogout = true
var dismissProfileSwitcherCalled = false
var handleAuthEvents = [AuthEvent]()
@ -31,7 +31,7 @@ class MockProfileSwitcherHandlerProcessor:
func showAddAccount() {}
func showAlert(_ alert: BitwardenShared.Alert) {
func showAlert(_ alert: BitwardenKit.Alert) {
alertsShown.append(alert)
}

View File

@ -1,14 +1,15 @@
import BitwardenKit
import Foundation
import XCTest
@testable import BitwardenShared
class MockUserVerificationHelperDelegate: UserVerificationDelegate {
var alertShown = [Alert]()
var alertShownHandler: ((Alert) async throws -> Void)?
var alertShown = [BitwardenKit.Alert]()
var alertShownHandler: ((BitwardenKit.Alert) async throws -> Void)?
var alertOnDismissed: (() -> Void)?
func showAlert(_ alert: Alert) {
func showAlert(_ alert: BitwardenKit.Alert) {
alertShown.append(alert)
Task {
do {
@ -19,7 +20,7 @@ class MockUserVerificationHelperDelegate: UserVerificationDelegate {
}
}
func showAlert(_ alert: BitwardenShared.Alert, onDismissed: (() -> Void)?) {
func showAlert(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) {
showAlert(alert)
alertOnDismissed = onDismissed
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
@testable import BitwardenShared
class MockVaultUnlockSetupHelper: VaultUnlockSetupHelper {

View File

@ -226,7 +226,7 @@ protocol UserVerificationDelegate: AnyObject {
///
/// - Parameter alert: The alert to show.
///
func showAlert(_ alert: Alert)
func showAlert(_ alert: BitwardenKit.Alert)
/// Shows an alert to the user
///
@ -234,5 +234,5 @@ protocol UserVerificationDelegate: AnyObject {
/// - alert: The alert to show.
/// - onDismissed: An optional closure that is called when the alert is dismissed.
///
func showAlert(_ alert: Alert, onDismissed: (() -> Void)?)
func showAlert(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?)
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
// MARK: - VaultUnlockSetupHelper

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenKitMocks
import BitwardenResources
import TestHelpers

View File

@ -340,7 +340,7 @@ extension VaultUnlockProcessor: ProfileSwitcherHandler {
coordinator.navigate(to: .landing)
}
func showAlert(_ alert: Alert) {
func showAlert(_ alert: BitwardenKit.Alert) {
coordinator.showAlert(alert)
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import BitwardenSdk
import SwiftUI

View File

@ -1,6 +1,7 @@
// swiftlint:disable:this file_name
import AuthenticationServices
import BitwardenKit
import BitwardenKitMocks
import BitwardenSdk
import Foundation

View File

@ -735,7 +735,7 @@ extension AppProcessor: Fido2UserInterfaceHelperDelegate {
}
}
func showAlert(_ alert: Alert) {
func showAlert(_ alert: BitwardenKit.Alert) {
coordinator?.showAlert(alert)
}

View File

@ -1,5 +1,6 @@
// swiftlint:disable:this file_name
import BitwardenKit
import BitwardenResources
import XCTest

View File

@ -1,19 +0,0 @@
import XCTest
@testable import BitwardenShared
// MARK: - MockUIViewController
class MockUIViewController: UIViewController {
var presentCalled = false
var presentedView: UIViewController?
override func present(
_ viewControllerToPresent: UIViewController,
animated _: Bool,
completion _: (() -> Void)? = nil,
) {
presentCalled = true
presentedView = viewControllerToPresent
}
}

View File

@ -36,7 +36,7 @@ protocol Coordinator<Route, Event>: AnyObject {
///
/// - Parameter alert: The alert to show.
///
func showAlert(_ alert: Alert)
func showAlert(_ alert: BitwardenKit.Alert)
/// Shows the alert.
///
@ -44,7 +44,7 @@ protocol Coordinator<Route, Event>: AnyObject {
/// - alert: The alert to show.
/// - onDismissed: An optional closure that is called when the alert is dismissed.
///
func showAlert(_ alert: Alert, onDismissed: (() -> Void)?)
func showAlert(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?)
/// Shows an alert for an error that occurred.
///
@ -167,7 +167,7 @@ extension Coordinator {
///
/// - Parameter alert: The alert to show.
///
func showAlert(_ alert: Alert) {
func showAlert(_ alert: BitwardenKit.Alert) {
showAlert(alert, onDismissed: nil)
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenKitMocks
import BitwardenResources
import TestHelpers

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
// MARK: - Alert+Account

View File

@ -1,7 +1,8 @@
// MARK: - Alert+Account
import BitwardenKit
import BitwardenResources
// MARK: - Alert+Account
extension Alert {
// MARK: Methods

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import XCTest

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import XCTest

View File

@ -1,3 +1,4 @@
import BitwardenKit
import UIKit
#if DEBUG

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
// MARK: - Alert + Settings

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import XCTest

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenKitMocks
import BitwardenResources
import XCTest

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenKitMocks
import BitwardenResources
import BitwardenSdk

View File

@ -401,7 +401,7 @@ extension AddEditSendItemProcessor: ProfileSwitcherHandler {
// No-Op for the AddEditSendItemProcessor.
}
func showAlert(_ alert: Alert) {
func showAlert(_ alert: BitwardenKit.Alert) {
coordinator.showAlert(alert)
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import BitwardenSdk
import UIKit

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
import BitwardenSdk
import XCTest

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
@preconcurrency import BitwardenSdk

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenKitMocks
import BitwardenResources
import BitwardenSdk

View File

@ -1,7 +1,8 @@
import BitwardenKit
@testable import BitwardenShared
class MockTextAutofillHelperDelegate: TextAutofillHelperDelegate {
var alertsShown = [Alert]()
var alertsShown = [BitwardenKit.Alert]()
var alertOnDismissed: (() -> Void)?
var completeTextRequestText: String?
@ -9,7 +10,7 @@ class MockTextAutofillHelperDelegate: TextAutofillHelperDelegate {
completeTextRequestText = text
}
func showAlert(_ alert: BitwardenShared.Alert, onDismissed: (() -> Void)?) {
func showAlert(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) {
alertsShown.append(alert)
alertOnDismissed = onDismissed
}

View File

@ -31,7 +31,7 @@ protocol TextAutofillHelperDelegate: AnyObject {
/// - alert: The alert to show.
/// - onDismissed: An optional closure that is called when the alert is dismissed.
///
func showAlert(_ alert: Alert, onDismissed: (() -> Void)?)
func showAlert(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?)
}
@MainActor

View File

@ -1,6 +1,7 @@
// swiftlint:disable:this file_name
import AuthenticationServices
import BitwardenKit
import BitwardenKitMocks
import BitwardenResources
import BitwardenSdk

View File

@ -1,95 +1,15 @@
// swiftlint:disable:this file_name
import BitwardenKit
import BitwardenResources
import BitwardenSdk
import XCTest
@testable import BitwardenShared
// MARK: - AlertTests
class AlertTests: BitwardenTestCase {
// MARK: Properties
var subject: Alert!
// MARK: Setup and Teardown
override func setUp() {
super.setUp()
subject = Alert.fixture(alertActions: [AlertAction.cancel()],
alertTextFields: [AlertTextField.fixture(autocorrectionType: .no)])
.addPreferred(AlertAction.ok())
}
override func tearDown() {
super.tearDown()
subject = nil
}
// MARK: Tests
/// `createAlertController` returns a `UIAlertController` based on the alert details.
@MainActor
func test_createAlertController() throws {
let alertController = subject.createAlertController()
XCTAssertEqual(alertController.title, "🍎")
XCTAssertEqual(alertController.message, "🥝")
XCTAssertEqual(alertController.preferredStyle, .alert)
XCTAssertEqual(alertController.actions.count, 2)
XCTAssertEqual(alertController.actions[0].title, "Cancel")
XCTAssertEqual(alertController.actions[0].style, .cancel)
XCTAssertEqual(alertController.actions[1].title, "OK")
XCTAssertEqual(alertController.actions[1].style, .default)
XCTAssertEqual(alertController.textFields?.count, 1)
let textField = try XCTUnwrap(alertController.textFields?.first)
XCTAssertEqual(textField.text, "value")
XCTAssertEqual(textField.placeholder, "placeholder")
XCTAssertEqual(textField.autocapitalizationType, .allCharacters)
XCTAssertEqual(textField.autocorrectionType, .no)
XCTAssertEqual(textField.isSecureTextEntry, true)
XCTAssertEqual(textField.keyboardType, .numberPad)
XCTAssertEqual(alertController.preferredAction?.title, "OK")
}
/// `createAlertController` sets an `onDismissed` closure that's called when the alert is dismissed.
@MainActor
func test_createAlertController_onDismissed() {
var dismissedCalled = false
let alertController = subject.createAlertController { dismissedCalled = true }
let rootViewController = UIViewController()
setKeyWindowRoot(viewController: rootViewController)
rootViewController.present(alertController, animated: false)
XCTAssertFalse(dismissedCalled)
rootViewController.dismiss(animated: false)
waitFor(rootViewController.presentedViewController == nil)
XCTAssertTrue(dismissedCalled)
}
/// `debugDescription` contains the alert's properties
func test_debugDescription() {
XCTAssertEqual(
subject!.debugDescription,
"Alert(title: 🍎, message: 🥝, alertActions: [BitwardenShared.AlertAction, BitwardenShared.AlertAction],"
+ " alertTextFields: [BitwardenShared.AlertTextField])",
)
}
/// Alert conforms to `Equatable`.
func test_equatable() {
XCTAssertEqual(subject, Alert.fixture(alertActions: [AlertAction.cancel()])
.addPreferred(AlertAction.ok()))
XCTAssertNotEqual(subject, Alert.fixture(alertActions: [AlertAction.cancel(style: .destructive)])
.addPreferred(AlertAction.ok()))
XCTAssertNotEqual(subject, Alert(title: "🍎", message: "🥝", preferredStyle: .alert))
XCTAssertNotEqual(subject, Alert.fixture(alertActions: [AlertAction.cancel()])
.addPreferred(AlertAction.ok { _, _ in }))
XCTAssertEqual(subject, Alert.fixture(alertActions: [AlertAction.cancel()])
.addPreferred(AlertAction.ok()))
}
// MARK: - VaultListProcessor MoreOptions Tests
class VaultListProcessorMoreOptionsTests: BitwardenTestCase {
@MainActor
func test_vault_moreOptions_login_canViewPassword() async throws { // swiftlint:disable:this function_body_length
var capturedAction: MoreOptionsAction?

View File

@ -640,7 +640,7 @@ extension VaultListProcessor: ProfileSwitcherHandler {
coordinator.navigate(to: .addAccount)
}
func showAlert(_ alert: Alert) {
func showAlert(_ alert: BitwardenKit.Alert) {
coordinator.showAlert(alert)
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenResources
extension Alert {

View File

@ -8,7 +8,7 @@ enum MockCoordinatorError: Error {
}
class MockCoordinator<Route, Event>: Coordinator {
var alertShown = [Alert]()
var alertShown = [BitwardenKit.Alert]()
var contexts: [AnyObject?] = []
var events = [Event]()
var isLoadingOverlayShowing = false
@ -31,7 +31,7 @@ class MockCoordinator<Route, Event>: Coordinator {
contexts.append(context)
}
func showAlert(_ alert: Alert) {
func showAlert(_ alert: BitwardenKit.Alert) {
alertShown.append(alert)
}

View File

@ -1,4 +1,5 @@
import AuthenticatorShared
import BitwardenKit
import SwiftUI
final class MockStackNavigator: StackNavigator {
@ -22,7 +23,7 @@ final class MockStackNavigator: StackNavigator {
}
var actions: [NavigationAction] = []
var alerts: [AuthenticatorShared.Alert] = []
var alerts: [BitwardenKit.Alert] = []
var isEmpty = true
var isPresenting: Bool { actions.last?.type == .presented }
var rootViewController: UIViewController?
@ -67,7 +68,7 @@ final class MockStackNavigator: StackNavigator {
return viewControllersToPop
}
func present(_ alert: AuthenticatorShared.Alert) {
func present(_ alert: BitwardenKit.Alert) {
alerts.append(alert)
}

View File

@ -8,7 +8,7 @@ enum MockCoordinatorError: Error {
}
class MockCoordinator<Route, Event>: Coordinator {
var alertShown = [Alert]()
var alertShown = [BitwardenKit.Alert]()
var alertOnDismissed: (() -> Void)?
var contexts: [AnyObject?] = []
var errorAlertsShown = [Error]()
@ -34,7 +34,7 @@ class MockCoordinator<Route, Event>: Coordinator {
contexts.append(context)
}
func showAlert(_ alert: BitwardenShared.Alert, onDismissed: (() -> Void)?) {
func showAlert(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) {
alertShown.append(alert)
alertOnDismissed = onDismissed
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenShared
import UIKit

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenShared
import SwiftUI
@ -25,7 +26,7 @@ final class MockStackNavigator: StackNavigator {
var actions: [NavigationAction] = []
var alertOnDismissed: (() -> Void)?
var alerts: [BitwardenShared.Alert] = []
var alerts: [BitwardenKit.Alert] = []
var isEmpty = true
var isNavigationBarHidden = false
var isPresenting = false
@ -71,11 +72,11 @@ final class MockStackNavigator: StackNavigator {
return viewControllersToPop
}
func present(_ alert: BitwardenShared.Alert) {
func present(_ alert: BitwardenKit.Alert) {
alerts.append(alert)
}
func present(_ alert: BitwardenShared.Alert, onDismissed: (() -> Void)?) {
func present(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) {
alerts.append(alert)
alertOnDismissed = onDismissed
}

View File

@ -27,21 +27,4 @@ open class BitwardenTestCase: BaseBitwardenTestCase {
super.tearDown()
UI.animated = false
}
/// Nests a `UIView` within a root view controller in the test window. Allows testing
/// changes to the view that require the view to exist within a window or are dependent on safe
/// area layouts.
///
/// This is currently in the `BitwardenShared` copy of `BitwardenTestCase`
/// because it relies on `UIView.addConstrained(:)`, which is still in `BitwardenShared`.
///
/// - Parameters:
/// - view: The `UIView` to add to a root view controller.
///
open func setKeyWindowRoot(view: UIView) {
let viewController = UIViewController()
viewController.view.addConstrained(subview: view)
window.rootViewController = viewController
window.makeKeyAndVisible()
}
}