mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-12 18:30:41 -06:00
[PM-26060] Consolidate Alert to BitwardenKit (#2081)
This commit is contained in:
parent
87e3a223a8
commit
f90ffd8b16
@ -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)],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 {}
|
|
||||||
@ -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?
|
|
||||||
}
|
|
||||||
@ -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)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -60,7 +60,7 @@ open class AnyCoordinator<Route, Event>: Coordinator {
|
|||||||
doNavigate(route, context)
|
doNavigate(route, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
open func showAlert(_ alert: Alert) {
|
open func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
doShowAlert(alert)
|
doShowAlert(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ public protocol Coordinator<Route, Event>: AnyObject {
|
|||||||
///
|
///
|
||||||
/// - Parameter alert: The alert to show.
|
/// - Parameter alert: The alert to show.
|
||||||
///
|
///
|
||||||
func showAlert(_ alert: Alert)
|
func showAlert(_ alert: BitwardenKit.Alert)
|
||||||
|
|
||||||
/// Shows the loading overlay view.
|
/// Shows the loading overlay view.
|
||||||
///
|
///
|
||||||
@ -141,7 +141,7 @@ extension Coordinator where Self: HasNavigator {
|
|||||||
///
|
///
|
||||||
/// - Parameter alert: The alert to show.
|
/// - Parameter alert: The alert to show.
|
||||||
///
|
///
|
||||||
func showAlert(_ alert: Alert) {
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
navigator?.present(alert)
|
navigator?.present(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
|
|
||||||
// MARK: - Alert + Settings
|
// MARK: - Alert + Settings
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenSdk
|
import BitwardenSdk
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ enum AuthenticatorItemRoute: Equatable {
|
|||||||
///
|
///
|
||||||
/// - Parameter alert: The alert to display.
|
/// - Parameter alert: The alert to display.
|
||||||
///
|
///
|
||||||
case alert(_ alert: Alert)
|
case alert(_ alert: BitwardenKit.Alert)
|
||||||
|
|
||||||
/// A route to dismiss the screen currently presented modally.
|
/// A route to dismiss the screen currently presented modally.
|
||||||
///
|
///
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
|
|
||||||
extension Alert {
|
extension Alert {
|
||||||
|
|||||||
@ -1,9 +1,29 @@
|
|||||||
|
import BitwardenKit
|
||||||
import TestHelpers
|
import TestHelpers
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
open class BitwardenTestCase: BaseBitwardenTestCase {
|
open class BitwardenTestCase: BaseBitwardenTestCase {
|
||||||
@MainActor
|
@MainActor
|
||||||
override open class func setUp() {
|
override open class func setUp() {
|
||||||
|
// Apply default appearances for snapshot tests.
|
||||||
|
UI.applyDefaultAppearances()
|
||||||
|
|
||||||
TestDataHelpers.defaultBundle = Bundle(for: Self.self)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import BitwardenKit
|
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
// MARK: - Alert+Error
|
// MARK: - Alert+Error
|
||||||
|
|
||||||
extension Alert {
|
public extension Alert {
|
||||||
// MARK: Methods
|
// MARK: Methods
|
||||||
|
|
||||||
/// The default alert style for a given error with a standard ok button to dismiss.
|
/// The default alert style for a given error with a standard ok button to dismiss.
|
||||||
@ -9,22 +9,22 @@ public class Alert {
|
|||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
/// A list of actions that the user can tap on in the alert.
|
/// 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.
|
/// 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.
|
/// 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.
|
/// 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.
|
/// 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.
|
/// The title of the message that is displayed at the top of the alert.
|
||||||
let title: String?
|
public let title: String?
|
||||||
|
|
||||||
// MARK: Initialization
|
// MARK: Initialization
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ public class Alert {
|
|||||||
/// - Returns: `self` to allow `add(_:)` methods to be chained.
|
/// - Returns: `self` to allow `add(_:)` methods to be chained.
|
||||||
///
|
///
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func add(_ action: AlertAction) -> Self {
|
public func add(_ action: AlertAction) -> Self {
|
||||||
alertActions.append(action)
|
alertActions.append(action)
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
@ -72,7 +72,7 @@ public class Alert {
|
|||||||
/// - Returns: `self` to allow `add(_:)` methods to be chained.
|
/// - Returns: `self` to allow `add(_:)` methods to be chained.
|
||||||
///
|
///
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func add(_ textField: AlertTextField) -> Self {
|
public func add(_ textField: AlertTextField) -> Self {
|
||||||
alertTextFields.append(textField)
|
alertTextFields.append(textField)
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ public class Alert {
|
|||||||
/// - Returns: `self` to allow `add(_:)` methods to be chained.
|
/// - Returns: `self` to allow `add(_:)` methods to be chained.
|
||||||
///
|
///
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func addPreferred(_ action: AlertAction) -> Self {
|
public func addPreferred(_ action: AlertAction) -> Self {
|
||||||
alertActions.append(action)
|
alertActions.append(action)
|
||||||
preferredAction = action
|
preferredAction = action
|
||||||
return self
|
return self
|
||||||
@ -98,7 +98,7 @@ public class Alert {
|
|||||||
/// - Returns An initialized `UIAlertController` that has the `AlertAction`s added.
|
/// - Returns An initialized `UIAlertController` that has the `AlertAction`s added.
|
||||||
///
|
///
|
||||||
@MainActor
|
@MainActor
|
||||||
func createAlertController(onDismissed: (() -> Void)? = nil) -> UIAlertController {
|
public func createAlertController(onDismissed: (() -> Void)? = nil) -> UIAlertController {
|
||||||
let alertController = AlertController(title: title, message: message, preferredStyle: preferredStyle)
|
let alertController = AlertController(title: title, message: message, preferredStyle: preferredStyle)
|
||||||
alertController.onDismissed = onDismissed
|
alertController.onDismissed = onDismissed
|
||||||
|
|
||||||
@ -9,16 +9,16 @@ public class AlertAction {
|
|||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
/// An optional handler that is called when the user taps on the action from the alert.
|
/// 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.
|
/// 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.
|
/// 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.
|
/// The title of the alert action to display in the alert.
|
||||||
let title: String
|
public let title: String
|
||||||
|
|
||||||
// MARK: Initialization
|
// MARK: Initialization
|
||||||
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import BitwardenKit
|
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
|
import TestHelpers
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import BitwardenShared
|
@testable import BitwardenKit
|
||||||
|
|
||||||
class AlertErrorTests: BitwardenTestCase {
|
class AlertErrorTests: BitwardenTestCase {
|
||||||
/// `defaultAlert(title:message:)` constructs an `Alert` with the title, message, and an OK button.
|
/// `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,
|
/// `defaultAlert(error:)` constructs an `Alert` with the title and message based on the error,
|
||||||
/// and an OK button.
|
/// and an OK button.
|
||||||
func test_defaultAlertError() {
|
func test_defaultAlertError() {
|
||||||
let subject = Alert.defaultAlert(error: StateServiceError.noActiveAccount)
|
let subject = Alert.defaultAlert(error: BitwardenTestError.example)
|
||||||
|
|
||||||
XCTAssertEqual(subject.title, Localizations.anErrorHasOccurred)
|
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)])
|
XCTAssertEqual(subject.alertActions, [AlertAction(title: Localizations.ok, style: .cancel)])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import BitwardenKit
|
|
||||||
import OSLog
|
import OSLog
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
|
import BitwardenKitMocks
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import BitwardenShared
|
@testable import BitwardenKit
|
||||||
|
|
||||||
// MARK: - AlertPresentableTests
|
// MARK: - AlertPresentableTests
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ class AlertPresentableTests: BitwardenTestCase {
|
|||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
rootViewController = UIViewController()
|
rootViewController = MockUIViewController()
|
||||||
subject = AlertPresentableSubject()
|
subject = AlertPresentableSubject()
|
||||||
subject.rootViewController = rootViewController
|
subject.rootViewController = rootViewController
|
||||||
setKeyWindowRoot(viewController: rootViewController)
|
setKeyWindowRoot(viewController: rootViewController)
|
||||||
@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -59,8 +59,16 @@ public class AlertTextField {
|
|||||||
self.text = text ?? ""
|
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
|
@objc
|
||||||
func textChanged(in textField: UITextField) {
|
public func textChanged(in textField: UITextField) {
|
||||||
text = textField.text ?? ""
|
text = textField.text ?? ""
|
||||||
onTextChanged?()
|
onTextChanged?()
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@testable import BitwardenShared
|
@testable import BitwardenKit
|
||||||
|
|
||||||
enum AlertError: LocalizedError {
|
enum AlertError: LocalizedError {
|
||||||
case alertActionNotFound(title: String)
|
case alertActionNotFound(title: String)
|
||||||
@ -17,7 +17,7 @@ enum AlertError: LocalizedError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Alert {
|
public extension Alert {
|
||||||
/// Simulates tapping the cancel button of the alert.
|
/// Simulates tapping the cancel button of the alert.
|
||||||
func tapCancel() async throws {
|
func tapCancel() async throws {
|
||||||
try await tapAction(title: Localizations.cancel)
|
try await tapAction(title: Localizations.cancel)
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import BitwardenKit
|
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import SnapshotTesting
|
|||||||
import TestHelpers
|
import TestHelpers
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import BitwardenShared
|
@testable import BitwardenKit
|
||||||
|
|
||||||
class LoadingOverlayViewTests: BitwardenTestCase {
|
class LoadingOverlayViewTests: BitwardenTestCase {
|
||||||
// MARK: Tests
|
// MARK: Tests
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import SnapshotTesting
|
import SnapshotTesting
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import BitwardenShared
|
@testable import BitwardenKit
|
||||||
|
|
||||||
class SectionViewTests: BitwardenTestCase {
|
class SectionViewTests: BitwardenTestCase {
|
||||||
// MARK: Tests
|
// MARK: Tests
|
||||||
|
|||||||
73
BitwardenKit/UI/Platform/PreviewContent/Alert+Fixtures.swift
Normal file
73
BitwardenKit/UI/Platform/PreviewContent/Alert+Fixtures.swift
Normal 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
|
||||||
@ -191,7 +191,7 @@ extension LandingProcessor: ProfileSwitcherHandler {
|
|||||||
// No-Op for the landing processor.
|
// No-Op for the landing processor.
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAlert(_ alert: Alert) {
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
coordinator.showAlert(alert)
|
coordinator.showAlert(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,7 +68,7 @@ protocol ProfileSwitcherHandler: AnyObject { // sourcery: AutoMockable
|
|||||||
///
|
///
|
||||||
/// - Parameter alert: The alert to show.
|
/// - 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;
|
/// Shows the profile switcher; this is used on iOS >=26 for displaying the sheet;
|
||||||
/// on iOS <26, `profileSwitcherState.isVisible` is used instead.
|
/// on iOS <26, `profileSwitcherState.isVisible` is used instead.
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import BitwardenKitMocks
|
|||||||
class MockProfileSwitcherHandlerProcessor:
|
class MockProfileSwitcherHandlerProcessor:
|
||||||
MockProcessor<ProfileSwitcherState, ProfileSwitcherAction, ProfileSwitcherEffect>,
|
MockProcessor<ProfileSwitcherState, ProfileSwitcherAction, ProfileSwitcherEffect>,
|
||||||
ProfileSwitcherHandler {
|
ProfileSwitcherHandler {
|
||||||
var alertsShown = [BitwardenShared.Alert]()
|
var alertsShown = [BitwardenKit.Alert]()
|
||||||
var allowLockAndLogout = true
|
var allowLockAndLogout = true
|
||||||
var dismissProfileSwitcherCalled = false
|
var dismissProfileSwitcherCalled = false
|
||||||
var handleAuthEvents = [AuthEvent]()
|
var handleAuthEvents = [AuthEvent]()
|
||||||
@ -31,7 +31,7 @@ class MockProfileSwitcherHandlerProcessor:
|
|||||||
|
|
||||||
func showAddAccount() {}
|
func showAddAccount() {}
|
||||||
|
|
||||||
func showAlert(_ alert: BitwardenShared.Alert) {
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
alertsShown.append(alert)
|
alertsShown.append(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
|
import BitwardenKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import BitwardenShared
|
@testable import BitwardenShared
|
||||||
|
|
||||||
class MockUserVerificationHelperDelegate: UserVerificationDelegate {
|
class MockUserVerificationHelperDelegate: UserVerificationDelegate {
|
||||||
var alertShown = [Alert]()
|
var alertShown = [BitwardenKit.Alert]()
|
||||||
var alertShownHandler: ((Alert) async throws -> Void)?
|
var alertShownHandler: ((BitwardenKit.Alert) async throws -> Void)?
|
||||||
var alertOnDismissed: (() -> Void)?
|
var alertOnDismissed: (() -> Void)?
|
||||||
|
|
||||||
func showAlert(_ alert: Alert) {
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
alertShown.append(alert)
|
alertShown.append(alert)
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
@ -19,7 +20,7 @@ class MockUserVerificationHelperDelegate: UserVerificationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAlert(_ alert: BitwardenShared.Alert, onDismissed: (() -> Void)?) {
|
func showAlert(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) {
|
||||||
showAlert(alert)
|
showAlert(alert)
|
||||||
alertOnDismissed = onDismissed
|
alertOnDismissed = onDismissed
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
@testable import BitwardenShared
|
@testable import BitwardenShared
|
||||||
|
|
||||||
class MockVaultUnlockSetupHelper: VaultUnlockSetupHelper {
|
class MockVaultUnlockSetupHelper: VaultUnlockSetupHelper {
|
||||||
|
|||||||
@ -226,7 +226,7 @@ protocol UserVerificationDelegate: AnyObject {
|
|||||||
///
|
///
|
||||||
/// - Parameter alert: The alert to show.
|
/// - Parameter alert: The alert to show.
|
||||||
///
|
///
|
||||||
func showAlert(_ alert: Alert)
|
func showAlert(_ alert: BitwardenKit.Alert)
|
||||||
|
|
||||||
/// Shows an alert to the user
|
/// Shows an alert to the user
|
||||||
///
|
///
|
||||||
@ -234,5 +234,5 @@ protocol UserVerificationDelegate: AnyObject {
|
|||||||
/// - alert: The alert to show.
|
/// - alert: The alert to show.
|
||||||
/// - onDismissed: An optional closure that is called when the alert is dismissed.
|
/// - 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)?)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
|
|
||||||
// MARK: - VaultUnlockSetupHelper
|
// MARK: - VaultUnlockSetupHelper
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenKitMocks
|
import BitwardenKitMocks
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import TestHelpers
|
import TestHelpers
|
||||||
|
|||||||
@ -340,7 +340,7 @@ extension VaultUnlockProcessor: ProfileSwitcherHandler {
|
|||||||
coordinator.navigate(to: .landing)
|
coordinator.navigate(to: .landing)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAlert(_ alert: Alert) {
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
coordinator.showAlert(alert)
|
coordinator.showAlert(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import BitwardenSdk
|
import BitwardenSdk
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// swiftlint:disable:this file_name
|
// swiftlint:disable:this file_name
|
||||||
|
|
||||||
import AuthenticationServices
|
import AuthenticationServices
|
||||||
|
import BitwardenKit
|
||||||
import BitwardenKitMocks
|
import BitwardenKitMocks
|
||||||
import BitwardenSdk
|
import BitwardenSdk
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|||||||
@ -735,7 +735,7 @@ extension AppProcessor: Fido2UserInterfaceHelperDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAlert(_ alert: Alert) {
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
coordinator?.showAlert(alert)
|
coordinator?.showAlert(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// swiftlint:disable:this file_name
|
// swiftlint:disable:this file_name
|
||||||
|
|
||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -36,7 +36,7 @@ protocol Coordinator<Route, Event>: AnyObject {
|
|||||||
///
|
///
|
||||||
/// - Parameter alert: The alert to show.
|
/// - Parameter alert: The alert to show.
|
||||||
///
|
///
|
||||||
func showAlert(_ alert: Alert)
|
func showAlert(_ alert: BitwardenKit.Alert)
|
||||||
|
|
||||||
/// Shows the alert.
|
/// Shows the alert.
|
||||||
///
|
///
|
||||||
@ -44,7 +44,7 @@ protocol Coordinator<Route, Event>: AnyObject {
|
|||||||
/// - alert: The alert to show.
|
/// - alert: The alert to show.
|
||||||
/// - onDismissed: An optional closure that is called when the alert is dismissed.
|
/// - 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.
|
/// Shows an alert for an error that occurred.
|
||||||
///
|
///
|
||||||
@ -167,7 +167,7 @@ extension Coordinator {
|
|||||||
///
|
///
|
||||||
/// - Parameter alert: The alert to show.
|
/// - Parameter alert: The alert to show.
|
||||||
///
|
///
|
||||||
func showAlert(_ alert: Alert) {
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
showAlert(alert, onDismissed: nil)
|
showAlert(alert, onDismissed: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenKitMocks
|
import BitwardenKitMocks
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import TestHelpers
|
import TestHelpers
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
|
|
||||||
// MARK: - Alert+Account
|
// MARK: - Alert+Account
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
// MARK: - Alert+Account
|
import BitwardenKit
|
||||||
|
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
|
|
||||||
|
// MARK: - Alert+Account
|
||||||
|
|
||||||
extension Alert {
|
extension Alert {
|
||||||
// MARK: Methods
|
// MARK: Methods
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
|
|
||||||
// MARK: - Alert + Settings
|
// MARK: - Alert + Settings
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenKitMocks
|
import BitwardenKitMocks
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenKitMocks
|
import BitwardenKitMocks
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import BitwardenSdk
|
import BitwardenSdk
|
||||||
|
|||||||
@ -401,7 +401,7 @@ extension AddEditSendItemProcessor: ProfileSwitcherHandler {
|
|||||||
// No-Op for the AddEditSendItemProcessor.
|
// No-Op for the AddEditSendItemProcessor.
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAlert(_ alert: Alert) {
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
coordinator.showAlert(alert)
|
coordinator.showAlert(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import BitwardenSdk
|
import BitwardenSdk
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import BitwardenSdk
|
import BitwardenSdk
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
@preconcurrency import BitwardenSdk
|
@preconcurrency import BitwardenSdk
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenKitMocks
|
import BitwardenKitMocks
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import BitwardenSdk
|
import BitwardenSdk
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
|
import BitwardenKit
|
||||||
@testable import BitwardenShared
|
@testable import BitwardenShared
|
||||||
|
|
||||||
class MockTextAutofillHelperDelegate: TextAutofillHelperDelegate {
|
class MockTextAutofillHelperDelegate: TextAutofillHelperDelegate {
|
||||||
var alertsShown = [Alert]()
|
var alertsShown = [BitwardenKit.Alert]()
|
||||||
var alertOnDismissed: (() -> Void)?
|
var alertOnDismissed: (() -> Void)?
|
||||||
var completeTextRequestText: String?
|
var completeTextRequestText: String?
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ class MockTextAutofillHelperDelegate: TextAutofillHelperDelegate {
|
|||||||
completeTextRequestText = text
|
completeTextRequestText = text
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAlert(_ alert: BitwardenShared.Alert, onDismissed: (() -> Void)?) {
|
func showAlert(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) {
|
||||||
alertsShown.append(alert)
|
alertsShown.append(alert)
|
||||||
alertOnDismissed = onDismissed
|
alertOnDismissed = onDismissed
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ protocol TextAutofillHelperDelegate: AnyObject {
|
|||||||
/// - alert: The alert to show.
|
/// - alert: The alert to show.
|
||||||
/// - onDismissed: An optional closure that is called when the alert is dismissed.
|
/// - 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
|
@MainActor
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// swiftlint:disable:this file_name
|
// swiftlint:disable:this file_name
|
||||||
|
|
||||||
import AuthenticationServices
|
import AuthenticationServices
|
||||||
|
import BitwardenKit
|
||||||
import BitwardenKitMocks
|
import BitwardenKitMocks
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import BitwardenSdk
|
import BitwardenSdk
|
||||||
|
|||||||
@ -1,95 +1,15 @@
|
|||||||
|
// swiftlint:disable:this file_name
|
||||||
|
|
||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
import BitwardenSdk
|
import BitwardenSdk
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import BitwardenShared
|
@testable import BitwardenShared
|
||||||
|
|
||||||
// MARK: - AlertTests
|
// MARK: - VaultListProcessor MoreOptions Tests
|
||||||
|
|
||||||
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()))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class VaultListProcessorMoreOptionsTests: BitwardenTestCase {
|
||||||
@MainActor
|
@MainActor
|
||||||
func test_vault_moreOptions_login_canViewPassword() async throws { // swiftlint:disable:this function_body_length
|
func test_vault_moreOptions_login_canViewPassword() async throws { // swiftlint:disable:this function_body_length
|
||||||
var capturedAction: MoreOptionsAction?
|
var capturedAction: MoreOptionsAction?
|
||||||
@ -640,7 +640,7 @@ extension VaultListProcessor: ProfileSwitcherHandler {
|
|||||||
coordinator.navigate(to: .addAccount)
|
coordinator.navigate(to: .addAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAlert(_ alert: Alert) {
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
coordinator.showAlert(alert)
|
coordinator.showAlert(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenResources
|
import BitwardenResources
|
||||||
|
|
||||||
extension Alert {
|
extension Alert {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ enum MockCoordinatorError: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MockCoordinator<Route, Event>: Coordinator {
|
class MockCoordinator<Route, Event>: Coordinator {
|
||||||
var alertShown = [Alert]()
|
var alertShown = [BitwardenKit.Alert]()
|
||||||
var contexts: [AnyObject?] = []
|
var contexts: [AnyObject?] = []
|
||||||
var events = [Event]()
|
var events = [Event]()
|
||||||
var isLoadingOverlayShowing = false
|
var isLoadingOverlayShowing = false
|
||||||
@ -31,7 +31,7 @@ class MockCoordinator<Route, Event>: Coordinator {
|
|||||||
contexts.append(context)
|
contexts.append(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAlert(_ alert: Alert) {
|
func showAlert(_ alert: BitwardenKit.Alert) {
|
||||||
alertShown.append(alert)
|
alertShown.append(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import AuthenticatorShared
|
import AuthenticatorShared
|
||||||
|
import BitwardenKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class MockStackNavigator: StackNavigator {
|
final class MockStackNavigator: StackNavigator {
|
||||||
@ -22,7 +23,7 @@ final class MockStackNavigator: StackNavigator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var actions: [NavigationAction] = []
|
var actions: [NavigationAction] = []
|
||||||
var alerts: [AuthenticatorShared.Alert] = []
|
var alerts: [BitwardenKit.Alert] = []
|
||||||
var isEmpty = true
|
var isEmpty = true
|
||||||
var isPresenting: Bool { actions.last?.type == .presented }
|
var isPresenting: Bool { actions.last?.type == .presented }
|
||||||
var rootViewController: UIViewController?
|
var rootViewController: UIViewController?
|
||||||
@ -67,7 +68,7 @@ final class MockStackNavigator: StackNavigator {
|
|||||||
return viewControllersToPop
|
return viewControllersToPop
|
||||||
}
|
}
|
||||||
|
|
||||||
func present(_ alert: AuthenticatorShared.Alert) {
|
func present(_ alert: BitwardenKit.Alert) {
|
||||||
alerts.append(alert)
|
alerts.append(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ enum MockCoordinatorError: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MockCoordinator<Route, Event>: Coordinator {
|
class MockCoordinator<Route, Event>: Coordinator {
|
||||||
var alertShown = [Alert]()
|
var alertShown = [BitwardenKit.Alert]()
|
||||||
var alertOnDismissed: (() -> Void)?
|
var alertOnDismissed: (() -> Void)?
|
||||||
var contexts: [AnyObject?] = []
|
var contexts: [AnyObject?] = []
|
||||||
var errorAlertsShown = [Error]()
|
var errorAlertsShown = [Error]()
|
||||||
@ -34,7 +34,7 @@ class MockCoordinator<Route, Event>: Coordinator {
|
|||||||
contexts.append(context)
|
contexts.append(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAlert(_ alert: BitwardenShared.Alert, onDismissed: (() -> Void)?) {
|
func showAlert(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) {
|
||||||
alertShown.append(alert)
|
alertShown.append(alert)
|
||||||
alertOnDismissed = onDismissed
|
alertOnDismissed = onDismissed
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenShared
|
import BitwardenShared
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import BitwardenKit
|
||||||
import BitwardenShared
|
import BitwardenShared
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ final class MockStackNavigator: StackNavigator {
|
|||||||
|
|
||||||
var actions: [NavigationAction] = []
|
var actions: [NavigationAction] = []
|
||||||
var alertOnDismissed: (() -> Void)?
|
var alertOnDismissed: (() -> Void)?
|
||||||
var alerts: [BitwardenShared.Alert] = []
|
var alerts: [BitwardenKit.Alert] = []
|
||||||
var isEmpty = true
|
var isEmpty = true
|
||||||
var isNavigationBarHidden = false
|
var isNavigationBarHidden = false
|
||||||
var isPresenting = false
|
var isPresenting = false
|
||||||
@ -71,11 +72,11 @@ final class MockStackNavigator: StackNavigator {
|
|||||||
return viewControllersToPop
|
return viewControllersToPop
|
||||||
}
|
}
|
||||||
|
|
||||||
func present(_ alert: BitwardenShared.Alert) {
|
func present(_ alert: BitwardenKit.Alert) {
|
||||||
alerts.append(alert)
|
alerts.append(alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
func present(_ alert: BitwardenShared.Alert, onDismissed: (() -> Void)?) {
|
func present(_ alert: BitwardenKit.Alert, onDismissed: (() -> Void)?) {
|
||||||
alerts.append(alert)
|
alerts.append(alert)
|
||||||
alertOnDismissed = onDismissed
|
alertOnDismissed = onDismissed
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,21 +27,4 @@ open class BitwardenTestCase: BaseBitwardenTestCase {
|
|||||||
super.tearDown()
|
super.tearDown()
|
||||||
UI.animated = false
|
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user