mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 15:57:30 -06:00
[PM-26061] Consolidate Debug Menu to BitwardenKit (#2177)
This commit is contained in:
parent
b7863001b1
commit
b72a3ed6dd
@ -1,10 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuAction
|
||||
|
||||
/// Actions that can be processed by a `DebugMenuProcessor`.
|
||||
///
|
||||
enum DebugMenuAction: Equatable {
|
||||
/// The dismiss button was tapped.
|
||||
case dismissTapped
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
/// A coordinator that manages navigation for the debug menu.
|
||||
///
|
||||
final class DebugMenuCoordinator: Coordinator, HasStackNavigator {
|
||||
// MARK: Types
|
||||
|
||||
typealias Services = HasAppSettingsStore
|
||||
& HasConfigService
|
||||
& HasErrorAlertServices.ErrorAlertServices
|
||||
|
||||
// MARK: Private Properties
|
||||
|
||||
/// The services used by this coordinator.
|
||||
private let services: Services
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The stack navigator that is managed by this coordinator.
|
||||
private(set) weak var stackNavigator: StackNavigator?
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Creates a new `DebugMenuCoordinator`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - services: The services used by this coordinator.
|
||||
/// - stackNavigator: The stack navigator that is managed by this coordinator.
|
||||
///
|
||||
init(
|
||||
services: Services,
|
||||
stackNavigator: StackNavigator,
|
||||
) {
|
||||
self.services = services
|
||||
self.stackNavigator = stackNavigator
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func navigate(
|
||||
to route: DebugMenuRoute,
|
||||
context: AnyObject?,
|
||||
) {
|
||||
switch route {
|
||||
case .dismiss:
|
||||
stackNavigator?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the process of displaying the debug menu.
|
||||
func start() {
|
||||
showDebugMenu()
|
||||
}
|
||||
|
||||
// MARK: Private Methods
|
||||
|
||||
/// Configures and displays the debug menu.
|
||||
private func showDebugMenu() {
|
||||
let processor = DebugMenuProcessor(
|
||||
coordinator: asAnyCoordinator(),
|
||||
services: services,
|
||||
state: DebugMenuState(),
|
||||
)
|
||||
|
||||
let view = DebugMenuView(store: Store(processor: processor))
|
||||
stackNavigator?.replace(view)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HasErrorAlertServices
|
||||
|
||||
extension DebugMenuCoordinator: HasErrorAlertServices {
|
||||
var errorAlertServices: ErrorAlertServices { services }
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
class DebugMenuCoordinatorTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var appSettingsStore: MockAppSettingsStore!
|
||||
var configService: MockConfigService!
|
||||
var stackNavigator: MockStackNavigator!
|
||||
var subject: DebugMenuCoordinator!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
appSettingsStore = MockAppSettingsStore()
|
||||
configService = MockConfigService()
|
||||
stackNavigator = MockStackNavigator()
|
||||
|
||||
subject = DebugMenuCoordinator(
|
||||
services: ServiceContainer.withMocks(
|
||||
appSettingsStore: appSettingsStore,
|
||||
configService: configService,
|
||||
),
|
||||
stackNavigator: stackNavigator,
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
appSettingsStore = nil
|
||||
configService = nil
|
||||
stackNavigator = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// `navigate(to:)` with `.dismiss` dismisses the view.
|
||||
@MainActor
|
||||
func test_navigate_dismiss() throws {
|
||||
subject.navigate(to: .dismiss)
|
||||
|
||||
let action = try XCTUnwrap(stackNavigator.actions.last)
|
||||
XCTAssertEqual(action.type, .dismissed)
|
||||
}
|
||||
|
||||
/// `start()` correctly shows the `DebugMenuView`.
|
||||
@MainActor
|
||||
func test_start() {
|
||||
subject.start()
|
||||
|
||||
XCTAssertTrue(stackNavigator.actions.last?.view is DebugMenuView)
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuEffect
|
||||
|
||||
/// Effects that can be processed by a `DebugMenuProcessor`.
|
||||
///
|
||||
enum DebugMenuEffect: Equatable {
|
||||
/// Triggers a refresh of feature flags, clearing local settings and re-fetching from the remote source.
|
||||
case refreshFeatureFlags
|
||||
|
||||
/// Toggles a specific feature flag's state.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - String: The identifier for the feature flag.
|
||||
/// - Bool: The state to which the feature flag should be set (enabled or disabled).
|
||||
case toggleFeatureFlag(String, Bool)
|
||||
|
||||
/// The view appeared and is ready to load data.
|
||||
case viewAppeared
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuProcessor
|
||||
|
||||
/// The processor used to manage state and handle actions for the `DebugMenuView`.
|
||||
///
|
||||
final class DebugMenuProcessor: StateProcessor<DebugMenuState, DebugMenuAction, DebugMenuEffect> {
|
||||
// MARK: Types
|
||||
|
||||
typealias Services = HasConfigService
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The `Coordinator` that handles navigation.
|
||||
private let coordinator: AnyCoordinator<DebugMenuRoute, Void>
|
||||
|
||||
/// The services used by the processor.
|
||||
private let services: Services
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initializes a `DebugMenuProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - coordinator: The coordinator used for navigation.
|
||||
/// - services: The services used by the processor.
|
||||
/// - state: The state of the debug menu.
|
||||
///
|
||||
init(
|
||||
coordinator: AnyCoordinator<DebugMenuRoute, Void>,
|
||||
services: Services,
|
||||
state: DebugMenuState,
|
||||
) {
|
||||
self.coordinator = coordinator
|
||||
self.services = services
|
||||
super.init(state: state)
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
override func receive(_ action: DebugMenuAction) {
|
||||
switch action {
|
||||
case .dismissTapped:
|
||||
coordinator.navigate(to: .dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
override func perform(_ effect: DebugMenuEffect) async {
|
||||
switch effect {
|
||||
case .viewAppeared:
|
||||
await fetchFlags()
|
||||
case .refreshFeatureFlags:
|
||||
await refreshFlags()
|
||||
case let .toggleFeatureFlag(flag, newValue):
|
||||
await services.configService.toggleDebugFeatureFlag(
|
||||
name: flag,
|
||||
newValue: newValue,
|
||||
)
|
||||
state.featureFlags = await services.configService.getDebugFeatureFlags(FeatureFlag.allCases)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private Functions
|
||||
|
||||
/// Fetch the current debug feature flags.
|
||||
private func fetchFlags() async {
|
||||
state.featureFlags = await services.configService.getDebugFeatureFlags(FeatureFlag.allCases)
|
||||
}
|
||||
|
||||
/// Refreshes the feature flags by resetting their local values and fetching the latest configurations.
|
||||
private func refreshFlags() async {
|
||||
state.featureFlags = await services.configService.refreshDebugFeatureFlags(FeatureFlag.allCases)
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
class DebugMenuProcessorTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var configService: MockConfigService!
|
||||
var coordinator: MockCoordinator<DebugMenuRoute, Void>!
|
||||
var subject: DebugMenuProcessor!
|
||||
|
||||
// MARK: Set Up & Tear Down
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
configService = MockConfigService()
|
||||
coordinator = MockCoordinator<DebugMenuRoute, Void>()
|
||||
subject = DebugMenuProcessor(
|
||||
coordinator: coordinator.asAnyCoordinator(),
|
||||
services: ServiceContainer.withMocks(
|
||||
configService: configService,
|
||||
),
|
||||
state: DebugMenuState(featureFlags: []),
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
configService = nil
|
||||
coordinator = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// `receive()` with `.dismissTapped` navigates to the `.dismiss` route.
|
||||
@MainActor
|
||||
func test_receive_dismissTapped() {
|
||||
subject.receive(.dismissTapped)
|
||||
XCTAssertEqual(coordinator.routes.last, .dismiss)
|
||||
}
|
||||
|
||||
/// `perform(.viewAppeared)` loads the correct feature flags.
|
||||
@MainActor
|
||||
func test_perform_appeared_loadsFeatureFlags() async {
|
||||
XCTAssertTrue(subject.state.featureFlags.isEmpty)
|
||||
|
||||
let flag = DebugMenuFeatureFlag(
|
||||
feature: .testFeatureFlag,
|
||||
isEnabled: false,
|
||||
)
|
||||
|
||||
configService.debugFeatureFlags = [flag]
|
||||
|
||||
await subject.perform(.viewAppeared)
|
||||
|
||||
XCTAssertTrue(subject.state.featureFlags.contains(flag))
|
||||
}
|
||||
|
||||
/// `perform(.refreshFeatureFlags)` refreshs the current feature flags.
|
||||
@MainActor
|
||||
func test_perform_refreshFeatureFlags() async {
|
||||
await subject.perform(.refreshFeatureFlags)
|
||||
XCTAssertTrue(configService.refreshDebugFeatureFlagsCalled)
|
||||
}
|
||||
|
||||
/// `perform(.toggleFeatureFlag)` changes the state of the feature flag.
|
||||
@MainActor
|
||||
func test_perform_toggleFeatureFlag() async {
|
||||
let flag = DebugMenuFeatureFlag(
|
||||
feature: .testFeatureFlag,
|
||||
isEnabled: true,
|
||||
)
|
||||
|
||||
await subject.perform(
|
||||
.toggleFeatureFlag(
|
||||
flag.feature.rawValue,
|
||||
false,
|
||||
),
|
||||
)
|
||||
|
||||
XCTAssertTrue(configService.toggleDebugFeatureFlagCalled)
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuState
|
||||
|
||||
/// The state used to present the `DebugMenuView`.
|
||||
///
|
||||
struct DebugMenuState: Equatable, Sendable {
|
||||
/// The current feature flags supported.
|
||||
var featureFlags: [DebugMenuFeatureFlag] = []
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
// swiftlint:disable:this file_name
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import BitwardenResources
|
||||
import SnapshotTesting
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
// MARK: - DebugMenuViewTests
|
||||
|
||||
class DebugMenuViewTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var processor: MockProcessor<DebugMenuState, DebugMenuAction, DebugMenuEffect>!
|
||||
var subject: DebugMenuView!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
processor = MockProcessor(
|
||||
state: DebugMenuState(
|
||||
featureFlags: [
|
||||
.init(
|
||||
feature: .testFeatureFlag,
|
||||
isEnabled: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
let store = Store(processor: processor)
|
||||
|
||||
subject = DebugMenuView(store: store)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
processor = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// Check the snapshot when feature flags are enabled and disabled.
|
||||
@MainActor
|
||||
func disabletest_snapshot_debugMenuWithFeatureFlags() {
|
||||
processor.state.featureFlags = [
|
||||
.init(
|
||||
feature: .testFeatureFlag,
|
||||
isEnabled: true,
|
||||
),
|
||||
]
|
||||
assertSnapshot(of: subject, as: .defaultPortrait)
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
// swiftlint:disable:this file_name
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import BitwardenResources
|
||||
import ViewInspector
|
||||
import ViewInspectorTestHelpers
|
||||
import XCTest
|
||||
|
||||
@testable import AuthenticatorShared
|
||||
|
||||
// MARK: - DebugMenuViewTests
|
||||
|
||||
class DebugMenuViewTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var processor: MockProcessor<DebugMenuState, DebugMenuAction, DebugMenuEffect>!
|
||||
var subject: DebugMenuView!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
processor = MockProcessor(
|
||||
state: DebugMenuState(
|
||||
featureFlags: [
|
||||
.init(
|
||||
feature: .testFeatureFlag,
|
||||
isEnabled: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
let store = Store(processor: processor)
|
||||
|
||||
subject = DebugMenuView(store: store)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
processor = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// Tapping the close button dispatches the `.dismissTapped` action.
|
||||
@MainActor
|
||||
func test_closeButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.close)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .dismissTapped)
|
||||
}
|
||||
|
||||
/// Tests that the toggle fires off the correct effect.
|
||||
@MainActor
|
||||
func test_featureFlag_toggled() async throws {
|
||||
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) {
|
||||
throw XCTSkip("Unable to run test in iOS 16, keep an eye on ViewInspector to see if it gets updated.")
|
||||
}
|
||||
let featureFlagName = FeatureFlag.testFeatureFlag.rawValue
|
||||
let toggle = try subject.inspect().find(viewWithAccessibilityIdentifier: featureFlagName).toggle()
|
||||
try toggle.tap()
|
||||
XCTAssertEqual(processor.effects.last, .toggleFeatureFlag(featureFlagName, true))
|
||||
}
|
||||
|
||||
/// Test that the refresh button sends the correct effect.
|
||||
@MainActor
|
||||
func disabletest_refreshFeatureFlags_tapped() async throws {
|
||||
let button = try subject.inspect().find(asyncButtonWithAccessibilityLabel: "RefreshFeatureFlagsButton")
|
||||
try await button.tap()
|
||||
XCTAssertEqual(processor.effects.last, .refreshFeatureFlags)
|
||||
}
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
import BitwardenKit
|
||||
import BitwardenResources
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - DebugMenuView
|
||||
|
||||
/// Represents the debug menu for configuring app settings and feature flags.
|
||||
///
|
||||
struct DebugMenuView: View {
|
||||
// MARK: Properties
|
||||
|
||||
/// The store used to render the view.
|
||||
@ObservedObject var store: Store<DebugMenuState, DebugMenuAction, DebugMenuEffect>
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
featureFlags
|
||||
} header: {
|
||||
featureFlagSectionHeader
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
store.send(.dismissTapped)
|
||||
} label: {
|
||||
Text(Localizations.close)
|
||||
}
|
||||
.accessibilityIdentifier("close-debug")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Debug Menu")
|
||||
.task {
|
||||
await store.perform(.viewAppeared)
|
||||
}
|
||||
}
|
||||
|
||||
/// The feature flags currently used in the app.
|
||||
private var featureFlags: some View {
|
||||
ForEach(store.state.featureFlags) { flag in
|
||||
Toggle(
|
||||
isOn: store.bindingAsync(
|
||||
get: { _ in flag.isEnabled },
|
||||
perform: { DebugMenuEffect.toggleFeatureFlag(flag.feature.rawValue, $0) },
|
||||
),
|
||||
) {
|
||||
Text(flag.feature.name)
|
||||
}
|
||||
.toggleStyle(.bitwarden)
|
||||
.accessibilityIdentifier(flag.feature.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// The header for the feature flags section.
|
||||
private var featureFlagSectionHeader: some View {
|
||||
HStack {
|
||||
Text("Feature Flags")
|
||||
Spacer()
|
||||
AsyncButton {
|
||||
await store.perform(.refreshFeatureFlags)
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
}
|
||||
.accessibilityLabel("RefreshFeatureFlagsButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
#Preview {
|
||||
DebugMenuView(
|
||||
store: Store(
|
||||
processor: StateProcessor(
|
||||
state: .init(
|
||||
featureFlags: [
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
#endif
|
||||
@ -1,55 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
/// A UIWindow subclass that detects and responds to shake gestures.
|
||||
///
|
||||
/// This window class allows you to provide a custom handler that will be called whenever a shake
|
||||
/// gesture is detected. This can be particularly useful for triggering debug or testing actions only
|
||||
/// in DEBUG_MENU mode, such as showing development menus or refreshing data.
|
||||
///
|
||||
public class ShakeWindow: UIWindow {
|
||||
/// The callback to be invoked when a shake gesture is detected.
|
||||
public var onShakeDetected: (() -> Void)?
|
||||
|
||||
/// Initializes a new ShakeWindow with a specific window scene and an optional shake detection handler.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - windowScene: The UIWindowScene instance with which the window is associated.
|
||||
/// - onShakeDetected: An optional closure that gets called when a shake gesture is detected.
|
||||
///
|
||||
public init(
|
||||
windowScene: UIWindowScene,
|
||||
onShakeDetected: (() -> Void)?,
|
||||
) {
|
||||
self.onShakeDetected = onShakeDetected
|
||||
super.init(windowScene: windowScene)
|
||||
}
|
||||
|
||||
/// Required initializer for UIWindow subclass. Not implemented as ShakeWindow requires
|
||||
/// a custom initialization method with shake detection handler.
|
||||
///
|
||||
/// - Parameter coder: An NSCoder instance for decoding the window.
|
||||
///
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// Overrides the default motionEnded function to detect shake motions.
|
||||
/// If a shake motion is detected and we are in DEBUG_MENU mode,
|
||||
/// the onShakeDetected closure is called.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - motion: An event-subtype constant indicating the kind of motion.
|
||||
/// - event: An object representing the event associated with the motion.
|
||||
///
|
||||
override public func motionEnded(
|
||||
_ motion: UIEvent.EventSubtype,
|
||||
with event: UIEvent?,
|
||||
) {
|
||||
#if DEBUG_MENU
|
||||
if motion == .motionShake {
|
||||
onShakeDetected?()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB |
@ -26,6 +26,9 @@ class AppCoordinator: Coordinator, HasRootNavigator {
|
||||
/// The coordinator currently being displayed.
|
||||
private var childCoordinator: AnyObject?
|
||||
|
||||
/// Whether the debug menu is currently being shown.
|
||||
private(set) var isShowingDebugMenu = false
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The module to use for creating child coordinators.
|
||||
@ -84,9 +87,7 @@ class AppCoordinator: Coordinator, HasRootNavigator {
|
||||
func navigate(to route: AppRoute, context _: AnyObject?) {
|
||||
switch route {
|
||||
case .debugMenu:
|
||||
#if DEBUG_MENU
|
||||
showDebugMenu()
|
||||
#endif
|
||||
case let .tab(tabRoute):
|
||||
showTab(route: tabRoute)
|
||||
}
|
||||
@ -160,7 +161,6 @@ class AppCoordinator: Coordinator, HasRootNavigator {
|
||||
rootNavigator?.rootViewController?.present(navigationController, animated: false)
|
||||
}
|
||||
|
||||
#if DEBUG_MENU
|
||||
/// Configures and presents the debug menu.
|
||||
///
|
||||
/// Initializes feedback generator for haptic feedback. Sets up a `UINavigationController`
|
||||
@ -168,22 +168,23 @@ class AppCoordinator: Coordinator, HasRootNavigator {
|
||||
/// Presents the navigation controller and triggers haptic feedback upon completion.
|
||||
///
|
||||
private func showDebugMenu() {
|
||||
guard !isShowingDebugMenu else { return }
|
||||
|
||||
let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
||||
feedbackGenerator.prepare()
|
||||
let stackNavigator = UINavigationController()
|
||||
stackNavigator.navigationBar.prefersLargeTitles = true
|
||||
stackNavigator.modalPresentationStyle = .fullScreen
|
||||
let debugMenuCoordinator = module.makeDebugMenuCoordinator(stackNavigator: stackNavigator)
|
||||
let debugMenuCoordinator = module.makeDebugMenuCoordinator(delegate: self, stackNavigator: stackNavigator)
|
||||
debugMenuCoordinator.start()
|
||||
childCoordinator = debugMenuCoordinator
|
||||
|
||||
rootNavigator?.rootViewController?.topmostViewController().present(
|
||||
stackNavigator,
|
||||
animated: true,
|
||||
completion: { feedbackGenerator.impactOccurred() },
|
||||
)
|
||||
isShowingDebugMenu = true
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - AuthCoordinatorDelegate
|
||||
@ -194,6 +195,14 @@ extension AppCoordinator: AuthCoordinatorDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DebugMenuCoordinatorDelegate
|
||||
|
||||
extension AppCoordinator: DebugMenuCoordinatorDelegate {
|
||||
func didDismissDebugMenu() {
|
||||
isShowingDebugMenu = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HasErrorAlertServices
|
||||
|
||||
extension AppCoordinator: HasErrorAlertServices {
|
||||
|
||||
@ -58,6 +58,22 @@ extension DefaultAppModule: AppModule {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DefaultAppModule + DebugMenuModule
|
||||
|
||||
extension DefaultAppModule: DebugMenuModule {
|
||||
public func makeDebugMenuCoordinator(
|
||||
delegate: DebugMenuCoordinatorDelegate,
|
||||
stackNavigator: StackNavigator,
|
||||
) -> AnyCoordinator<DebugMenuRoute, Void> {
|
||||
DebugMenuCoordinator(
|
||||
delegate: delegate,
|
||||
services: services,
|
||||
stackNavigator: stackNavigator,
|
||||
)
|
||||
.asAnyCoordinator()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DefaultAppModule + FlightRecorderModule
|
||||
|
||||
extension DefaultAppModule: FlightRecorderModule {
|
||||
|
||||
@ -38,6 +38,19 @@ class AppModuleTests: BitwardenTestCase {
|
||||
XCTAssertNotNil(rootViewController.childViewController)
|
||||
}
|
||||
|
||||
/// `makeDebugMenuCoordinator()` builds the debug menu coordinator.
|
||||
@MainActor
|
||||
func test_makeDebugMenuCoordinator() {
|
||||
let navigationController = UINavigationController()
|
||||
let coordinator = subject.makeDebugMenuCoordinator(
|
||||
delegate: MockDebugMenuCoordinatorDelegate(),
|
||||
stackNavigator: navigationController,
|
||||
)
|
||||
coordinator.start()
|
||||
XCTAssertEqual(navigationController.viewControllers.count, 1)
|
||||
XCTAssertTrue(navigationController.viewControllers[0] is UIHostingController<DebugMenuView>)
|
||||
}
|
||||
|
||||
/// `makeNavigationController()` builds a navigation controller.
|
||||
@MainActor
|
||||
func test_makeNavigationController() {
|
||||
|
||||
22
BitwardenKit/Core/Platform/Extensions/Array+Extensions.swift
Normal file
22
BitwardenKit/Core/Platform/Extensions/Array+Extensions.swift
Normal file
@ -0,0 +1,22 @@
|
||||
// MARK: - Array + Extensions
|
||||
|
||||
public extension Array {
|
||||
/// Safely access elements in an array by index without running into an out-of-bounds error.
|
||||
/// This works like normal array subscript access, but if the index is out of bounds, then
|
||||
/// returns nil instead of throwing an error. This can be useful in cases, particularly in tests,
|
||||
/// where we want to access array elements by index number, and not have additional error handling
|
||||
/// if the index in question does not exist in the array.
|
||||
///
|
||||
/// This can be used in a subscript. For example, `array[safeIndex: 2]`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - safeIndex: The position of the element to access.
|
||||
/// - Returns: The element at the specified index if it is within bounds, otherwise `nil`.
|
||||
subscript(safeIndex index: Int) -> Element? {
|
||||
guard index >= 0, index < endIndex else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self[index]
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import BitwardenKit
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
class ArrayExtensionsTests: BitwardenTestCase {
|
||||
// MARK: Tests
|
||||
|
||||
@ -62,7 +62,7 @@ class ErrorReportBuilderTests: BitwardenTestCase {
|
||||
callStack: exampleCallStack,
|
||||
)
|
||||
// swiftlint:disable line_length
|
||||
assertInlineSnapshot(of: errorReport.replacingHexAddresses(), as: .lines) {
|
||||
assertInlineSnapshot(of: errorReport.zeroingUnwantedHexStrings(), as: .lines) {
|
||||
#"""
|
||||
Swift.DecodingError.keyNotFound(TestKeys(stringValue: "ciphers", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"ciphers\", intValue: nil).", underlyingError: nil))
|
||||
The data couldn’t be read because it is missing.
|
||||
@ -78,6 +78,7 @@ class ErrorReportBuilderTests: BitwardenTestCase {
|
||||
AuthenticatorBridgeKitMocks: 0x0000000000000000
|
||||
BitwardenKit: 0x0000000000000000
|
||||
BitwardenKitMocks: 0x0000000000000000
|
||||
BitwardenSdk_0000000000000000_PackageProduct: 0x0000000000000000
|
||||
BitwardenResources: 0x0000000000000000
|
||||
AuthenticatorBridgeKit: 0x0000000000000000
|
||||
|
||||
@ -99,7 +100,7 @@ class ErrorReportBuilderTests: BitwardenTestCase {
|
||||
for: BitwardenTestError.example,
|
||||
callStack: exampleCallStack,
|
||||
)
|
||||
assertInlineSnapshot(of: errorReport.replacingHexAddresses(), as: .lines) {
|
||||
assertInlineSnapshot(of: errorReport.zeroingUnwantedHexStrings(), as: .lines) {
|
||||
"""
|
||||
TestHelpers.BitwardenTestError.example
|
||||
An example error used to test throwing capabilities.
|
||||
@ -115,6 +116,7 @@ class ErrorReportBuilderTests: BitwardenTestCase {
|
||||
AuthenticatorBridgeKitMocks: 0x0000000000000000
|
||||
BitwardenKit: 0x0000000000000000
|
||||
BitwardenKitMocks: 0x0000000000000000
|
||||
BitwardenSdk_0000000000000000_PackageProduct: 0x0000000000000000
|
||||
BitwardenResources: 0x0000000000000000
|
||||
AuthenticatorBridgeKit: 0x0000000000000000
|
||||
|
||||
@ -134,7 +136,7 @@ class ErrorReportBuilderTests: BitwardenTestCase {
|
||||
for: BitwardenTestError.example,
|
||||
callStack: exampleCallStack,
|
||||
)
|
||||
assertInlineSnapshot(of: errorReport.replacingHexAddresses(), as: .lines) {
|
||||
assertInlineSnapshot(of: errorReport.zeroingUnwantedHexStrings(), as: .lines) {
|
||||
"""
|
||||
TestHelpers.BitwardenTestError.example
|
||||
An example error used to test throwing capabilities.
|
||||
@ -150,6 +152,7 @@ class ErrorReportBuilderTests: BitwardenTestCase {
|
||||
AuthenticatorBridgeKitMocks: 0x0000000000000000
|
||||
BitwardenKit: 0x0000000000000000
|
||||
BitwardenKitMocks: 0x0000000000000000
|
||||
BitwardenSdk_0000000000000000_PackageProduct: 0x0000000000000000
|
||||
BitwardenResources: 0x0000000000000000
|
||||
AuthenticatorBridgeKit: 0x0000000000000000
|
||||
|
||||
@ -165,10 +168,18 @@ class ErrorReportBuilderTests: BitwardenTestCase {
|
||||
|
||||
private extension String {
|
||||
/// Replaces any hex addresses within a string with all zeros.
|
||||
func replacingHexAddresses() -> String {
|
||||
let pattern = "0x[0-9a-fA-F]{12,16}" // Matches 12 to 16 hex digits after '0x'
|
||||
let replacement = "0x0000000000000000"
|
||||
func zeroingUnwantedHexStrings() -> String {
|
||||
let hexAddressPattern = "0x[0-9a-fA-F]{12,16}" // Matches 12 to 16 hex digits after '0x'
|
||||
let hexAddressReplacement = "0x0000000000000000"
|
||||
|
||||
let sdkAddressPattern = "_[0-9a-fA-F]{12,16}_" // Matches 12 to 16 hex digits between underscores
|
||||
let sdkAddressReplacement = "_0000000000000000_"
|
||||
|
||||
return applyingRegularExpression(pattern: hexAddressPattern, replacement: hexAddressReplacement)
|
||||
.applyingRegularExpression(pattern: sdkAddressPattern, replacement: sdkAddressReplacement)
|
||||
}
|
||||
|
||||
func applyingRegularExpression(pattern: String, replacement: String) -> String {
|
||||
do {
|
||||
let regex = try NSRegularExpression(pattern: pattern, options: [])
|
||||
let range = NSRange(startIndex ..< endIndex, in: self)
|
||||
|
||||
@ -3,8 +3,11 @@ import UIKit
|
||||
/// A UIWindow subclass that detects and responds to shake gestures.
|
||||
///
|
||||
/// This window class allows you to provide a custom handler that will be called whenever a shake
|
||||
/// gesture is detected. This can be particularly useful for triggering debug or testing actions only
|
||||
/// in DEBUG_MENU mode, such as showing development menus or refreshing data.
|
||||
/// gesture is detected. **Note:** The shake detection only functions when compiled with the
|
||||
/// `DEBUG_MENU` conditional compilation flag. In release builds, shake gestures are ignored.
|
||||
///
|
||||
/// This is particularly useful for triggering debug menus or testing actions in development builds
|
||||
/// while ensuring the functionality is completely removed from production builds.
|
||||
///
|
||||
public class ShakeWindow: UIWindow {
|
||||
/// The callback to be invoked when a shake gesture is detected.
|
||||
@ -1,5 +1,3 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuAction
|
||||
|
||||
/// Actions that can be processed by a `DebugMenuProcessor`.
|
||||
@ -11,4 +9,6 @@ enum DebugMenuAction: Equatable {
|
||||
case generateCrash
|
||||
/// The generate error report button was tapped.
|
||||
case generateErrorReport
|
||||
/// The generate SDK error report button was tapped.
|
||||
case generateSdkErrorReport
|
||||
}
|
||||
@ -1,9 +1,8 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
/// An object that is notified when the debug menu is dismissed.
|
||||
///
|
||||
protocol DebugMenuCoordinatorDelegate: AnyObject {
|
||||
public protocol DebugMenuCoordinatorDelegate: AnyObject { // sourcery: AutoMockable
|
||||
/// The debug menu has been dismissed.
|
||||
///
|
||||
func didDismissDebugMenu()
|
||||
@ -11,11 +10,10 @@ protocol DebugMenuCoordinatorDelegate: AnyObject {
|
||||
|
||||
/// A coordinator that manages navigation for the debug menu.
|
||||
///
|
||||
final class DebugMenuCoordinator: Coordinator, HasStackNavigator {
|
||||
public final class DebugMenuCoordinator: Coordinator, HasStackNavigator {
|
||||
// MARK: Types
|
||||
|
||||
typealias Services = HasAppSettingsStore
|
||||
& HasConfigService
|
||||
public typealias Services = HasConfigService
|
||||
& HasErrorAlertServices.ErrorAlertServices
|
||||
& HasErrorReporter
|
||||
|
||||
@ -30,7 +28,7 @@ final class DebugMenuCoordinator: Coordinator, HasStackNavigator {
|
||||
// MARK: Properties
|
||||
|
||||
/// The stack navigator that is managed by this coordinator.
|
||||
private(set) weak var stackNavigator: StackNavigator?
|
||||
public private(set) weak var stackNavigator: StackNavigator?
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
@ -41,7 +39,7 @@ final class DebugMenuCoordinator: Coordinator, HasStackNavigator {
|
||||
/// - services: The services used by this coordinator.
|
||||
/// - stackNavigator: The stack navigator that is managed by this coordinator.
|
||||
///
|
||||
init(
|
||||
public init(
|
||||
delegate: DebugMenuCoordinatorDelegate,
|
||||
services: Services,
|
||||
stackNavigator: StackNavigator,
|
||||
@ -53,7 +51,7 @@ final class DebugMenuCoordinator: Coordinator, HasStackNavigator {
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
func navigate(
|
||||
public func navigate(
|
||||
to route: DebugMenuRoute,
|
||||
context: AnyObject?,
|
||||
) {
|
||||
@ -66,7 +64,7 @@ final class DebugMenuCoordinator: Coordinator, HasStackNavigator {
|
||||
}
|
||||
|
||||
/// Starts the process of displaying the debug menu.
|
||||
func start() {
|
||||
public func start() {
|
||||
showDebugMenu()
|
||||
}
|
||||
|
||||
@ -88,5 +86,5 @@ final class DebugMenuCoordinator: Coordinator, HasStackNavigator {
|
||||
// MARK: - HasErrorAlertServices
|
||||
|
||||
extension DebugMenuCoordinator: HasErrorAlertServices {
|
||||
var errorAlertServices: ErrorAlertServices { services }
|
||||
public var errorAlertServices: ErrorAlertServices { services }
|
||||
}
|
||||
@ -1,13 +1,11 @@
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
class DebugMenuCoordinatorTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var appSettingsStore: MockAppSettingsStore!
|
||||
var configService: MockConfigService!
|
||||
var delegate: MockDebugMenuCoordinatorDelegate!
|
||||
var stackNavigator: MockStackNavigator!
|
||||
@ -18,7 +16,6 @@ class DebugMenuCoordinatorTests: BitwardenTestCase {
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
appSettingsStore = MockAppSettingsStore()
|
||||
configService = MockConfigService()
|
||||
delegate = MockDebugMenuCoordinatorDelegate()
|
||||
stackNavigator = MockStackNavigator()
|
||||
@ -26,7 +23,6 @@ class DebugMenuCoordinatorTests: BitwardenTestCase {
|
||||
subject = DebugMenuCoordinator(
|
||||
delegate: delegate,
|
||||
services: ServiceContainer.withMocks(
|
||||
appSettingsStore: appSettingsStore,
|
||||
configService: configService,
|
||||
),
|
||||
stackNavigator: stackNavigator,
|
||||
@ -36,7 +32,6 @@ class DebugMenuCoordinatorTests: BitwardenTestCase {
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
appSettingsStore = nil
|
||||
configService = nil
|
||||
delegate = nil
|
||||
stackNavigator = nil
|
||||
@ -45,6 +40,12 @@ class DebugMenuCoordinatorTests: BitwardenTestCase {
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// The coordinator has error alert services.
|
||||
@MainActor
|
||||
func test_errorAlertServices() {
|
||||
XCTAssertNotNil(subject.errorAlertServices)
|
||||
}
|
||||
|
||||
/// `navigate(to:)` with `.dismiss` dismisses the view.
|
||||
@MainActor
|
||||
func test_navigate_dismiss() throws {
|
||||
@ -63,11 +64,3 @@ class DebugMenuCoordinatorTests: BitwardenTestCase {
|
||||
XCTAssertTrue(stackNavigator.actions.last?.view is DebugMenuView)
|
||||
}
|
||||
}
|
||||
|
||||
class MockDebugMenuCoordinatorDelegate: DebugMenuCoordinatorDelegate {
|
||||
var didDismissDebugMenuCalled = false
|
||||
|
||||
func didDismissDebugMenu() {
|
||||
didDismissDebugMenuCalled = true
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,3 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuEffect
|
||||
|
||||
/// Effects that can be processed by a `DebugMenuProcessor`.
|
||||
@ -5,26 +5,16 @@ import Foundation
|
||||
|
||||
/// An object that builds coordinator for the debug menu.
|
||||
@MainActor
|
||||
protocol DebugMenuModule {
|
||||
public protocol DebugMenuModule {
|
||||
/// Initializes a coordinator for navigating between `DebugMenuRoute`s.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - delegate: The delegate for the debug menu coordinator.
|
||||
/// - stackNavigator: The stack navigator that will be used to navigate between routes.
|
||||
/// - Returns: A coordinator that can navigate to `DebugMenuRoute`s.
|
||||
///
|
||||
func makeDebugMenuCoordinator(
|
||||
delegate: DebugMenuCoordinatorDelegate,
|
||||
stackNavigator: StackNavigator,
|
||||
) -> AnyCoordinator<DebugMenuRoute, Void>
|
||||
}
|
||||
|
||||
extension DefaultAppModule: DebugMenuModule {
|
||||
func makeDebugMenuCoordinator(
|
||||
stackNavigator: StackNavigator,
|
||||
) -> AnyCoordinator<DebugMenuRoute, Void> {
|
||||
DebugMenuCoordinator(
|
||||
services: services,
|
||||
stackNavigator: stackNavigator,
|
||||
)
|
||||
.asAnyCoordinator()
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import Foundation
|
||||
|
||||
@ -20,6 +19,18 @@ final class DebugMenuProcessor: StateProcessor<DebugMenuState, DebugMenuAction,
|
||||
/// The services used by the processor.
|
||||
private let services: Services
|
||||
|
||||
// MARK: Computed Properties
|
||||
|
||||
/// The current feature flags. This requires `FeatureFlag` to have been extended in the executable's
|
||||
/// namespace to conform to `CaseIterable`.
|
||||
private var currentFeatureFlags: [FeatureFlag] {
|
||||
guard let featureFlagType = FeatureFlag.self as? any CaseIterable.Type,
|
||||
let flags = featureFlagType.allCases as? [FeatureFlag] else {
|
||||
return []
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initializes a `DebugMenuProcessor`.
|
||||
@ -48,10 +59,21 @@ final class DebugMenuProcessor: StateProcessor<DebugMenuState, DebugMenuAction,
|
||||
case .generateCrash:
|
||||
preconditionFailure("Generated crash from debug view.")
|
||||
case .generateErrorReport:
|
||||
services.errorReporter.log(
|
||||
error: FlightRecorderError.fileSizeError(
|
||||
NSError(
|
||||
domain: "Generated Error",
|
||||
code: 0,
|
||||
userInfo: [
|
||||
"AdditionalMessage": "Generated error report from debug view.",
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
case .generateSdkErrorReport:
|
||||
services.errorReporter.log(error: BitwardenSdk.BitwardenError.Api(ApiError.ResponseContent(
|
||||
message: "Generated error report from debug view.",
|
||||
message: "Generated SDK error report from debug view.",
|
||||
)))
|
||||
services.errorReporter.log(error: KeychainServiceError.osStatusError(1))
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +88,7 @@ final class DebugMenuProcessor: StateProcessor<DebugMenuState, DebugMenuAction,
|
||||
name: flag,
|
||||
newValue: newValue,
|
||||
)
|
||||
state.featureFlags = await services.configService.getDebugFeatureFlags(FeatureFlag.allCases)
|
||||
state.featureFlags = await services.configService.getDebugFeatureFlags(currentFeatureFlags)
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,11 +96,11 @@ final class DebugMenuProcessor: StateProcessor<DebugMenuState, DebugMenuAction,
|
||||
|
||||
/// Fetch the current debug feature flags.
|
||||
private func fetchFlags() async {
|
||||
state.featureFlags = await services.configService.getDebugFeatureFlags(FeatureFlag.allCases)
|
||||
state.featureFlags = await services.configService.getDebugFeatureFlags(currentFeatureFlags)
|
||||
}
|
||||
|
||||
/// Refreshes the feature flags by resetting their local values and fetching the latest configurations.
|
||||
private func refreshFlags() async {
|
||||
state.featureFlags = await services.configService.refreshDebugFeatureFlags(FeatureFlag.allCases)
|
||||
state.featureFlags = await services.configService.refreshDebugFeatureFlags(currentFeatureFlags)
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,8 @@
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import BitwardenSdk
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
@testable import BitwardenKit
|
||||
|
||||
class DebugMenuProcessorTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
@ -66,7 +65,7 @@ class DebugMenuProcessorTests: BitwardenTestCase {
|
||||
XCTAssertTrue(subject.state.featureFlags.contains(flag))
|
||||
}
|
||||
|
||||
/// `perform(.refreshFeatureFlags)` refreshs the current feature flags.
|
||||
/// `perform(.refreshFeatureFlags)` refreshes the current feature flags.
|
||||
@MainActor
|
||||
func test_perform_refreshFeatureFlags() async {
|
||||
await subject.perform(.refreshFeatureFlags)
|
||||
@ -91,19 +90,33 @@ class DebugMenuProcessorTests: BitwardenTestCase {
|
||||
XCTAssertTrue(configService.toggleDebugFeatureFlagCalled)
|
||||
}
|
||||
|
||||
/// `receive()` with `.generateErrorReport` generates error reports on the error reporter.
|
||||
/// `receive()` with `.generateErrorReport` sends an error report to the error reporter.
|
||||
@MainActor
|
||||
func test_receive_generateErrorReport() {
|
||||
subject.receive(.generateErrorReport)
|
||||
XCTAssertEqual(
|
||||
errorReporter.errors[0] as? BitwardenSdk.BitwardenError,
|
||||
BitwardenSdk.BitwardenError.Api(ApiError.ResponseContent(
|
||||
message: "Generated error report from debug view.",
|
||||
)),
|
||||
errorReporter.errors[safeIndex: 0] as? FlightRecorderError,
|
||||
FlightRecorderError.fileSizeError(
|
||||
NSError(
|
||||
domain: "Generated Error",
|
||||
code: 0,
|
||||
userInfo: [
|
||||
"AdditionalMessage": "Generated error report from debug view.",
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// `receive()` with `.generateSdkErrorReport` sends an SDK error report to the error reporter.
|
||||
@MainActor
|
||||
func test_receive_generateSdkErrorReport() {
|
||||
subject.receive(.generateSdkErrorReport)
|
||||
XCTAssertEqual(
|
||||
errorReporter.errors[1] as? KeychainServiceError,
|
||||
KeychainServiceError.osStatusError(1),
|
||||
errorReporter.errors[safeIndex: 0] as? BitwardenSdk.BitwardenError,
|
||||
BitwardenSdk.BitwardenError.Api(ApiError.ResponseContent(
|
||||
message: "Generated SDK error report from debug view.",
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,3 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuRoute
|
||||
|
||||
/// A route to specific screens in the` DebugMenuView`
|
||||
@ -1,6 +1,3 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuState
|
||||
|
||||
/// The state used to present the `DebugMenuView`.
|
||||
@ -1,11 +1,10 @@
|
||||
// swiftlint:disable:this file_name
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import BitwardenResources
|
||||
import SnapshotTesting
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
@testable import BitwardenKit
|
||||
|
||||
// MARK: - DebugMenuViewTests
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
// swiftlint:disable:this file_name
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import BitwardenResources
|
||||
import ViewInspectorTestHelpers
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
@testable import BitwardenKit
|
||||
|
||||
// MARK: - DebugMenuViewTests
|
||||
|
||||
@ -47,7 +46,7 @@ class DebugMenuViewTests: BitwardenTestCase {
|
||||
/// Tapping the close button dispatches the `.dismissTapped` action.
|
||||
@MainActor
|
||||
func test_closeButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.close)
|
||||
let button = try subject.inspect().findCloseToolbarButton()
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .dismissTapped)
|
||||
}
|
||||
@ -80,6 +79,14 @@ class DebugMenuViewTests: BitwardenTestCase {
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .generateErrorReport)
|
||||
}
|
||||
|
||||
/// Tapping the generate SDK error report button dispatches the `.generateSdkErrorReport` action.
|
||||
@MainActor
|
||||
func test_generateSdkErrorReport_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.generateSdkErrorReport)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .generateSdkErrorReport)
|
||||
}
|
||||
|
||||
/// Test that the refresh button sends the correct effect.
|
||||
@MainActor
|
||||
func disabletest_refreshFeatureFlags_tapped() async throws {
|
||||
@ -1,4 +1,3 @@
|
||||
import BitwardenKit
|
||||
import BitwardenResources
|
||||
import SwiftUI
|
||||
|
||||
@ -6,7 +5,7 @@ import SwiftUI
|
||||
|
||||
/// Represents the debug menu for configuring app settings and feature flags.
|
||||
///
|
||||
struct DebugMenuView: View {
|
||||
public struct DebugMenuView: View {
|
||||
// MARK: Properties
|
||||
|
||||
/// The store used to render the view.
|
||||
@ -14,7 +13,7 @@ struct DebugMenuView: View {
|
||||
|
||||
// MARK: View
|
||||
|
||||
var body: some View {
|
||||
public var body: some View {
|
||||
List {
|
||||
Section {
|
||||
featureFlags
|
||||
@ -29,12 +28,9 @@ struct DebugMenuView: View {
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
closeToolbarButton {
|
||||
store.send(.dismissTapped)
|
||||
} label: {
|
||||
Text(Localizations.close)
|
||||
}
|
||||
.accessibilityIdentifier("close-debug")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Debug Menu")
|
||||
@ -52,6 +48,12 @@ struct DebugMenuView: View {
|
||||
Text(Localizations.generateErrorReport)
|
||||
}
|
||||
.accessibilityIdentifier("GenerateErrorReportButton")
|
||||
Button {
|
||||
store.send(.generateSdkErrorReport)
|
||||
} label: {
|
||||
Text(Localizations.generateSdkErrorReport)
|
||||
}
|
||||
.accessibilityIdentifier("GenerateSdkErrorReportButton")
|
||||
Button {
|
||||
store.send(.generateCrash)
|
||||
} label: {
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@ -1052,6 +1052,7 @@
|
||||
"GotIt" = "Got it";
|
||||
"GenerateCrash" = "Generate crash";
|
||||
"GenerateErrorReport" = "Generate error report";
|
||||
"GenerateSdkErrorReport" = "Generate SDK error report";
|
||||
"AllowAuthenticatorSyncing" = "Allow authenticator syncing";
|
||||
"AuthenticatorSync" = "Authenticator sync";
|
||||
"NoAccountFoundPleaseLogInAgainIfYouContinueToSeeThisError" = "No account found. Please log in again if you continue to see this error.";
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
extension Array {
|
||||
/// Gets the element on the index when inside the bounds
|
||||
/// of the array, otherwise returns `nil`.
|
||||
subscript(safeIndex index: Int) -> Element? {
|
||||
guard index >= 0, index < endIndex else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self[index]
|
||||
}
|
||||
}
|
||||
@ -64,6 +64,22 @@ extension DefaultAppModule: AppModule {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DefaultAppModule + DebugMenuModule
|
||||
|
||||
extension DefaultAppModule: DebugMenuModule {
|
||||
public func makeDebugMenuCoordinator(
|
||||
delegate: DebugMenuCoordinatorDelegate,
|
||||
stackNavigator: StackNavigator,
|
||||
) -> AnyCoordinator<DebugMenuRoute, Void> {
|
||||
DebugMenuCoordinator(
|
||||
delegate: delegate,
|
||||
services: services,
|
||||
stackNavigator: stackNavigator,
|
||||
)
|
||||
.asAnyCoordinator()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DefaultAppModule + FlightRecorderModule
|
||||
|
||||
extension DefaultAppModule: FlightRecorderModule {
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuModule
|
||||
|
||||
/// An object that builds coordinator for the debug menu.
|
||||
@MainActor
|
||||
protocol DebugMenuModule {
|
||||
/// Initializes a coordinator for navigating between `DebugMenuRoute`s.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - delegate: The delegate for the debug menu coordinator.
|
||||
/// - stackNavigator: The stack navigator that will be used to navigate between routes.
|
||||
/// - Returns: A coordinator that can navigate to `DebugMenuRoute`s.
|
||||
///
|
||||
func makeDebugMenuCoordinator(
|
||||
delegate: DebugMenuCoordinatorDelegate,
|
||||
stackNavigator: StackNavigator,
|
||||
) -> AnyCoordinator<DebugMenuRoute, Void>
|
||||
}
|
||||
|
||||
extension DefaultAppModule: DebugMenuModule {
|
||||
func makeDebugMenuCoordinator(
|
||||
delegate: DebugMenuCoordinatorDelegate,
|
||||
stackNavigator: StackNavigator,
|
||||
) -> AnyCoordinator<DebugMenuRoute, Void> {
|
||||
DebugMenuCoordinator(
|
||||
delegate: delegate,
|
||||
services: services,
|
||||
stackNavigator: stackNavigator,
|
||||
)
|
||||
.asAnyCoordinator()
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - DebugMenuRoute
|
||||
|
||||
/// A route to specific screens in the` DebugMenuView`
|
||||
public enum DebugMenuRoute: Equatable, Hashable {
|
||||
/// A route to dismiss the screen currently presented modally.
|
||||
case dismiss
|
||||
}
|
||||
@ -23,6 +23,7 @@ class MockAppModule:
|
||||
var authRouter = MockRouter<AuthEvent, AuthRoute>(routeForEvent: { _ in .vaultUnlock })
|
||||
var authenticatorItemCoordinator = MockCoordinator<AuthenticatorItemRoute, AuthenticatorItemEvent>()
|
||||
var debugMenuCoordinator = MockCoordinator<DebugMenuRoute, Void>()
|
||||
var debugMenuCoordinatorDelegate: DebugMenuCoordinatorDelegate?
|
||||
var fileSelectionDelegate: FileSelectionDelegate?
|
||||
var fileSelectionCoordinator = MockCoordinator<FileSelectionRoute, FileSelectionEvent>()
|
||||
var flightRecorderCoordinator = MockCoordinator<FlightRecorderRoute, Void>()
|
||||
@ -61,9 +62,11 @@ class MockAppModule:
|
||||
}
|
||||
|
||||
func makeDebugMenuCoordinator(
|
||||
delegate: DebugMenuCoordinatorDelegate,
|
||||
stackNavigator: StackNavigator,
|
||||
) -> AnyCoordinator<DebugMenuRoute, Void> {
|
||||
debugMenuCoordinator.asAnyCoordinator()
|
||||
debugMenuCoordinatorDelegate = delegate
|
||||
return debugMenuCoordinator.asAnyCoordinator()
|
||||
}
|
||||
|
||||
func makeFileSelectionCoordinator(
|
||||
|
||||
@ -263,6 +263,7 @@ targets:
|
||||
- target: BitwardenKit/AuthenticatorBridgeKitMocks
|
||||
- target: BitwardenKit/BitwardenKitMocks
|
||||
- target: BitwardenKit/TestHelpers
|
||||
- package: BitwardenSdk
|
||||
- package: SnapshotTesting
|
||||
product: InlineSnapshotTesting
|
||||
randomExecutionOrder: true
|
||||
@ -287,6 +288,7 @@ targets:
|
||||
- target: BitwardenKit/AuthenticatorBridgeKitMocks
|
||||
- target: BitwardenKit/BitwardenKitMocks
|
||||
- target: BitwardenKit/TestHelpers
|
||||
- package: BitwardenSdk
|
||||
- package: SnapshotTesting
|
||||
- package: SnapshotTesting
|
||||
product: InlineSnapshotTesting
|
||||
@ -313,5 +315,6 @@ targets:
|
||||
- target: BitwardenKit/BitwardenKitMocks
|
||||
- target: BitwardenKit/TestHelpers
|
||||
- target: BitwardenKit/ViewInspectorTestHelpers
|
||||
- package: BitwardenSdk
|
||||
- package: ViewInspector
|
||||
randomExecutionOrder: true
|
||||
|
||||
@ -141,6 +141,7 @@ targets:
|
||||
- "**/sourcery.yml"
|
||||
buildPhase: none
|
||||
dependencies:
|
||||
- package: BitwardenSdk
|
||||
- package: SwiftUIIntrospect
|
||||
- target: BitwardenResources
|
||||
- target: Networking
|
||||
@ -190,6 +191,7 @@ targets:
|
||||
- target: BitwardenKit
|
||||
- target: BitwardenKitMocks
|
||||
- target: TestHelpers
|
||||
- package: BitwardenSdk
|
||||
- package: SnapshotTesting
|
||||
product: InlineSnapshotTesting
|
||||
randomExecutionOrder: true
|
||||
|
||||
@ -184,6 +184,7 @@ targets:
|
||||
- target: BitwardenKit/BitwardenKit
|
||||
- target: BitwardenKit/BitwardenResources
|
||||
- target: BitwardenKit/Networking
|
||||
- package: BitwardenSdk
|
||||
- package: Firebase
|
||||
product: FirebaseCrashlytics
|
||||
preBuildScripts:
|
||||
@ -247,6 +248,7 @@ targets:
|
||||
- "**/*Tests.*"
|
||||
- "**/TestHelpers/*"
|
||||
dependencies:
|
||||
- package: BitwardenSdk
|
||||
- target: BitwardenShared
|
||||
BitwardenActionExtensionTests:
|
||||
type: bundle.unit-test
|
||||
@ -282,6 +284,7 @@ targets:
|
||||
- "**/*Tests.*"
|
||||
- "**/TestHelpers/*"
|
||||
dependencies:
|
||||
- package: BitwardenSdk
|
||||
- target: BitwardenShared
|
||||
BitwardenAutoFillExtensionTests:
|
||||
type: bundle.unit-test
|
||||
@ -317,6 +320,7 @@ targets:
|
||||
- "**/*Tests.*"
|
||||
- "**/TestHelpers/*"
|
||||
dependencies:
|
||||
- package: BitwardenSdk
|
||||
- target: BitwardenShared
|
||||
BitwardenShareExtensionTests:
|
||||
type: bundle.unit-test
|
||||
@ -425,6 +429,7 @@ targets:
|
||||
- target: BitwardenKit/AuthenticatorBridgeKitMocks
|
||||
- target: BitwardenKit/BitwardenKitMocks
|
||||
- target: BitwardenKit/TestHelpers
|
||||
- package: BitwardenSdk
|
||||
- package: SnapshotTesting
|
||||
product: InlineSnapshotTesting
|
||||
randomExecutionOrder: true
|
||||
@ -455,6 +460,7 @@ targets:
|
||||
- target: BitwardenShared
|
||||
- target: BitwardenKit/BitwardenKitMocks
|
||||
- target: BitwardenKit/TestHelpers
|
||||
- package: BitwardenSdk
|
||||
- package: SnapshotTesting
|
||||
- package: SnapshotTesting
|
||||
product: InlineSnapshotTesting
|
||||
@ -487,6 +493,7 @@ targets:
|
||||
- target: BitwardenKit/BitwardenKitMocks
|
||||
- target: BitwardenKit/TestHelpers
|
||||
- target: BitwardenKit/ViewInspectorTestHelpers
|
||||
- package: BitwardenSdk
|
||||
- package: ViewInspector
|
||||
randomExecutionOrder: true
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user