From 261dd54dc70488ecedc4e0b0afbb0ba670e801d9 Mon Sep 17 00:00:00 2001 From: Katherine Bertelsen Date: Wed, 5 Mar 2025 13:38:56 -0600 Subject: [PATCH] [PM-18417] refactor: Pull BaseBitwardenTestCase into TestHelpers (#1379) --- .../Settings.bundle/Acknowledgements.plist | 8 + .../Acknowledgements/SwiftLint.plist | 2 +- .../Acknowledgements/SwiftUI-Introspect.plist | 23 ++ .../Core/Platform/Extentions/Optional.swift | 10 - .../Platform/Extentions/OptionalTests.swift | 25 -- .../ItemList/ItemList/ItemListState.swift | 1 + .../Support/BitwardenTestCase.swift | 4 + .../Core/Platform/Extensions/Data.swift | 2 +- .../Core/Platform/Extensions/DataTests.swift | 2 +- .../Core/Platform}/Extensions/Optional.swift | 4 +- .../Platform}/Extensions/OptionalTests.swift | 2 +- .../Extensions/SequenceAsyncTests.swift | 4 +- .../API/Account/AccountAPIService.swift | 1 + .../Support/AuthenticatorTestCase.swift | 246 +----------------- .../Support/BitwardenTestCase.swift | 228 +--------------- .../Support/BaseBitwardenTestCase.swift | 239 +++++++++++++++++ project-bwa.yml | 1 + project-bwk.yml | 1 + project-pm.yml | 1 + 19 files changed, 294 insertions(+), 510 deletions(-) create mode 100644 Authenticator/Application/Support/Settings.bundle/Acknowledgements/SwiftUI-Introspect.plist delete mode 100644 AuthenticatorShared/Core/Platform/Extentions/Optional.swift delete mode 100644 AuthenticatorShared/Core/Platform/Extentions/OptionalTests.swift create mode 100644 BitwardenKit/Application/TestHelpers/Support/BitwardenTestCase.swift rename {BitwardenShared => BitwardenKit}/Core/Platform/Extensions/Data.swift (98%) rename {BitwardenShared => BitwardenKit}/Core/Platform/Extensions/DataTests.swift (92%) rename {BitwardenShared/UI/Platform/Application => BitwardenKit/Core/Platform}/Extensions/Optional.swift (94%) rename {BitwardenShared/UI/Platform/Application => BitwardenKit/Core/Platform}/Extensions/OptionalTests.swift (99%) create mode 100644 TestHelpers/Support/BaseBitwardenTestCase.swift diff --git a/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist b/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist index 1eb26ce5c..abcc906cf 100644 --- a/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist +++ b/Authenticator/Application/Support/Settings.bundle/Acknowledgements.plist @@ -162,6 +162,14 @@ Type PSChildPaneSpecifier + + File + Acknowledgements/SwiftUI-Introspect + Title + swiftui-introspect + Type + PSChildPaneSpecifier + File Acknowledgements/ViewInspector diff --git a/Authenticator/Application/Support/Settings.bundle/Acknowledgements/SwiftLint.plist b/Authenticator/Application/Support/Settings.bundle/Acknowledgements/SwiftLint.plist index 83857c45b..2d9813e23 100644 --- a/Authenticator/Application/Support/Settings.bundle/Acknowledgements/SwiftLint.plist +++ b/Authenticator/Application/Support/Settings.bundle/Acknowledgements/SwiftLint.plist @@ -8,7 +8,7 @@ FooterText 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 diff --git a/Authenticator/Application/Support/Settings.bundle/Acknowledgements/SwiftUI-Introspect.plist b/Authenticator/Application/Support/Settings.bundle/Acknowledgements/SwiftUI-Introspect.plist new file mode 100644 index 000000000..acf6cf6f0 --- /dev/null +++ b/Authenticator/Application/Support/Settings.bundle/Acknowledgements/SwiftUI-Introspect.plist @@ -0,0 +1,23 @@ + + + + + PreferenceSpecifiers + + + FooterText + 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. + License + MIT + Type + PSGroupSpecifier + + + + diff --git a/AuthenticatorShared/Core/Platform/Extentions/Optional.swift b/AuthenticatorShared/Core/Platform/Extentions/Optional.swift deleted file mode 100644 index 2ac83b348..000000000 --- a/AuthenticatorShared/Core/Platform/Extentions/Optional.swift +++ /dev/null @@ -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 - } -} diff --git a/AuthenticatorShared/Core/Platform/Extentions/OptionalTests.swift b/AuthenticatorShared/Core/Platform/Extentions/OptionalTests.swift deleted file mode 100644 index 5ce09d702..000000000 --- a/AuthenticatorShared/Core/Platform/Extentions/OptionalTests.swift +++ /dev/null @@ -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) - } -} diff --git a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListState.swift b/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListState.swift index eafba0da7..a3a7165b3 100644 --- a/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListState.swift +++ b/AuthenticatorShared/UI/Vault/ItemList/ItemList/ItemListState.swift @@ -1,3 +1,4 @@ +import BitwardenKit import Foundation // MARK: - ItemListState diff --git a/BitwardenKit/Application/TestHelpers/Support/BitwardenTestCase.swift b/BitwardenKit/Application/TestHelpers/Support/BitwardenTestCase.swift new file mode 100644 index 000000000..3a2861080 --- /dev/null +++ b/BitwardenKit/Application/TestHelpers/Support/BitwardenTestCase.swift @@ -0,0 +1,4 @@ +import TestHelpers +import XCTest + +open class BitwardenTestCase: BaseBitwardenTestCase {} diff --git a/BitwardenShared/Core/Platform/Extensions/Data.swift b/BitwardenKit/Core/Platform/Extensions/Data.swift similarity index 98% rename from BitwardenShared/Core/Platform/Extensions/Data.swift rename to BitwardenKit/Core/Platform/Extensions/Data.swift index 7915352e9..64061e329 100644 --- a/BitwardenShared/Core/Platform/Extensions/Data.swift +++ b/BitwardenKit/Core/Platform/Extensions/Data.swift @@ -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. diff --git a/BitwardenShared/Core/Platform/Extensions/DataTests.swift b/BitwardenKit/Core/Platform/Extensions/DataTests.swift similarity index 92% rename from BitwardenShared/Core/Platform/Extensions/DataTests.swift rename to BitwardenKit/Core/Platform/Extensions/DataTests.swift index 709ce76e5..8154897ce 100644 --- a/BitwardenShared/Core/Platform/Extensions/DataTests.swift +++ b/BitwardenKit/Core/Platform/Extensions/DataTests.swift @@ -1,6 +1,6 @@ import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class DataTests: BitwardenTestCase { // MARK: Tests diff --git a/BitwardenShared/UI/Platform/Application/Extensions/Optional.swift b/BitwardenKit/Core/Platform/Extensions/Optional.swift similarity index 94% rename from BitwardenShared/UI/Platform/Application/Extensions/Optional.swift rename to BitwardenKit/Core/Platform/Extensions/Optional.swift index 7415e5707..2b8fa7db4 100644 --- a/BitwardenShared/UI/Platform/Application/Extensions/Optional.swift +++ b/BitwardenKit/Core/Platform/Extensions/Optional.swift @@ -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 -extension String? { +public extension String? { // MARK: Properties /// Returns true if the value is `nil`, an empty string or a string full of `.whitespacesAndNewlines`. diff --git a/BitwardenShared/UI/Platform/Application/Extensions/OptionalTests.swift b/BitwardenKit/Core/Platform/Extensions/OptionalTests.swift similarity index 99% rename from BitwardenShared/UI/Platform/Application/Extensions/OptionalTests.swift rename to BitwardenKit/Core/Platform/Extensions/OptionalTests.swift index 46d76af58..9de6ae4eb 100644 --- a/BitwardenShared/UI/Platform/Application/Extensions/OptionalTests.swift +++ b/BitwardenKit/Core/Platform/Extensions/OptionalTests.swift @@ -1,6 +1,6 @@ import XCTest -@testable import BitwardenShared +@testable import BitwardenKit class OptionalTests: BitwardenTestCase { // MARK: Tests diff --git a/BitwardenShared/Core/Auth/Repositories/Extensions/SequenceAsyncTests.swift b/BitwardenShared/Core/Auth/Repositories/Extensions/SequenceAsyncTests.swift index d10d8bef9..f99efd9e2 100644 --- a/BitwardenShared/Core/Auth/Repositories/Extensions/SequenceAsyncTests.swift +++ b/BitwardenShared/Core/Auth/Repositories/Extensions/SequenceAsyncTests.swift @@ -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 { diff --git a/BitwardenShared/Core/Auth/Services/API/Account/AccountAPIService.swift b/BitwardenShared/Core/Auth/Services/API/Account/AccountAPIService.swift index e508bee06..99c18d4ee 100644 --- a/BitwardenShared/Core/Auth/Services/API/Account/AccountAPIService.swift +++ b/BitwardenShared/Core/Auth/Services/API/Account/AccountAPIService.swift @@ -1,3 +1,4 @@ +import BitwardenKit import CryptoKit import Foundation import Networking diff --git a/GlobalTestHelpers-bwa/Support/AuthenticatorTestCase.swift b/GlobalTestHelpers-bwa/Support/AuthenticatorTestCase.swift index a694349ab..c6f32bec0 100644 --- a/GlobalTestHelpers-bwa/Support/AuthenticatorTestCase.swift +++ b/GlobalTestHelpers-bwa/Support/AuthenticatorTestCase.swift @@ -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( - 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") - } } } diff --git a/GlobalTestHelpers/Support/BitwardenTestCase.swift b/GlobalTestHelpers/Support/BitwardenTestCase.swift index 7db932186..3a7a6ca60 100644 --- a/GlobalTestHelpers/Support/BitwardenTestCase.swift +++ b/GlobalTestHelpers/Support/BitwardenTestCase.swift @@ -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( - 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") - } - } } diff --git a/TestHelpers/Support/BaseBitwardenTestCase.swift b/TestHelpers/Support/BaseBitwardenTestCase.swift new file mode 100644 index 000000000..3d8fbf95a --- /dev/null +++ b/TestHelpers/Support/BaseBitwardenTestCase.swift @@ -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( + 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") + } + } +} diff --git a/project-bwa.yml b/project-bwa.yml index dc1c84caa..3b2f9414a 100644 --- a/project-bwa.yml +++ b/project-bwa.yml @@ -58,6 +58,7 @@ schemes: - AuthenticatorTests - AuthenticatorSharedTests - BitwardenKit/AuthenticatorBridgeKitTests + - BitwardenKit/BitwardenKitTests - BitwardenKit/NetworkingTests AuthenticatorShared: build: diff --git a/project-bwk.yml b/project-bwk.yml index b4d74da67..b7b76af1f 100644 --- a/project-bwk.yml +++ b/project-bwk.yml @@ -127,6 +127,7 @@ targets: - "**/TestHelpers/*" dependencies: - target: BitwardenKit + - target: TestHelpers randomExecutionOrder: true Networking: type: framework diff --git a/project-pm.yml b/project-pm.yml index 4467808c6..033b76992 100644 --- a/project-pm.yml +++ b/project-pm.yml @@ -71,6 +71,7 @@ schemes: - BitwardenShareExtensionTests - BitwardenSharedTests - BitwardenKit/AuthenticatorBridgeKitTests + - BitwardenKit/BitwardenKitTests - BitwardenKit/NetworkingTests BitwardenActionExtension: build: