mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 00:42:29 -06:00
[PM-27049] Initial version of the Test Harness app (#2142)
This commit is contained in:
parent
820865c290
commit
2046d8a6ed
3
Bitwarden.xcworkspace/contents.xcworkspacedata
generated
3
Bitwarden.xcworkspace/contents.xcworkspacedata
generated
@ -10,4 +10,7 @@
|
||||
<FileRef
|
||||
location = "group:BitwardenKit.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:TestHarness.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
19
Configs/Common-bwth.xcconfig
Normal file
19
Configs/Common-bwth.xcconfig
Normal file
@ -0,0 +1,19 @@
|
||||
CODE_SIGN_STYLE = Automatic
|
||||
DEVELOPMENT_TEAM = LTZ2PFU5D6
|
||||
ORGANIZATION_IDENTIFIER = com.bitwarden
|
||||
BASE_BUNDLE_ID = $(ORGANIZATION_IDENTIFIER).testharness
|
||||
SHARED_APP_GROUP_IDENTIFIER = group.${ORGANIZATION_IDENTIFIER}.testharness
|
||||
APPICON_NAME = AppIcon
|
||||
|
||||
// The above code signing settings can be overriden by adding a Local-bwth.xcconfig
|
||||
// file in the Configs directory.
|
||||
//
|
||||
// As an example, add the file Local-bwth.xcconfig with the following contents:
|
||||
//
|
||||
// DEVELOPMENT_TEAM = <Your team ID>
|
||||
// ORGANIZATION_IDENTIFIER = <Your reversed domain name>
|
||||
// PROVISIONING_PROFILE_SPECIFIER = <Optional provisioning profile specifier for the main app>
|
||||
//
|
||||
// This should allow Xcode to build the application based on these settings without
|
||||
// code signing errors or having to modify the project itself.
|
||||
//
|
||||
8
Configs/TestHarness-Debug.xcconfig
Normal file
8
Configs/TestHarness-Debug.xcconfig
Normal file
@ -0,0 +1,8 @@
|
||||
#include "./Common-bwth.xcconfig"
|
||||
#include "./Base-Debug.xcconfig"
|
||||
#include? "./Local-bwth.xcconfig"
|
||||
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
||||
CODE_SIGN_ENTITLEMENTS = TestHarness/Application/Support/Entitlements/TestHarness.entitlements
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_ID)
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) PREVIEWS $(BITWARDEN_FLAGS)
|
||||
8
Configs/TestHarness-Release.xcconfig
Normal file
8
Configs/TestHarness-Release.xcconfig
Normal file
@ -0,0 +1,8 @@
|
||||
#include "./Common-bwth.xcconfig"
|
||||
#include "./Base-Release.xcconfig"
|
||||
#include? "./Local-bwth.xcconfig"
|
||||
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
||||
CODE_SIGN_ENTITLEMENTS = TestHarness/Application/Support/Entitlements/TestHarness.entitlements
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_ID)
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) $(BITWARDEN_FLAGS)
|
||||
6
Configs/TestHarnessShared-Debug.xcconfig
Normal file
6
Configs/TestHarnessShared-Debug.xcconfig
Normal file
@ -0,0 +1,6 @@
|
||||
#include "./Common-bwth.xcconfig"
|
||||
#include "./Base-Debug.xcconfig"
|
||||
#include? "./Local-bwth.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_ID).testharness-shared
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) $(BITWARDEN_FLAGS)
|
||||
6
Configs/TestHarnessShared-Release.xcconfig
Normal file
6
Configs/TestHarnessShared-Release.xcconfig
Normal file
@ -0,0 +1,6 @@
|
||||
#include "./Common-bwth.xcconfig"
|
||||
#include "./Base-Release.xcconfig"
|
||||
#include? "./Local-bwth.xcconfig"
|
||||
|
||||
PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_ID).testharness-shared
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) $(BITWARDEN_FLAGS)
|
||||
5
GlobalTestHelpers-bwth/GlobalTestHelpers.swift
Normal file
5
GlobalTestHelpers-bwth/GlobalTestHelpers.swift
Normal file
@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
/// Global test helpers for TestHarness tests.
|
||||
///
|
||||
/// This file provides shared testing utilities used across the TestHarness test suite.
|
||||
40
README-bwth.md
Normal file
40
README-bwth.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Test Harness
|
||||
|
||||
A playground application for testing and demonstrating various Bitwarden iOS features and flows.
|
||||
|
||||
## Overview
|
||||
|
||||
The Test Harness app provides a simple interface to trigger and test different scenarios within the Bitwarden iOS ecosystem, including:
|
||||
|
||||
- Password Autofill flows
|
||||
- Passkey Autofill flows (coming soon)
|
||||
- Passkey Creation flows (coming soon)
|
||||
|
||||
## Purpose
|
||||
|
||||
This app is designed for:
|
||||
- Manual testing of specific flows
|
||||
- Demonstrating feature functionality
|
||||
- Debugging and development
|
||||
- Integration testing scenarios
|
||||
|
||||
## Structure
|
||||
|
||||
The app follows the same architectural patterns as the main Bitwarden apps:
|
||||
- Coordinator-based navigation
|
||||
- Processor/State/Action/Effect pattern for views
|
||||
- Service container for dependency injection
|
||||
- Sourcery for mock generation
|
||||
|
||||
## Building
|
||||
|
||||
The Test Harness is part of the main iOS workspace. To build:
|
||||
|
||||
1. Run `./Scripts/bootstrap.sh` to generate the Xcode project
|
||||
2. Open `Bitwarden.xcworkspace`
|
||||
3. Select the `TestHarness` scheme
|
||||
4. Build and run
|
||||
|
||||
## Note
|
||||
|
||||
This is a development/testing tool and is not intended for production use or App Store distribution.
|
||||
@ -13,6 +13,7 @@ repo_root=$(dirname "$script_dir")
|
||||
mint run xcodegen --spec "$repo_root/project-bwk.yml"
|
||||
mint run xcodegen --spec "$repo_root/project-pm.yml"
|
||||
mint run xcodegen --spec "$repo_root/project-bwa.yml"
|
||||
mint run xcodegen --spec "$repo_root/project-bwth.yml"
|
||||
echo "✅ Bootstrapped!"
|
||||
|
||||
# Check Xcode version matches .xcode-version
|
||||
|
||||
41
TestHarness/Application/AppDelegate.swift
Normal file
41
TestHarness/Application/AppDelegate.swift
Normal file
@ -0,0 +1,41 @@
|
||||
import BitwardenKit
|
||||
import TestHarnessShared
|
||||
import UIKit
|
||||
|
||||
/// A protocol for an `AppDelegate` that can be used by the `SceneDelegate` to look up the
|
||||
/// `AppDelegate` when the app is running (`AppDelegate`) or testing (`TestingAppDelegate`).
|
||||
protocol AppDelegateType: AnyObject {
|
||||
/// The processor that manages application level logic.
|
||||
var appProcessor: AppProcessor? { get }
|
||||
|
||||
/// Whether the app is running for unit tests.
|
||||
var isTesting: Bool { get }
|
||||
}
|
||||
|
||||
/// The app's `UIApplicationDelegate` which serves as the entry point into the app.
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, AppDelegateType {
|
||||
// MARK: Properties
|
||||
|
||||
/// The processor that manages application level logic.
|
||||
var appProcessor: AppProcessor?
|
||||
|
||||
/// Whether the app is running for unit tests.
|
||||
var isTesting: Bool {
|
||||
ProcessInfo.processInfo.arguments.contains("-testing")
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func application(
|
||||
_: UIApplication,
|
||||
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil,
|
||||
) -> Bool {
|
||||
// Exit early if testing to avoid running any app functionality.
|
||||
guard !isTesting else { return true }
|
||||
|
||||
let services = ServiceContainer()
|
||||
let appModule = DefaultAppModule(services: services)
|
||||
appProcessor = AppProcessor(appModule: appModule, services: services)
|
||||
return true
|
||||
}
|
||||
}
|
||||
100
TestHarness/Application/SceneDelegate.swift
Normal file
100
TestHarness/Application/SceneDelegate.swift
Normal file
@ -0,0 +1,100 @@
|
||||
import BitwardenKit
|
||||
import SwiftUI
|
||||
import TestHarnessShared
|
||||
import UIKit
|
||||
|
||||
/// The app's `UIWindowSceneDelegate` which manages the app's window and scene lifecycle.
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
// MARK: Properties
|
||||
|
||||
/// The processor that manages application level logic.
|
||||
var appProcessor: AppProcessor? {
|
||||
(UIApplication.shared.delegate as? AppDelegateType)?.appProcessor
|
||||
}
|
||||
|
||||
/// Whether the app is still starting up. This ensures the splash view isn't dismissed on start
|
||||
/// up until the processor has shown the initial view.
|
||||
var isStartingUp = true
|
||||
|
||||
/// Window shown as either the splash view on startup or when the app is backgrounded to
|
||||
/// prevent private information from being visible in the app switcher.
|
||||
var splashWindow: UIWindow?
|
||||
|
||||
/// The main window for this scene.
|
||||
var window: UIWindow?
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func scene(
|
||||
_ scene: UIScene,
|
||||
willConnectTo session: UISceneSession,
|
||||
options connectionOptions: UIScene.ConnectionOptions,
|
||||
) {
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
guard let appProcessor else {
|
||||
if (UIApplication.shared.delegate as? AppDelegateType)?.isTesting == true {
|
||||
// If the app is running tests, show a testing view.
|
||||
window = buildSplashWindow(windowScene: windowScene)
|
||||
window?.makeKeyAndVisible()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let rootViewController = RootViewController()
|
||||
let appWindow = UIWindow(windowScene: windowScene)
|
||||
appWindow.rootViewController = rootViewController
|
||||
appWindow.makeKeyAndVisible()
|
||||
window = appWindow
|
||||
|
||||
// Splash window. This is initially visible until the app's processor has finished starting.
|
||||
splashWindow = buildSplashWindow(windowScene: windowScene)
|
||||
|
||||
// Start the app's processor and show the splash view until the initial view is shown.
|
||||
Task {
|
||||
await appProcessor.start(
|
||||
navigator: rootViewController,
|
||||
window: appWindow,
|
||||
)
|
||||
hideSplash()
|
||||
isStartingUp = false
|
||||
}
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
showSplash()
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
guard !isStartingUp else { return }
|
||||
hideSplash()
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
/// Builds the splash window for display in the specified window scene.
|
||||
///
|
||||
/// - Parameter windowScene: The window scene that the splash window will be shown in.
|
||||
/// - Returns: A window containing the splash view.
|
||||
private func buildSplashWindow(windowScene: UIWindowScene) -> UIWindow {
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.isHidden = false
|
||||
window.rootViewController = UIStoryboard(
|
||||
name: "LaunchScreen",
|
||||
bundle: .main,
|
||||
).instantiateInitialViewController()
|
||||
window.windowLevel = UIWindow.Level.alert + 1
|
||||
return window
|
||||
}
|
||||
|
||||
/// Hides the splash view.
|
||||
private func hideSplash() {
|
||||
UIView.animate(withDuration: UI.duration(0.4)) {
|
||||
self.splashWindow?.alpha = 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows the splash view.
|
||||
private func showSplash() {
|
||||
splashWindow?.alpha = 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
<?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>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.${BASE_BUNDLE_ID}</string>
|
||||
<string>$(SHARED_APP_GROUP_IDENTIFIER)</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.bitwarden.testharness</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
82
TestHarness/Application/Support/Info.plist
Normal file
82
TestHarness/Application/Support/Info.plist
Normal file
@ -0,0 +1,82 @@
|
||||
<?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>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Test Harness</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
</array>
|
||||
<key>CFBundleName</key>
|
||||
<string>Test Harness</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>15.0</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile~ipad</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<dict>
|
||||
<key>arm64</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Resources/Assets.xcassets/AppIcons.appiconset</string>
|
||||
<key>BitwardenTestHarnessSharedAppGroup</key>
|
||||
<string>$(SHARED_APP_GROUP_IDENTIFIER)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina5_9" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Test Harness" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BLO-Tb-egS">
|
||||
<rect key="frame" x="87.666666666666686" y="390.66666666666669" width="200" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="ssv-dM-0pp"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/>
|
||||
<color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="BLO-Tb-egS" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="ZNN-2t-45e"/>
|
||||
<constraint firstItem="BLO-Tb-egS" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="rFh-72-gEC"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="52.671755725190835" y="374.64788732394368"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
22
TestHarness/Application/TestHelpers/Support/Info.plist
Normal file
22
TestHarness/Application/TestHelpers/Support/Info.plist
Normal file
@ -0,0 +1,22 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,24 @@
|
||||
import TestHarnessShared
|
||||
import UIKit
|
||||
|
||||
/// The app's `UIApplicationDelegate` used when running tests.
|
||||
class TestingAppDelegate: UIResponder, UIApplicationDelegate, AppDelegateType {
|
||||
// MARK: Properties
|
||||
|
||||
/// The processor that manages application level logic.
|
||||
var appProcessor: AppProcessor?
|
||||
|
||||
/// Whether the app is running for unit tests.
|
||||
var isTesting: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func application(
|
||||
_: UIApplication,
|
||||
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil,
|
||||
) -> Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
8
TestHarness/Application/main.swift
Normal file
8
TestHarness/Application/main.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import UIKit
|
||||
|
||||
/// Determine the app delegate class that should be used during this launch of the app. If the `TestingAppDelegate`
|
||||
/// can be found, the app was launched in a test environment, and we should use the `TestingAppDelegate` for
|
||||
/// handling all app lifecycle events.
|
||||
private let appDelegateClass: AnyClass = NSClassFromString("TestHarnessTests.TestingAppDelegate") ?? AppDelegate.self
|
||||
|
||||
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(appDelegateClass))
|
||||
40
TestHarnessShared/Core/Platform/ServiceContainer.swift
Normal file
40
TestHarnessShared/Core/Platform/ServiceContainer.swift
Normal file
@ -0,0 +1,40 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
/// The services provided by the `ServiceContainer`.
|
||||
typealias Services = HasErrorReportBuilder
|
||||
|
||||
/// The default implementation of a container that provides the services used by the application.
|
||||
///
|
||||
public class ServiceContainer: Services {
|
||||
// MARK: Properties
|
||||
|
||||
/// A helper for building an error report containing the details of an error that occurred.
|
||||
public let errorReportBuilder: ErrorReportBuilder
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `ServiceContainer`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - errorReportBuilder: A helper for building an error report containing the details of an
|
||||
public init(
|
||||
errorReportBuilder: ErrorReportBuilder,
|
||||
) {
|
||||
self.errorReportBuilder = errorReportBuilder
|
||||
}
|
||||
|
||||
public convenience init() {
|
||||
let appInfoService = DefaultAppInfoService()
|
||||
let stateService = DefaultStateService()
|
||||
|
||||
let errorReportBuilder = DefaultErrorReportBuilder(
|
||||
activeAccountStateProvider: stateService,
|
||||
appInfoService: appInfoService,
|
||||
)
|
||||
|
||||
self.init(
|
||||
errorReportBuilder: errorReportBuilder,
|
||||
)
|
||||
}
|
||||
}
|
||||
16
TestHarnessShared/Core/Platform/Services/StateService.swift
Normal file
16
TestHarnessShared/Core/Platform/Services/StateService.swift
Normal file
@ -0,0 +1,16 @@
|
||||
import BitwardenKit
|
||||
|
||||
/// Protocol for the service that controls the general state of the app.
|
||||
protocol StateService {}
|
||||
|
||||
// MARK: - DefaultStateService
|
||||
|
||||
/// A default implementation of `StateService`.
|
||||
///
|
||||
actor DefaultStateService: StateService, ActiveAccountStateProvider {
|
||||
// MARK: Methods
|
||||
|
||||
func getActiveAccountId() async throws -> String {
|
||||
"Test-Harness-Account-ID"
|
||||
}
|
||||
}
|
||||
2
TestHarnessShared/Sourcery/Generated/.gitignore
vendored
Normal file
2
TestHarnessShared/Sourcery/Generated/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Ignore SwiftGen generated files
|
||||
*.swift
|
||||
18
TestHarnessShared/Sourcery/sourcery.yml
Normal file
18
TestHarnessShared/Sourcery/sourcery.yml
Normal file
@ -0,0 +1,18 @@
|
||||
sources:
|
||||
- ..
|
||||
|
||||
templates:
|
||||
- ../../Sourcery/Templates/AutoMockable.stencil
|
||||
|
||||
output:
|
||||
Generated
|
||||
|
||||
exclude:
|
||||
- Generated
|
||||
- Tests
|
||||
- TestHelpers
|
||||
- Fixtures
|
||||
|
||||
args:
|
||||
autoMockableImports: ["BitwardenKit"]
|
||||
autoMockableTestableImports: ["TestHarnessShared"]
|
||||
@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
/// Actions that can be processed by a `SimpleLoginFormProcessor`.
|
||||
///
|
||||
enum SimpleLoginFormAction: Equatable {
|
||||
/// The password field was updated.
|
||||
case passwordChanged(String)
|
||||
|
||||
/// The username field was updated.
|
||||
case usernameChanged(String)
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
/// Effects that can be processed by a `SimpleLoginFormProcessor`.
|
||||
///
|
||||
enum SimpleLoginFormEffect: Equatable {}
|
||||
@ -0,0 +1,41 @@
|
||||
import BitwardenKit
|
||||
import Combine
|
||||
|
||||
/// The processor for the simple login form test screen.
|
||||
///
|
||||
class SimpleLoginFormProcessor: StateProcessor<
|
||||
SimpleLoginFormState,
|
||||
SimpleLoginFormAction,
|
||||
SimpleLoginFormEffect,
|
||||
> {
|
||||
// MARK: Types
|
||||
|
||||
typealias Services = HasErrorReporter
|
||||
|
||||
// MARK: Private Properties
|
||||
|
||||
/// The coordinator that handles navigation.
|
||||
private let coordinator: AnyCoordinator<RootRoute, Void>
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `SimpleLoginFormProcessor`.
|
||||
///
|
||||
/// - Parameter coordinator: The coordinator that handles navigation.
|
||||
///
|
||||
init(coordinator: AnyCoordinator<RootRoute, Void>) {
|
||||
self.coordinator = coordinator
|
||||
super.init(state: SimpleLoginFormState())
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
override func receive(_ action: SimpleLoginFormAction) {
|
||||
switch action {
|
||||
case let .usernameChanged(newValue):
|
||||
state.username = newValue
|
||||
case let .passwordChanged(newValue):
|
||||
state.password = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
|
||||
/// The state for the simple login form test screen.
|
||||
///
|
||||
struct SimpleLoginFormState: Equatable {
|
||||
// MARK: Properties
|
||||
|
||||
/// The title of the screen.
|
||||
var title: String = Localizations.simpleLoginForm
|
||||
|
||||
/// The username field value.
|
||||
var username: String = ""
|
||||
|
||||
/// The password field value.
|
||||
var password: String = ""
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
import BitwardenKit
|
||||
import SwiftUI
|
||||
|
||||
/// A view that displays a simple login form for testing autofill functionality.
|
||||
///
|
||||
struct SimpleLoginFormView: View {
|
||||
// MARK: Properties
|
||||
|
||||
/// The store used to render the view.
|
||||
@ObservedObject var store: Store<SimpleLoginFormState, SimpleLoginFormAction, SimpleLoginFormEffect>
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.navigationTitle(store.state.title)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
|
||||
// MARK: Private Views
|
||||
|
||||
/// The main content view.
|
||||
private var content: some View {
|
||||
Form {
|
||||
Section {
|
||||
TextField(
|
||||
Localizations.username,
|
||||
text: store.binding(
|
||||
get: \.username,
|
||||
send: SimpleLoginFormAction.usernameChanged,
|
||||
),
|
||||
)
|
||||
.textContentType(.username)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
|
||||
SecureField(
|
||||
Localizations.password,
|
||||
text: store.binding(
|
||||
get: \.password,
|
||||
send: SimpleLoginFormAction.passwordChanged,
|
||||
),
|
||||
)
|
||||
.textContentType(.password)
|
||||
} header: {
|
||||
Text(Localizations.credentials)
|
||||
} footer: {
|
||||
Text(Localizations.simpleLoginFormDescription)
|
||||
}
|
||||
|
||||
Section {
|
||||
if !store.state.username.isEmpty || !store.state.password.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if !store.state.username.isEmpty {
|
||||
Text(Localizations.usernameValue(store.state.username))
|
||||
.styleGuide(.body)
|
||||
}
|
||||
if !store.state.password.isEmpty {
|
||||
Text(Localizations.passwordValue(String(repeating: "•", count: store.state.password.count)))
|
||||
.styleGuide(.body)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(Localizations.enterCredentialsAbove)
|
||||
.foregroundColor(.secondary)
|
||||
.styleGuide(.body)
|
||||
}
|
||||
} header: {
|
||||
Text(Localizations.formValues)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#if DEBUG
|
||||
#Preview {
|
||||
NavigationView {
|
||||
SimpleLoginFormView(store: Store(processor: StateProcessor(state: SimpleLoginFormState())))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
100
TestHarnessShared/UI/Platform/Application/AppCoordinator.swift
Normal file
100
TestHarnessShared/UI/Platform/Application/AppCoordinator.swift
Normal file
@ -0,0 +1,100 @@
|
||||
import BitwardenKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// MARK: - AppCoordinator
|
||||
|
||||
/// A coordinator that manages the app's top-level navigation.
|
||||
///
|
||||
@MainActor
|
||||
class AppCoordinator: Coordinator, HasRootNavigator {
|
||||
// MARK: Types
|
||||
|
||||
/// The types of modules used by this coordinator.
|
||||
typealias Module = RootModule
|
||||
|
||||
// MARK: Private Properties
|
||||
|
||||
/// The coordinator currently being displayed.
|
||||
private var childCoordinator: AnyObject?
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The module to use for creating child coordinators.
|
||||
let module: Module
|
||||
|
||||
/// The navigator to use for presenting screens.
|
||||
private(set) weak var rootNavigator: RootNavigator?
|
||||
|
||||
/// The service container used by the coordinator.
|
||||
private let services: Services
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Creates a new `AppCoordinator`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - module: The module to use for creating child coordinators.
|
||||
/// - rootNavigator: The navigator to use for presenting screens.
|
||||
/// - services: The service container used by the coordinator.
|
||||
///
|
||||
init(
|
||||
module: Module,
|
||||
rootNavigator: RootNavigator,
|
||||
services: Services,
|
||||
) {
|
||||
self.module = module
|
||||
self.rootNavigator = rootNavigator
|
||||
self.services = services
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func handleEvent(_ event: AppEvent, context: AnyObject?) async {
|
||||
switch event {
|
||||
case .didStart:
|
||||
showRoot(route: .scenarioPicker)
|
||||
}
|
||||
}
|
||||
|
||||
func navigate(to route: AppRoute, context _: AnyObject?) {
|
||||
switch route {
|
||||
case let .root(rootRoute):
|
||||
showRoot(route: rootRoute)
|
||||
}
|
||||
}
|
||||
|
||||
func start() {
|
||||
// Nothing to do here - the initial route is specified by `AppProcessor` and this
|
||||
// coordinator doesn't need to navigate within the `Navigator` since it's the root.
|
||||
}
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
/// Shows the root route.
|
||||
///
|
||||
/// - Parameter route: The root route to show.
|
||||
///
|
||||
private func showRoot(route: RootRoute) {
|
||||
if let coordinator = childCoordinator as? AnyCoordinator<RootRoute, Void> {
|
||||
coordinator.navigate(to: route)
|
||||
} else {
|
||||
guard let rootNavigator else { return }
|
||||
let navigationController = UINavigationController()
|
||||
navigationController.navigationBar.prefersLargeTitles = true
|
||||
let coordinator = module.makeRootCoordinator(
|
||||
stackNavigator: navigationController,
|
||||
)
|
||||
coordinator.start()
|
||||
coordinator.navigate(to: route)
|
||||
childCoordinator = coordinator
|
||||
rootNavigator.show(child: navigationController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HasErrorAlertServices
|
||||
|
||||
extension AppCoordinator: HasErrorAlertServices {
|
||||
var errorAlertServices: ErrorAlertServices { services }
|
||||
}
|
||||
8
TestHarnessShared/UI/Platform/Application/AppEvent.swift
Normal file
8
TestHarnessShared/UI/Platform/Application/AppEvent.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
/// The events handled by the `AppCoordinator`.
|
||||
///
|
||||
public enum AppEvent {
|
||||
/// The app has started.
|
||||
case didStart
|
||||
}
|
||||
65
TestHarnessShared/UI/Platform/Application/AppModule.swift
Normal file
65
TestHarnessShared/UI/Platform/Application/AppModule.swift
Normal file
@ -0,0 +1,65 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - AppModule
|
||||
|
||||
/// A protocol for an object that contains the dependencies for creating coordinators in the app flow.
|
||||
///
|
||||
@MainActor
|
||||
public protocol AppModule: AnyObject {
|
||||
/// Creates an `AppCoordinator`.
|
||||
///
|
||||
/// - Parameter navigator: The navigator to use for presenting screens.
|
||||
/// - Returns: An `AppCoordinator` instance.
|
||||
///
|
||||
func makeAppCoordinator(
|
||||
navigator: RootNavigator,
|
||||
) -> AnyCoordinator<AppRoute, AppEvent>
|
||||
}
|
||||
|
||||
// MARK: - DefaultAppModule
|
||||
|
||||
/// A default implementation of `AppModule`.
|
||||
///
|
||||
@MainActor
|
||||
public class DefaultAppModule: AppModule {
|
||||
// MARK: Properties
|
||||
|
||||
/// The services used by the module.
|
||||
let services: ServiceContainer
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `DefaultAppModule`.
|
||||
///
|
||||
/// - Parameter services: The services used by the module.
|
||||
///
|
||||
public init(services: ServiceContainer) {
|
||||
self.services = services
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
public func makeAppCoordinator(
|
||||
navigator: RootNavigator,
|
||||
) -> AnyCoordinator<AppRoute, AppEvent> {
|
||||
AppCoordinator(
|
||||
module: self,
|
||||
rootNavigator: navigator,
|
||||
services: services,
|
||||
).asAnyCoordinator()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RootModule
|
||||
|
||||
extension DefaultAppModule: RootModule {
|
||||
func makeRootCoordinator(
|
||||
stackNavigator: StackNavigator,
|
||||
) -> AnyCoordinator<RootRoute, Void> {
|
||||
RootCoordinator(
|
||||
services: services,
|
||||
stackNavigator: stackNavigator,
|
||||
).asAnyCoordinator()
|
||||
}
|
||||
}
|
||||
58
TestHarnessShared/UI/Platform/Application/AppProcessor.swift
Normal file
58
TestHarnessShared/UI/Platform/Application/AppProcessor.swift
Normal file
@ -0,0 +1,58 @@
|
||||
import BitwardenKit
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// The `AppProcessor` processes actions received at the application level and contains the logic
|
||||
/// to control the top-level flow through the app.
|
||||
///
|
||||
@MainActor
|
||||
public class AppProcessor {
|
||||
// MARK: Properties
|
||||
|
||||
/// The root module to use to create sub-coordinators.
|
||||
let appModule: AppModule
|
||||
|
||||
/// The root coordinator of the app.
|
||||
var coordinator: AnyCoordinator<AppRoute, AppEvent>?
|
||||
|
||||
/// The services used by the app.
|
||||
let services: ServiceContainer
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initializes an `AppProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appModule: The root module to use to create sub-coordinators.
|
||||
/// - services: The services used by the app.
|
||||
///
|
||||
public init(
|
||||
appModule: AppModule,
|
||||
services: ServiceContainer,
|
||||
) {
|
||||
self.appModule = appModule
|
||||
self.services = services
|
||||
|
||||
UI.applyDefaultAppearances()
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Starts the application flow by navigating the user to the first flow.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - navigator: The object that will be used to navigate between routes.
|
||||
/// - window: The window to use to set the app's theme.
|
||||
///
|
||||
public func start(
|
||||
navigator: RootNavigator,
|
||||
window: UIWindow?,
|
||||
) async {
|
||||
let coordinator = appModule.makeAppCoordinator(navigator: navigator)
|
||||
coordinator.start()
|
||||
self.coordinator = coordinator
|
||||
|
||||
await coordinator.handleEvent(.didStart)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import BitwardenKit
|
||||
import XCTest
|
||||
|
||||
@testable import TestHarnessShared
|
||||
|
||||
/// Tests for `AppProcessor`.
|
||||
///
|
||||
class AppProcessorTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var appModule: MockAppModule!
|
||||
var subject: AppProcessor!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
appModule = MockAppModule()
|
||||
let services = ServiceContainer()
|
||||
subject = AppProcessor(appModule: appModule, services: services)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
appModule = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// `start()` creates and starts the app coordinator.
|
||||
func test_start() async {
|
||||
let navigator = MockRootNavigator()
|
||||
await subject.start(navigator: navigator, window: nil)
|
||||
|
||||
XCTAssertTrue(appModule.makeAppCoordinatorCalled)
|
||||
XCTAssertNotNil(subject.coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
/// A mock `AppModule` for testing.
|
||||
///
|
||||
@MainActor
|
||||
class MockAppModule: AppModule {
|
||||
var makeAppCoordinatorCalled = false
|
||||
|
||||
func makeAppCoordinator(navigator: RootNavigator) -> AnyCoordinator<AppRoute, AppEvent> {
|
||||
makeAppCoordinatorCalled = true
|
||||
return AppCoordinator(
|
||||
module: self,
|
||||
rootNavigator: navigator,
|
||||
services: ServiceContainer(),
|
||||
).asAnyCoordinator()
|
||||
}
|
||||
}
|
||||
|
||||
extension MockAppModule: RootModule {
|
||||
func makeRootCoordinator(stackNavigator: StackNavigator) -> AnyCoordinator<RootRoute, Void> {
|
||||
RootCoordinator(
|
||||
services: ServiceContainer(),
|
||||
stackNavigator: stackNavigator,
|
||||
).asAnyCoordinator()
|
||||
}
|
||||
}
|
||||
8
TestHarnessShared/UI/Platform/Application/AppRoute.swift
Normal file
8
TestHarnessShared/UI/Platform/Application/AppRoute.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
/// The routes for navigating through the app.
|
||||
///
|
||||
public enum AppRoute {
|
||||
/// A route to the root screen.
|
||||
case root(RootRoute)
|
||||
}
|
||||
2
TestHarnessShared/UI/Platform/Application/Support/Generated/.gitignore
vendored
Normal file
2
TestHarnessShared/UI/Platform/Application/Support/Generated/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Ignore SwiftGen generated files
|
||||
*.swift
|
||||
22
TestHarnessShared/UI/Platform/Application/Support/Info.plist
Normal file
22
TestHarnessShared/UI/Platform/Application/Support/Info.plist
Normal file
@ -0,0 +1,22 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,13 @@
|
||||
"TestHarness" = "Test Harness";
|
||||
"TestScenarios" = "Test Scenarios";
|
||||
"SimpleLoginForm" = "Simple Login Form";
|
||||
"PasskeyAutofill" = "Passkey Autofill";
|
||||
"CreatePasskey" = "Create Passkey";
|
||||
"Username" = "Username";
|
||||
"Password" = "Password";
|
||||
"Credentials" = "Credentials";
|
||||
"SimpleLoginFormDescription" = "Use this simple login form to test autofill functionality.";
|
||||
"EnterCredentialsAbove" = "Enter credentials above";
|
||||
"FormValues" = "Form Values";
|
||||
"UsernameValue" = "Username: %@";
|
||||
"PasswordValue" = "Password: %@";
|
||||
@ -0,0 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
/// Utility and factory methods for handling test harness shared resources.
|
||||
///
|
||||
public class TestHarnessResources {
|
||||
/// The language code at initialization.
|
||||
public static var initialLanguageCode: String?
|
||||
|
||||
/// Override SwiftGen's lookup function in order to determine the language manually.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The localization key.
|
||||
/// - table: The localization table.
|
||||
/// - fallbackValue: The fallback value if the key is not found.
|
||||
/// - Returns: The localized string.
|
||||
///
|
||||
public static func localizationFunction(key: String, table: String, fallbackValue: String) -> String {
|
||||
if let languageCode = initialLanguageCode,
|
||||
let path = Bundle(for: TestHarnessResources.self).path(forResource: languageCode, ofType: "lproj"),
|
||||
let bundle = Bundle(path: path) {
|
||||
return bundle.localizedString(forKey: key, value: fallbackValue, table: table)
|
||||
}
|
||||
return Bundle.main.localizedString(forKey: key, value: fallbackValue, table: table)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
74
TestHarnessShared/UI/Platform/Root/RootCoordinator.swift
Normal file
74
TestHarnessShared/UI/Platform/Root/RootCoordinator.swift
Normal file
@ -0,0 +1,74 @@
|
||||
import BitwardenKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// MARK: - RootCoordinator
|
||||
|
||||
/// A coordinator that manages navigation in the root flow of test scenarios.
|
||||
///
|
||||
@MainActor
|
||||
class RootCoordinator: Coordinator, HasStackNavigator {
|
||||
// MARK: Private Properties
|
||||
|
||||
/// The services used by this coordinator.
|
||||
private let services: Services
|
||||
|
||||
/// The stack navigator used to display screens.
|
||||
private(set) weak var stackNavigator: StackNavigator?
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Creates a new `RootCoordinator`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - services: The services used by this coordinator.
|
||||
/// - stackNavigator: The stack navigator used to display screens.
|
||||
///
|
||||
init(
|
||||
services: Services,
|
||||
stackNavigator: StackNavigator,
|
||||
) {
|
||||
self.services = services
|
||||
self.stackNavigator = stackNavigator
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func navigate(to route: RootRoute, context: AnyObject?) {
|
||||
switch route {
|
||||
case .scenarioPicker:
|
||||
showScenarioPicker()
|
||||
case .simpleLoginForm:
|
||||
showSimpleLoginForm()
|
||||
}
|
||||
}
|
||||
|
||||
func start() {
|
||||
// Nothing to do here - the initial route is set by the parent coordinator.
|
||||
}
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
/// Shows the scenario picker screen.
|
||||
///
|
||||
private func showScenarioPicker() {
|
||||
let processor = ScenarioPickerProcessor(coordinator: asAnyCoordinator())
|
||||
let view = ScenarioPickerView(store: Store(processor: processor))
|
||||
stackNavigator?.replace(view)
|
||||
}
|
||||
|
||||
/// Shows the simple login form test screen.
|
||||
///
|
||||
private func showSimpleLoginForm() {
|
||||
let processor = SimpleLoginFormProcessor(coordinator: asAnyCoordinator())
|
||||
let view = SimpleLoginFormView(store: Store(processor: processor))
|
||||
let viewController = UIHostingController(rootView: view)
|
||||
stackNavigator?.push(viewController)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HasErrorAlertServices
|
||||
|
||||
extension RootCoordinator: HasErrorAlertServices {
|
||||
var errorAlertServices: ErrorAlertServices { services }
|
||||
}
|
||||
16
TestHarnessShared/UI/Platform/Root/RootModule.swift
Normal file
16
TestHarnessShared/UI/Platform/Root/RootModule.swift
Normal file
@ -0,0 +1,16 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
/// A protocol for an object that contains the dependencies for creating coordinators in the root flow.
|
||||
///
|
||||
@MainActor
|
||||
protocol RootModule: AnyObject {
|
||||
/// Creates a `RootCoordinator`.
|
||||
///
|
||||
/// - Parameter stackNavigator: The stack navigator to use for presenting screens.
|
||||
/// - Returns: A `RootCoordinator` instance.
|
||||
///
|
||||
func makeRootCoordinator(
|
||||
stackNavigator: StackNavigator,
|
||||
) -> AnyCoordinator<RootRoute, Void>
|
||||
}
|
||||
11
TestHarnessShared/UI/Platform/Root/RootRoute.swift
Normal file
11
TestHarnessShared/UI/Platform/Root/RootRoute.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
/// The routes for navigating within the home flow.
|
||||
///
|
||||
public enum RootRoute {
|
||||
/// A route to the scenario picker home screen.
|
||||
case scenarioPicker
|
||||
|
||||
/// A route to the simple login form test screen.
|
||||
case simpleLoginForm
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
/// Actions that can be processed by a `ScenarioPickerProcessor`.
|
||||
///
|
||||
enum ScenarioPickerAction: Equatable {
|
||||
/// A test scenario was tapped.
|
||||
///
|
||||
/// - Parameter scenario: The scenario that was tapped.
|
||||
case scenarioTapped(ScenarioItem)
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
/// Effects that can be processed by a `ScenarioPickerProcessor`.
|
||||
///
|
||||
enum ScenarioPickerEffect: Equatable {}
|
||||
@ -0,0 +1,39 @@
|
||||
import BitwardenKit
|
||||
import Combine
|
||||
|
||||
/// The processor for the scenario picker screen.
|
||||
///
|
||||
class ScenarioPickerProcessor: StateProcessor<ScenarioPickerState, ScenarioPickerAction, ScenarioPickerEffect> {
|
||||
// MARK: Types
|
||||
|
||||
typealias Services = HasErrorReporter
|
||||
|
||||
// MARK: Private Properties
|
||||
|
||||
/// The coordinator that handles navigation.
|
||||
private let coordinator: AnyCoordinator<RootRoute, Void>
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `ScenarioPickerProcessor`.
|
||||
///
|
||||
/// - Parameter coordinator: The coordinator that handles navigation.
|
||||
///
|
||||
init(coordinator: AnyCoordinator<RootRoute, Void>) {
|
||||
self.coordinator = coordinator
|
||||
super.init(state: ScenarioPickerState())
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
override func receive(_ action: ScenarioPickerAction) {
|
||||
switch action {
|
||||
case let .scenarioTapped(scenario):
|
||||
guard let route = scenario.route else {
|
||||
// Scenario not yet implemented
|
||||
return
|
||||
}
|
||||
coordinator.navigate(to: route)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
|
||||
/// A scenario that can be selected in the test harness.
|
||||
///
|
||||
struct ScenarioItem: Equatable, Identifiable {
|
||||
// MARK: Properties
|
||||
|
||||
/// The unique identifier for the scenario.
|
||||
let id: String
|
||||
|
||||
/// The display title for the scenario.
|
||||
let title: String
|
||||
|
||||
/// The route to navigate to when this scenario is selected.
|
||||
let route: RootRoute?
|
||||
}
|
||||
|
||||
/// The state for the scenario picker screen.
|
||||
///
|
||||
struct ScenarioPickerState: Equatable {
|
||||
// MARK: Properties
|
||||
|
||||
/// The title of the screen.
|
||||
var title: String = Localizations.testHarness
|
||||
|
||||
/// The available test scenarios.
|
||||
var scenarios: [ScenarioItem] = [
|
||||
ScenarioItem(id: "simpleLoginForm", title: Localizations.simpleLoginForm, route: .simpleLoginForm),
|
||||
ScenarioItem(id: "passkeyAutofill", title: Localizations.passkeyAutofill, route: nil),
|
||||
ScenarioItem(id: "createPasskey", title: Localizations.createPasskey, route: nil),
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import BitwardenKit
|
||||
import SwiftUI
|
||||
|
||||
/// A view that displays a list of test scenarios available in the test harness.
|
||||
///
|
||||
struct ScenarioPickerView: View {
|
||||
// MARK: Properties
|
||||
|
||||
/// The store used to render the view.
|
||||
@ObservedObject var store: Store<ScenarioPickerState, ScenarioPickerAction, ScenarioPickerEffect>
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
content
|
||||
.navigationTitle(store.state.title)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
|
||||
// MARK: Private Views
|
||||
|
||||
/// The main content view.
|
||||
private var content: some View {
|
||||
List {
|
||||
Section {
|
||||
ForEach(store.state.scenarios) { scenario in
|
||||
Button {
|
||||
store.send(.scenarioTapped(scenario))
|
||||
} label: {
|
||||
HStack {
|
||||
Text(scenario.title)
|
||||
.styleGuide(.body)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
} header: {
|
||||
Text(Localizations.testScenarios)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#if DEBUG
|
||||
#Preview {
|
||||
NavigationView {
|
||||
ScenarioPickerView(store: Store(processor: StateProcessor(state: ScenarioPickerState())))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
37
TestPlans/TestHarness-Default.xctestplan
Normal file
37
TestPlans/TestHarness-Default.xctestplan
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"configurations" : [
|
||||
{
|
||||
"id" : "00000000-0000-0000-0000-000000000001",
|
||||
"name" : "Test Scheme Action",
|
||||
"options" : {
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
"targetForVariableExpansion" : {
|
||||
"containerPath" : "container:TestHarness.xcodeproj",
|
||||
"identifier" : "TestHarness",
|
||||
"name" : "TestHarness"
|
||||
}
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"parallelizable" : true,
|
||||
"target" : {
|
||||
"containerPath" : "container:TestHarness.xcodeproj",
|
||||
"identifier" : "TestHarnessTests",
|
||||
"name" : "TestHarnessTests"
|
||||
}
|
||||
},
|
||||
{
|
||||
"parallelizable" : true,
|
||||
"target" : {
|
||||
"containerPath" : "container:TestHarness.xcodeproj",
|
||||
"identifier" : "TestHarnessSharedTests",
|
||||
"name" : "TestHarnessSharedTests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 1
|
||||
}
|
||||
33
TestPlans/TestHarness-Unit.xctestplan
Normal file
33
TestPlans/TestHarness-Unit.xctestplan
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"configurations" : [
|
||||
{
|
||||
"id" : "00000000-0000-0000-0000-000000000001",
|
||||
"name" : "Test Scheme Action",
|
||||
"options" : {
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
"targetForVariableExpansion" : {
|
||||
"containerPath" : "container:TestHarness.xcodeproj",
|
||||
"identifier" : "TestHarness",
|
||||
"name" : "TestHarness"
|
||||
}
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"parallelizable" : true,
|
||||
"skippedTests" : [
|
||||
"BitwardenKitTests",
|
||||
"NetworkingTests"
|
||||
],
|
||||
"target" : {
|
||||
"containerPath" : "container:TestHarness.xcodeproj",
|
||||
"identifier" : "TestHarnessSharedTests",
|
||||
"name" : "TestHarnessSharedTests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 1
|
||||
}
|
||||
179
project-bwth.yml
Normal file
179
project-bwth.yml
Normal file
@ -0,0 +1,179 @@
|
||||
name: TestHarness
|
||||
configs:
|
||||
Debug: debug
|
||||
Release: release
|
||||
include:
|
||||
- path: project-common.yml
|
||||
options:
|
||||
fileTypes:
|
||||
"icon":
|
||||
file: true
|
||||
settings:
|
||||
MARKETING_VERSION: 1.0.0
|
||||
CURRENT_PROJECT_VERSION: 1
|
||||
projectReferences:
|
||||
BitwardenKit:
|
||||
path: BitwardenKit.xcodeproj
|
||||
schemes:
|
||||
TestHarness:
|
||||
build:
|
||||
targets:
|
||||
TestHarness: all
|
||||
TestHarnessTests: [test]
|
||||
test:
|
||||
commandLineArguments:
|
||||
"-testing": true
|
||||
environmentVariables:
|
||||
TZ: UTC
|
||||
gatherCoverageData: true
|
||||
coverageTargets:
|
||||
- TestHarness
|
||||
- TestHarnessShared
|
||||
- BitwardenKit/BitwardenKit
|
||||
targets:
|
||||
- TestHarnessTests
|
||||
- TestHarnessSharedTests
|
||||
- BitwardenKit/BitwardenKitTests
|
||||
testPlans:
|
||||
- path: TestPlans/TestHarness-Default.xctestplan
|
||||
defaultPlan: true
|
||||
- path: TestPlans/TestHarness-Unit.xctestplan
|
||||
TestHarnessShared:
|
||||
build:
|
||||
targets:
|
||||
TestHarnessShared: all
|
||||
TestHarnessSharedTests: [test]
|
||||
test:
|
||||
commandLineArguments:
|
||||
"-testing": true
|
||||
environmentVariables:
|
||||
TZ: UTC
|
||||
gatherCoverageData: true
|
||||
targets:
|
||||
- TestHarnessSharedTests
|
||||
testPlans:
|
||||
- path: TestPlans/TestHarness-Default.xctestplan
|
||||
defaultPlan: true
|
||||
- path: TestPlans/TestHarness-Unit.xctestplan
|
||||
targets:
|
||||
TestHarness:
|
||||
type: application
|
||||
platform: iOS
|
||||
configFiles:
|
||||
Debug: Configs/TestHarness-Debug.xcconfig
|
||||
Release: Configs/TestHarness-Release.xcconfig
|
||||
settings:
|
||||
base:
|
||||
INFOPLIST_FILE: TestHarness/Application/Support/Info.plist
|
||||
sources:
|
||||
- path: TestHarness
|
||||
excludes:
|
||||
- "**/*Tests.*"
|
||||
- "**/TestHelpers/*"
|
||||
- path: README-bwth.md
|
||||
buildPhase: none
|
||||
dependencies:
|
||||
- target: TestHarnessShared
|
||||
- target: BitwardenKit/BitwardenKit
|
||||
- target: BitwardenKit/BitwardenResources
|
||||
postCompileScripts:
|
||||
- script: |
|
||||
if [[ ! "$PATH" =~ "/opt/homebrew/bin" ]]; then
|
||||
PATH="/opt/homebrew/bin:$PATH"
|
||||
fi
|
||||
mint run swiftlint
|
||||
name: Swiftlint
|
||||
basedOnDependencyAnalysis: false
|
||||
- script: |
|
||||
if [[ ! "$PATH" =~ "/opt/homebrew/bin" ]]; then
|
||||
PATH="/opt/homebrew/bin:$PATH"
|
||||
fi
|
||||
mint run swiftformat --lint --lenient .
|
||||
name: SwiftFormat Lint
|
||||
basedOnDependencyAnalysis: false
|
||||
TestHarnessTests:
|
||||
type: bundle.unit-test
|
||||
platform: iOS
|
||||
settings:
|
||||
base:
|
||||
INFOPLIST_FILE: TestHarness/Application/TestHelpers/Support/Info.plist
|
||||
sources:
|
||||
- path: TestHarness
|
||||
includes:
|
||||
- "**/*Tests.*"
|
||||
- "**/TestHelpers/*"
|
||||
- path: GlobalTestHelpers-bwth
|
||||
dependencies:
|
||||
- target: TestHarness
|
||||
- target: BitwardenKit/TestHelpers
|
||||
randomExecutionOrder: true
|
||||
|
||||
TestHarnessShared:
|
||||
type: framework
|
||||
platform: iOS
|
||||
configFiles:
|
||||
Debug: Configs/TestHarnessShared-Debug.xcconfig
|
||||
Release: Configs/TestHarnessShared-Release.xcconfig
|
||||
settings:
|
||||
base:
|
||||
APPLICATION_EXTENSION_API_ONLY: true
|
||||
INFOPLIST_FILE: TestHarnessShared/UI/Platform/Application/Support/Info.plist
|
||||
sources:
|
||||
- path: TestHarnessShared
|
||||
excludes:
|
||||
- "**/*Tests.*"
|
||||
- "**/TestHelpers/*"
|
||||
- "**/Fixtures/*"
|
||||
- "**/Sourcery/Generated/*"
|
||||
- "**/sourcery.yml"
|
||||
- path: TestHarnessShared/Sourcery/sourcery.yml
|
||||
buildPhase: none
|
||||
- path: TestHarnessShared/UI/Platform/Application/Support/Generated/Localizations.swift
|
||||
optional: true
|
||||
dependencies:
|
||||
- target: BitwardenKit/BitwardenKit
|
||||
- target: BitwardenKit/BitwardenResources
|
||||
preBuildScripts:
|
||||
- name: Sourcery
|
||||
script: |
|
||||
if [[ ! "$PATH" =~ "/opt/homebrew/bin" ]]; then
|
||||
PATH="/opt/homebrew/bin:$PATH"
|
||||
fi
|
||||
mint run sourcery --config TestHarnessShared/Sourcery/sourcery.yml
|
||||
basedOnDependencyAnalysis: false
|
||||
outputFiles:
|
||||
- $(SRCROOT)/TestHarnessShared/Sourcery/Generated/AutoMockable.generated.swift
|
||||
- name: SwiftGen
|
||||
script: |
|
||||
if [[ ! "$PATH" =~ "/opt/homebrew/bin" ]]; then
|
||||
PATH="/opt/homebrew/bin:$PATH"
|
||||
fi
|
||||
mint run swiftgen config run --config "swiftgen-bwth.yml"
|
||||
basedOnDependencyAnalysis: false
|
||||
outputFiles:
|
||||
- $(SRCROOT)/TestHarnessShared/UI/Platform/Application/Support/Generated/Localizations.swift
|
||||
TestHarnessSharedTests:
|
||||
type: bundle.unit-test
|
||||
platform: iOS
|
||||
settings:
|
||||
base:
|
||||
BUNDLE_LOADER: "$(TEST_HOST)"
|
||||
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/TestHarness.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TestHarness"
|
||||
INFOPLIST_FILE: TestHarnessShared/UI/Platform/Application/TestHelpers/Support/Info.plist
|
||||
sources:
|
||||
- path: TestHarnessShared
|
||||
includes:
|
||||
- "**/*Tests.*"
|
||||
- "**/TestHelpers/*"
|
||||
- "**/Fixtures/*"
|
||||
- path: GlobalTestHelpers-bwth
|
||||
- path: TestHarnessShared/Sourcery/Generated
|
||||
optional: true
|
||||
- path: TestHarnessShared/Sourcery/Generated/AutoMockable.generated.swift
|
||||
optional: true
|
||||
dependencies:
|
||||
- target: TestHarness
|
||||
- target: TestHarnessShared
|
||||
- target: BitwardenKit/BitwardenKitMocks
|
||||
- target: BitwardenKit/TestHelpers
|
||||
randomExecutionOrder: true
|
||||
11
swiftgen-bwth.yml
Normal file
11
swiftgen-bwth.yml
Normal file
@ -0,0 +1,11 @@
|
||||
output_dir: TestHarnessShared/UI/Platform/Application/Support/Generated/
|
||||
strings:
|
||||
inputs:
|
||||
- TestHarnessShared/UI/Platform/Application/Support/Localizations/en.lproj
|
||||
outputs:
|
||||
- templateName: structured-swift5
|
||||
output: Localizations.swift
|
||||
params:
|
||||
enumName: Localizations
|
||||
publicAccess: true
|
||||
lookupFunction: TestHarnessResources.localizationFunction(key:table:fallbackValue:)
|
||||
Loading…
x
Reference in New Issue
Block a user