mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 04:34:55 -06:00
[PM-18417] refactor: Pull BaseBitwardenTestCase into TestHelpers (#1379)
This commit is contained in:
parent
8c7ed26a01
commit
261dd54dc7
@ -162,6 +162,14 @@
|
||||
<key>Type</key>
|
||||
<string>PSChildPaneSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>File</key>
|
||||
<string>Acknowledgements/SwiftUI-Introspect</string>
|
||||
<key>Title</key>
|
||||
<string>swiftui-introspect</string>
|
||||
<key>Type</key>
|
||||
<string>PSChildPaneSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>File</key>
|
||||
<string>Acknowledgements/ViewInspector</string>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<key>FooterText</key>
|
||||
<string>The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Realm Inc.
|
||||
Copyright (c) 2025 The SwiftLint Contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreferenceSpecifiers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright 2019 Timber Software
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</string>
|
||||
<key>License</key>
|
||||
<string>MIT</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -1,10 +0,0 @@
|
||||
// MARK: - Optional
|
||||
|
||||
extension Optional where Wrapped: Collection {
|
||||
// MARK: Properties
|
||||
|
||||
/// Returns true if the value is `nil` or an empty collection.
|
||||
var isEmptyOrNil: Bool {
|
||||
self?.isEmpty ?? true
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
class OptionalTests: AuthenticatorTestCase {
|
||||
// MARK: Tests
|
||||
|
||||
/// `isEmptyOrNil` returns `true` if the wrapped collection is empty.
|
||||
func test_isEmptyOrNil_empty() {
|
||||
let subject: [String]? = []
|
||||
XCTAssertTrue(subject.isEmptyOrNil)
|
||||
}
|
||||
|
||||
/// `isEmptyOrNil` returns `true` if the value is `nil`.
|
||||
func test_isEmptyOrNil_nil() {
|
||||
let subject: [String]? = nil
|
||||
XCTAssertTrue(subject.isEmptyOrNil)
|
||||
}
|
||||
|
||||
/// `isEmptyOrNil` returns `false` if the wrapped collection is not empty.
|
||||
func test_isEmptyOrNil_notEmpty() {
|
||||
let subject: [String]? = ["a", "b", "c"]
|
||||
XCTAssertFalse(subject.isEmptyOrNil)
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - ItemListState
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
import TestHelpers
|
||||
import XCTest
|
||||
|
||||
open class BitwardenTestCase: BaseBitwardenTestCase {}
|
||||
@ -1,7 +1,7 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
public extension Data {
|
||||
/// Generates a hash value for the provided data.
|
||||
///
|
||||
/// - Parameter using: The type of cryptographically secure hashing being performed.
|
||||
@ -1,6 +1,6 @@
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
@testable import BitwardenKit
|
||||
|
||||
class DataTests: BitwardenTestCase {
|
||||
// MARK: Tests
|
||||
@ -1,6 +1,6 @@
|
||||
// MARK: - Optional where Wrapped: Collection
|
||||
|
||||
extension Optional where Wrapped: Collection {
|
||||
public extension Optional where Wrapped: Collection {
|
||||
// MARK: Properties
|
||||
|
||||
/// Returns true if the value is `nil` or an empty collection.
|
||||
@ -11,7 +11,7 @@ extension Optional where Wrapped: Collection {
|
||||
|
||||
// MARK: - Optional<String>
|
||||
|
||||
extension String? {
|
||||
public extension String? {
|
||||
// MARK: Properties
|
||||
|
||||
/// Returns true if the value is `nil`, an empty string or a string full of `.whitespacesAndNewlines`.
|
||||
@ -1,6 +1,6 @@
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
@testable import BitwardenKit
|
||||
|
||||
class OptionalTests: BitwardenTestCase {
|
||||
// MARK: Tests
|
||||
@ -1,7 +1,7 @@
|
||||
@testable import BitwardenShared
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
final class SequenceAsyncTests: BitwardenTestCase {
|
||||
/// `asyncMap` correctly maps each element.
|
||||
func test_asyncMap_success() async {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import Networking
|
||||
|
||||
@ -1,23 +1,12 @@
|
||||
import AuthenticatorShared
|
||||
import SnapshotTesting
|
||||
import SwiftUI
|
||||
import TestHelpers
|
||||
import XCTest
|
||||
|
||||
open class AuthenticatorTestCase: XCTestCase {
|
||||
/// The window being used for testing. Defaults to a new window with the same size as `UIScreen.main.bounds`.
|
||||
public var window: UIWindow!
|
||||
|
||||
open class AuthenticatorTestCase: BaseBitwardenTestCase {
|
||||
@MainActor
|
||||
override open class func setUp() {
|
||||
if UIDevice.current.name != "iPhone 16 Pro" || UIDevice.current.systemVersion != "18.1" {
|
||||
assertionFailure(
|
||||
"""
|
||||
Tests must be run using iOS 18.1 on an iPhone 16 Pro simulator.
|
||||
Snapshot tests depend on using the correct device.
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// Apply default appearances for snapshot tests.
|
||||
UI.applyDefaultAppearances()
|
||||
}
|
||||
@ -29,8 +18,6 @@ open class AuthenticatorTestCase: XCTestCase {
|
||||
super.setUp()
|
||||
UI.animated = false
|
||||
UI.sizeCategory = .large
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.layer.speed = 100
|
||||
}
|
||||
|
||||
/// Executes any logic that should be applied after each test runs.
|
||||
@ -38,234 +25,5 @@ open class AuthenticatorTestCase: XCTestCase {
|
||||
override open func tearDown() {
|
||||
super.tearDown()
|
||||
UI.animated = false
|
||||
window = nil
|
||||
}
|
||||
|
||||
/// Asserts that an asynchronous block of code will throw an error. The test will fail if the
|
||||
/// block does not throw an error.
|
||||
///
|
||||
/// - Note: This method does not rethrow the error thrown by `block`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - block: The block to be executed. This block is run asynchronously.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func assertAsyncThrows(
|
||||
_ block: () async throws -> Void,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
do {
|
||||
try await block()
|
||||
XCTFail("The block did not throw an error.", file: file, line: line)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/// Asserts that an asynchronous block of code will throw a specific error. The test will fail
|
||||
/// if the block does not throw an error or if the error thrown does not equal the provided error.
|
||||
///
|
||||
/// - Note: This method does not rethrow the error thrown by `block`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - error: The specific error that must be thrown by `block`.
|
||||
/// - block: The block to be executed. This block is run asynchronously.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func assertAsyncThrows<E: Error & Equatable>(
|
||||
error: E,
|
||||
_ block: () async throws -> Void,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
do {
|
||||
try await block()
|
||||
XCTFail("The block did not throw an error.", file: file, line: line)
|
||||
} catch let caughtError as E {
|
||||
XCTAssertEqual(caughtError, error, file: file, line: line)
|
||||
} catch let caughtError {
|
||||
XCTFail(
|
||||
"The error caught (\(caughtError)) does not match the type of error provided (\(error)).",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that an asynchronous block of code does not throw an error. The test will fail
|
||||
/// if the block throws an error.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - block: The block to be executed. This block is run asynchronously.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func assertAsyncDoesNotThrow(
|
||||
_ block: () async throws -> Void,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
do {
|
||||
try await block()
|
||||
} catch {
|
||||
XCTFail("The block threw an error.", file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a `UIViewController` the root view controller in the test window. Allows testing
|
||||
/// changes to the navigation stack when they would ordinarily be invisible to the testing
|
||||
/// environment.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewController: The `UIViewController` to make root view controller.
|
||||
///
|
||||
open func setKeyWindowRoot(viewController: UIViewController) {
|
||||
window.rootViewController = viewController
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// - 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()
|
||||
}
|
||||
|
||||
/// Wait for a condition to be true. The test will fail if the condition isn't met before the
|
||||
/// specified timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - condition: Return `true` to continue or `false` to keep waiting.
|
||||
/// - timeout: How long to wait before failing.
|
||||
/// - failureMessage: Message to display when the condition fails to be met.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func waitFor(
|
||||
_ condition: () -> Bool,
|
||||
timeout: TimeInterval = 10.0,
|
||||
failureMessage: String = "waitFor condition wasn't met within the time limit",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) {
|
||||
let start = Date()
|
||||
let limit = Date(timeIntervalSinceNow: timeout)
|
||||
|
||||
while !condition(), limit > Date() {
|
||||
let next = Date(timeIntervalSinceNow: 0.2)
|
||||
RunLoop.current.run(mode: RunLoop.Mode.default, before: next)
|
||||
}
|
||||
|
||||
// If the condition took more than 3 seconds to satisfy, add a warning to the logs to look into it.
|
||||
let elapsed = Date().timeIntervalSince(start)
|
||||
if elapsed > 3 {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.maximumFractionDigits = 3
|
||||
numberFormatter.minimumFractionDigits = 3
|
||||
numberFormatter.minimumIntegerDigits = 1
|
||||
let elapsedString: String = numberFormatter.string(from: NSNumber(value: elapsed)) ?? "nil"
|
||||
print("warning: \(name) line \(line) `waitFor` took \(elapsedString) seconds")
|
||||
}
|
||||
|
||||
XCTAssert(condition(), failureMessage, file: file, line: line)
|
||||
}
|
||||
|
||||
/// Wait for a condition to be true. The test will fail if the condition isn't met before the
|
||||
/// specified timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - condition: An expression that evaluates to `true` to continue or `false` to keep waiting.
|
||||
/// - timeout: How long to wait before failing.
|
||||
/// - failureMessage: Message to display when the condition fails to be met.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func waitFor(
|
||||
_ condition: @autoclosure () -> Bool,
|
||||
timeout: TimeInterval = 10.0,
|
||||
failureMessage: String = "waitFor condition wasn't met within the time limit",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) {
|
||||
waitFor(
|
||||
condition,
|
||||
timeout: timeout,
|
||||
failureMessage: failureMessage,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
/// Wait for a condition asynchronously to be true. The test will fail if the condition isn't met before the
|
||||
/// specified timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - condition: Return `true` to continue or `false` to keep waiting.
|
||||
/// - timeout: How long to wait before failing.
|
||||
/// - failureMessage: Message to display when the condition fails to be met.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func waitForAsync(
|
||||
_ condition: @escaping () -> Bool,
|
||||
timeout: TimeInterval = 10.0,
|
||||
failureMessage: String = "waitForAsync condition wasn't met within the time limit",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async throws {
|
||||
let start = Date()
|
||||
let limit = Date(timeIntervalSinceNow: timeout)
|
||||
|
||||
while !condition(), limit > Date() {
|
||||
try await Task.sleep(nanoseconds: 2 * 100_000_000)
|
||||
}
|
||||
|
||||
warnIfNeeded(start: start, line: line)
|
||||
|
||||
XCTAssert(condition(), failureMessage, file: file, line: line)
|
||||
}
|
||||
|
||||
/// Warns if `functionName` took more than `afterSeconds` to complete
|
||||
/// - Parameters:
|
||||
/// - start: When `waitFor` started
|
||||
/// - afterSeconds: The seconds that have passed since `start` to check against
|
||||
/// - functionName: The function name
|
||||
/// - line: File line were this was originated
|
||||
private func warnIfNeeded(
|
||||
start: Date,
|
||||
afterSeconds: Int = 3,
|
||||
functionName: String = #function,
|
||||
line: UInt = #line
|
||||
) {
|
||||
// If the condition took more than 3 seconds to satisfy, add a warning to the logs to look into it.
|
||||
let elapsed = Date().timeIntervalSince(start)
|
||||
if elapsed > 3 {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.maximumFractionDigits = 3
|
||||
numberFormatter.minimumFractionDigits = 3
|
||||
numberFormatter.minimumIntegerDigits = 1
|
||||
let elapsedString: String = numberFormatter.string(from: NSNumber(value: elapsed)) ?? "nil"
|
||||
print("warning: \(name) line \(line) `\(functionName)` took \(elapsedString) seconds")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +1,10 @@
|
||||
import BitwardenShared
|
||||
import SnapshotTesting
|
||||
import SwiftUI
|
||||
import TestHelpers
|
||||
import XCTest
|
||||
|
||||
open class BitwardenTestCase: XCTestCase {
|
||||
/// The window being used for testing. Defaults to a new window with the same size as `UIScreen.main.bounds`.
|
||||
public var window: UIWindow!
|
||||
|
||||
open class BitwardenTestCase: BaseBitwardenTestCase {
|
||||
@MainActor
|
||||
override open class func setUp() {
|
||||
if UIDevice.current.name != "iPhone 16 Pro" || UIDevice.current.systemVersion != "18.1" {
|
||||
assertionFailure(
|
||||
"""
|
||||
Tests must be run using iOS 18.1 on an iPhone 16 Pro simulator.
|
||||
Snapshot tests depend on using the correct device.
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// Apply default appearances for snapshot tests.
|
||||
UI.applyDefaultAppearances()
|
||||
}
|
||||
@ -29,8 +16,6 @@ open class BitwardenTestCase: XCTestCase {
|
||||
super.setUp()
|
||||
UI.animated = false
|
||||
UI.sizeCategory = .large
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.layer.speed = 100
|
||||
}
|
||||
|
||||
/// Executes any logic that should be applied after each test runs.
|
||||
@ -38,103 +23,15 @@ open class BitwardenTestCase: XCTestCase {
|
||||
override open func tearDown() {
|
||||
super.tearDown()
|
||||
UI.animated = false
|
||||
window = nil
|
||||
}
|
||||
|
||||
/// Asserts that an asynchronous block of code will throw an error. The test will fail if the
|
||||
/// block does not throw an error.
|
||||
///
|
||||
/// - Note: This method does not rethrow the error thrown by `block`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - block: The block to be executed. This block is run asynchronously.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func assertAsyncThrows(
|
||||
_ block: () async throws -> Void,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
do {
|
||||
try await block()
|
||||
XCTFail("The block did not throw an error.", file: file, line: line)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/// Asserts that an asynchronous block of code will throw a specific error. The test will fail
|
||||
/// if the block does not throw an error or if the error thrown does not equal the provided error.
|
||||
///
|
||||
/// - Note: This method does not rethrow the error thrown by `block`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - error: The specific error that must be thrown by `block`.
|
||||
/// - block: The block to be executed. This block is run asynchronously.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func assertAsyncThrows<E: Error & Equatable>(
|
||||
error: E,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
_ block: () async throws -> Void
|
||||
) async {
|
||||
do {
|
||||
try await block()
|
||||
XCTFail("The block did not throw an error.", file: file, line: line)
|
||||
} catch let caughtError as E {
|
||||
XCTAssertEqual(caughtError, error, file: file, line: line)
|
||||
} catch let caughtError {
|
||||
XCTFail(
|
||||
"The error caught (\(caughtError)) does not match the type of error provided (\(error)).",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that an asynchronous block of code does not throw an error. The test will fail
|
||||
/// if the block throws an error.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - block: The block to be executed. This block is run asynchronously.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func assertAsyncDoesNotThrow(
|
||||
_ block: () async throws -> Void,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
do {
|
||||
try await block()
|
||||
} catch {
|
||||
XCTFail("The block threw an error.", file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a `UIViewController` the root view controller in the test window. Allows testing
|
||||
/// changes to the navigation stack when they would ordinarily be invisible to the testing
|
||||
/// environment.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewController: The `UIViewController` to make root view controller.
|
||||
///
|
||||
open func setKeyWindowRoot(viewController: UIViewController) {
|
||||
window.rootViewController = viewController
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
@ -144,119 +41,4 @@ open class BitwardenTestCase: XCTestCase {
|
||||
window.rootViewController = viewController
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
/// Wait for a condition to be true. The test will fail if the condition isn't met before the
|
||||
/// specified timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - condition: Return `true` to continue or `false` to keep waiting.
|
||||
/// - timeout: How long to wait before failing.
|
||||
/// - failureMessage: Message to display when the condition fails to be met.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func waitFor(
|
||||
_ condition: () -> Bool,
|
||||
timeout: TimeInterval = 10.0,
|
||||
failureMessage: String = "waitFor condition wasn't met within the time limit",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) {
|
||||
let start = Date()
|
||||
let limit = Date(timeIntervalSinceNow: timeout)
|
||||
|
||||
while !condition(), limit > Date() {
|
||||
let next = Date(timeIntervalSinceNow: 0.2)
|
||||
RunLoop.current.run(mode: RunLoop.Mode.default, before: next)
|
||||
}
|
||||
|
||||
warnIfNeeded(start: start, line: line)
|
||||
|
||||
XCTAssert(condition(), failureMessage, file: file, line: line)
|
||||
}
|
||||
|
||||
/// Wait for a condition to be true. The test will fail if the condition isn't met before the
|
||||
/// specified timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - condition: An expression that evaluates to `true` to continue or `false` to keep waiting.
|
||||
/// - timeout: How long to wait before failing.
|
||||
/// - failureMessage: Message to display when the condition fails to be met.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func waitFor(
|
||||
_ condition: @autoclosure () -> Bool,
|
||||
timeout: TimeInterval = 10.0,
|
||||
failureMessage: String = "waitFor condition wasn't met within the time limit",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) {
|
||||
waitFor(
|
||||
condition,
|
||||
timeout: timeout,
|
||||
failureMessage: failureMessage,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
/// Wait for a condition asynchronously to be true. The test will fail if the condition isn't met before the
|
||||
/// specified timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - condition: Return `true` to continue or `false` to keep waiting.
|
||||
/// - timeout: How long to wait before failing.
|
||||
/// - failureMessage: Message to display when the condition fails to be met.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func waitForAsync(
|
||||
_ condition: @escaping () -> Bool,
|
||||
timeout: TimeInterval = 10.0,
|
||||
failureMessage: String = "waitForAsync condition wasn't met within the time limit",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async throws {
|
||||
let start = Date()
|
||||
let limit = Date(timeIntervalSinceNow: timeout)
|
||||
|
||||
while !condition(), limit > Date() {
|
||||
try await Task.sleep(nanoseconds: 2 * 100_000_000)
|
||||
}
|
||||
|
||||
warnIfNeeded(start: start, line: line)
|
||||
|
||||
XCTAssert(condition(), failureMessage, file: file, line: line)
|
||||
}
|
||||
|
||||
/// Warns if `functionName` took more than `afterSeconds` to complete
|
||||
/// - Parameters:
|
||||
/// - start: When `waitFor` started
|
||||
/// - afterSeconds: The seconds that have passed since `start` to check against
|
||||
/// - functionName: The function name
|
||||
/// - line: File line were this was originated
|
||||
private func warnIfNeeded(
|
||||
start: Date,
|
||||
afterSeconds: Int = 3,
|
||||
functionName: String = #function,
|
||||
line: UInt = #line
|
||||
) {
|
||||
// If the condition took more than 3 seconds to satisfy, add a warning to the logs to look into it.
|
||||
let elapsed = Date().timeIntervalSince(start)
|
||||
if elapsed > 3 {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.maximumFractionDigits = 3
|
||||
numberFormatter.minimumFractionDigits = 3
|
||||
numberFormatter.minimumIntegerDigits = 1
|
||||
let elapsedString: String = numberFormatter.string(from: NSNumber(value: elapsed)) ?? "nil"
|
||||
print("warning: \(name) line \(line) `\(functionName)` took \(elapsedString) seconds")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
239
TestHelpers/Support/BaseBitwardenTestCase.swift
Normal file
239
TestHelpers/Support/BaseBitwardenTestCase.swift
Normal file
@ -0,0 +1,239 @@
|
||||
import XCTest
|
||||
|
||||
open class BaseBitwardenTestCase: XCTestCase {
|
||||
/// The window being used for testing. Defaults to a new window with the same size as `UIScreen.main.bounds`.
|
||||
public var window: UIWindow!
|
||||
|
||||
@MainActor
|
||||
override open class func setUp() {
|
||||
if UIDevice.current.name != "iPhone 16 Pro" || UIDevice.current.systemVersion != "18.1" {
|
||||
assertionFailure(
|
||||
"""
|
||||
Tests must be run using iOS 18.1 on an iPhone 16 Pro simulator.
|
||||
Snapshot tests depend on using the correct device.
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes any logic that should be applied before each test runs.
|
||||
///
|
||||
@MainActor
|
||||
override open func setUp() {
|
||||
super.setUp()
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.layer.speed = 100
|
||||
}
|
||||
|
||||
/// Executes any logic that should be applied after each test runs.
|
||||
///
|
||||
override open func tearDown() {
|
||||
super.tearDown()
|
||||
window = nil
|
||||
}
|
||||
|
||||
/// Asserts that an asynchronous block of code will throw an error. The test will fail if the
|
||||
/// block does not throw an error.
|
||||
///
|
||||
/// - Note: This method does not rethrow the error thrown by `block`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - block: The block to be executed. This block is run asynchronously.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func assertAsyncThrows(
|
||||
_ block: () async throws -> Void,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
do {
|
||||
try await block()
|
||||
XCTFail("The block did not throw an error.", file: file, line: line)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/// Asserts that an asynchronous block of code will throw a specific error. The test will fail
|
||||
/// if the block does not throw an error or if the error thrown does not equal the provided error.
|
||||
///
|
||||
/// - Note: This method does not rethrow the error thrown by `block`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - error: The specific error that must be thrown by `block`.
|
||||
/// - block: The block to be executed. This block is run asynchronously.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func assertAsyncThrows<E: Error & Equatable>(
|
||||
error: E,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
_ block: () async throws -> Void
|
||||
) async {
|
||||
do {
|
||||
try await block()
|
||||
XCTFail("The block did not throw an error.", file: file, line: line)
|
||||
} catch let caughtError as E {
|
||||
XCTAssertEqual(caughtError, error, file: file, line: line)
|
||||
} catch let caughtError {
|
||||
XCTFail(
|
||||
"The error caught (\(caughtError)) does not match the type of error provided (\(error)).",
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that an asynchronous block of code does not throw an error. The test will fail
|
||||
/// if the block throws an error.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - block: The block to be executed. This block is run asynchronously.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func assertAsyncDoesNotThrow(
|
||||
_ block: () async throws -> Void,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async {
|
||||
do {
|
||||
try await block()
|
||||
} catch {
|
||||
XCTFail("The block threw an error.", file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a `UIViewController` the root view controller in the test window. Allows testing
|
||||
/// changes to the navigation stack when they would ordinarily be invisible to the testing
|
||||
/// environment.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - viewController: The `UIViewController` to make root view controller.
|
||||
///
|
||||
open func setKeyWindowRoot(viewController: UIViewController) {
|
||||
window.rootViewController = viewController
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
/// Wait for a condition to be true. The test will fail if the condition isn't met before the
|
||||
/// specified timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - condition: Return `true` to continue or `false` to keep waiting.
|
||||
/// - timeout: How long to wait before failing.
|
||||
/// - failureMessage: Message to display when the condition fails to be met.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func waitFor(
|
||||
_ condition: () -> Bool,
|
||||
timeout: TimeInterval = 10.0,
|
||||
failureMessage: String = "waitFor condition wasn't met within the time limit",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) {
|
||||
let start = Date()
|
||||
let limit = Date(timeIntervalSinceNow: timeout)
|
||||
|
||||
while !condition(), limit > Date() {
|
||||
let next = Date(timeIntervalSinceNow: 0.2)
|
||||
RunLoop.current.run(mode: RunLoop.Mode.default, before: next)
|
||||
}
|
||||
|
||||
warnIfNeeded(start: start, line: line)
|
||||
|
||||
XCTAssert(condition(), failureMessage, file: file, line: line)
|
||||
}
|
||||
|
||||
/// Wait for a condition to be true. The test will fail if the condition isn't met before the
|
||||
/// specified timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - condition: An expression that evaluates to `true` to continue or `false` to keep waiting.
|
||||
/// - timeout: How long to wait before failing.
|
||||
/// - failureMessage: Message to display when the condition fails to be met.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func waitFor(
|
||||
_ condition: @autoclosure () -> Bool,
|
||||
timeout: TimeInterval = 10.0,
|
||||
failureMessage: String = "waitFor condition wasn't met within the time limit",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) {
|
||||
waitFor(
|
||||
condition,
|
||||
timeout: timeout,
|
||||
failureMessage: failureMessage,
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
/// Wait for a condition asynchronously to be true. The test will fail if the condition isn't met before the
|
||||
/// specified timeout.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - condition: Return `true` to continue or `false` to keep waiting.
|
||||
/// - timeout: How long to wait before failing.
|
||||
/// - failureMessage: Message to display when the condition fails to be met.
|
||||
/// - file: The file in which the failure occurred. Defaults to the file name of the test
|
||||
/// case in which the function was called from.
|
||||
/// - line: The line number in which the failure occurred. Defaults to the line number on
|
||||
/// which this function was called from.
|
||||
///
|
||||
open func waitForAsync(
|
||||
_ condition: @escaping () -> Bool,
|
||||
timeout: TimeInterval = 10.0,
|
||||
failureMessage: String = "waitForAsync condition wasn't met within the time limit",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) async throws {
|
||||
let start = Date()
|
||||
let limit = Date(timeIntervalSinceNow: timeout)
|
||||
|
||||
while !condition(), limit > Date() {
|
||||
try await Task.sleep(nanoseconds: 2 * 100_000_000)
|
||||
}
|
||||
|
||||
warnIfNeeded(start: start, line: line)
|
||||
|
||||
XCTAssert(condition(), failureMessage, file: file, line: line)
|
||||
}
|
||||
|
||||
/// Warns if `functionName` took more than `afterSeconds` to complete
|
||||
/// - Parameters:
|
||||
/// - start: When `waitFor` started
|
||||
/// - afterSeconds: The seconds that have passed since `start` to check against
|
||||
/// - functionName: The function name
|
||||
/// - line: File line were this was originated
|
||||
private func warnIfNeeded(
|
||||
start: Date,
|
||||
afterSeconds: Int = 3,
|
||||
functionName: String = #function,
|
||||
line: UInt = #line
|
||||
) {
|
||||
// If the condition took more than 3 seconds to satisfy, add a warning to the logs to look into it.
|
||||
let elapsed = Date().timeIntervalSince(start)
|
||||
if elapsed > 3 {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.maximumFractionDigits = 3
|
||||
numberFormatter.minimumFractionDigits = 3
|
||||
numberFormatter.minimumIntegerDigits = 1
|
||||
let elapsedString: String = numberFormatter.string(from: NSNumber(value: elapsed)) ?? "nil"
|
||||
print("warning: \(name) line \(line) `\(functionName)` took \(elapsedString) seconds")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -58,6 +58,7 @@ schemes:
|
||||
- AuthenticatorTests
|
||||
- AuthenticatorSharedTests
|
||||
- BitwardenKit/AuthenticatorBridgeKitTests
|
||||
- BitwardenKit/BitwardenKitTests
|
||||
- BitwardenKit/NetworkingTests
|
||||
AuthenticatorShared:
|
||||
build:
|
||||
|
||||
@ -127,6 +127,7 @@ targets:
|
||||
- "**/TestHelpers/*"
|
||||
dependencies:
|
||||
- target: BitwardenKit
|
||||
- target: TestHelpers
|
||||
randomExecutionOrder: true
|
||||
Networking:
|
||||
type: framework
|
||||
|
||||
@ -71,6 +71,7 @@ schemes:
|
||||
- BitwardenShareExtensionTests
|
||||
- BitwardenSharedTests
|
||||
- BitwardenKit/AuthenticatorBridgeKitTests
|
||||
- BitwardenKit/BitwardenKitTests
|
||||
- BitwardenKit/NetworkingTests
|
||||
BitwardenActionExtension:
|
||||
build:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user