[PM-26063] Add Flight Recorder toast banner to Authenticator's item list (#2151)

This commit is contained in:
Matt Czech 2025-11-19 16:33:25 -06:00 committed by GitHub
parent 28d94d4be2
commit bdc2c53927
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 376 additions and 22 deletions

View File

@ -134,6 +134,7 @@ class AppCoordinator: Coordinator, HasRootNavigator {
let tabNavigator = BitwardenTabBarController()
let coordinator = module.makeTabCoordinator(
errorReporter: services.errorReporter,
itemListDelegate: self,
rootNavigator: rootNavigator,
tabNavigator: tabNavigator,
)
@ -198,3 +199,11 @@ extension AppCoordinator: AuthCoordinatorDelegate {
extension AppCoordinator: HasErrorAlertServices {
var errorAlertServices: ErrorAlertServices { services }
}
// MARK: - ItemListCoordinatorDelegate
extension AppCoordinator: ItemListCoordinatorDelegate {
func switchToSettingsTab(route: SettingsRoute) {
showTab(route: .settings(route))
}
}

View File

@ -0,0 +1,44 @@
import BitwardenKit
import BitwardenKitMocks
import Testing
@testable import AuthenticatorShared
@MainActor
struct AppCoordinatorTests {
// MARK: Properties
let module = MockAppModule()
let rootNavigator = MockRootNavigator()
let services = ServiceContainer.withMocks()
let subject: AppCoordinator
// MARK: Initialization
init() {
subject = AppCoordinator(
appContext: .mainApp,
module: module,
rootNavigator: rootNavigator,
services: services,
)
}
// MARK: Tests
/// `switchToSettingsTab(route:)` navigates to the settings tab with the specified route.
@Test
func switchToSettingsTab_navigatesToSettingsTab() {
subject.switchToSettingsTab(route: .settings)
#expect(module.tabCoordinator.routes.last == .settings(.settings))
}
/// `switchToSettingsTab(route:)` navigates to the settings tab with the export items route.
@Test
func switchToSettingsTab_navigatesToExportItems() {
subject.switchToSettingsTab(route: .exportItems)
#expect(module.tabCoordinator.routes.last == .settings(.exportItems))
}
}

View File

@ -30,6 +30,9 @@ final class TabCoordinator: Coordinator, HasTabNavigator {
/// The coordinator used to navigate to `ItemListRoute`s.
private var itemListCoordinator: AnyCoordinator<ItemListRoute, ItemListEvent>?
/// A delegate of the `ItemListCoordinator`.
private weak var itemListDelegate: ItemListCoordinatorDelegate?
/// The module used to create child coordinators.
private let module: Module
@ -45,20 +48,20 @@ final class TabCoordinator: Coordinator, HasTabNavigator {
///
/// - Parameters:
/// - errorReporter: The error reporter used by the tab coordinator.
/// - itemListDelegate: A delegate of the `ItemListCoordinator`.
/// - module: The module used to create child coordinators.
/// - rootNavigator: The root navigator used to display this coordinator's interface.
/// - settingsDelegate: A delegate of the `SettingsCoordinator`.
/// - tabNavigator: The tab navigator that is managed by this coordinator.
/// - vaultDelegate: A delegate of the `VaultCoordinator`.
/// - vaultRepository: A vault repository used to the vault tab title.
///
init(
errorReporter: ErrorReporter,
itemListDelegate: ItemListCoordinatorDelegate,
module: Module,
rootNavigator: RootNavigator,
tabNavigator: TabNavigator,
) {
self.errorReporter = errorReporter
self.itemListDelegate = itemListDelegate
self.module = module
self.rootNavigator = rootNavigator
self.tabNavigator = tabNavigator
@ -82,13 +85,14 @@ final class TabCoordinator: Coordinator, HasTabNavigator {
}
func start() {
guard let rootNavigator, let tabNavigator else { return }
guard let itemListDelegate, let rootNavigator, let tabNavigator else { return }
rootNavigator.show(child: tabNavigator)
let itemListNavigator = module.makeNavigationController()
itemListNavigator.navigationBar.prefersLargeTitles = true
itemListCoordinator = module.makeItemListCoordinator(
delegate: itemListDelegate,
stackNavigator: itemListNavigator,
)

View File

@ -0,0 +1,70 @@
import BitwardenKit
import BitwardenKitMocks
import Testing
@testable import AuthenticatorShared
@MainActor
struct TabCoordinatorTests {
// MARK: Properties
let errorReporter = MockErrorReporter()
let itemListDelegate = MockItemListCoordinatorDelegate()
let module = MockAppModule()
let rootNavigator = MockRootNavigator()
let tabNavigator = MockTabNavigator()
let subject: TabCoordinator
// MARK: Initialization
init() {
subject = TabCoordinator(
errorReporter: errorReporter,
itemListDelegate: itemListDelegate,
module: module,
rootNavigator: rootNavigator,
tabNavigator: tabNavigator,
)
}
// MARK: Tests
/// `start()` shows the tab navigator as a child of the root navigator.
@Test
func start_showsTabNavigator() {
subject.start()
#expect(rootNavigator.navigatorShown === tabNavigator)
}
/// `start()` sets up the tab navigator with the correct navigators.
@Test
func start_setsNavigators() {
subject.start()
#expect(tabNavigator.navigators.count == 2)
}
/// `navigate(to:)` with `.itemList` navigates to the item list route.
@Test
func navigateTo_itemList() {
subject.start()
subject.navigate(to: .itemList(.list), context: nil)
#expect(module.itemListCoordinator.routes == [.list])
#expect(module.itemListCoordinatorDelegate === itemListDelegate)
#expect(tabNavigator.selectedIndex == TabRoute.itemList(.list).index)
}
/// `navigate(to:)` with `.settings` navigates to the settings route.
@Test
func navigateTo_settings() {
subject.start()
subject.navigate(to: .settings(.settings), context: nil)
#expect(module.settingsCoordinator.routes == [.settings])
#expect(tabNavigator.selectedIndex == TabRoute.settings(.settings).index)
}
}

View File

@ -5,20 +5,20 @@ import UIKit
/// An object that builds coordinators for the tab interface.
///
@MainActor
protocol TabModule: AnyObject {
/// Initializes a coordinator for navigating to `TabRoute`s.
///
/// - Parameter:
/// - errorReporter: The error reporter used by the tab module.
/// - itemListDelegate: The delegate of the `ItemListCoordinator`.
/// - rootNavigator: The root navigator used to display this coordinator's interface.
/// - settingsDelegate: The delegate for the settings coordinator.
/// - tabNavigator: The navigator used by the coordinator to navigate between routes.
/// - vaultDelegate: The delegate for the vault coordinator.
/// - vaultRepository: The vault repository used by the tab module.
/// - Returns: A new coordinator that can navigate to any `TabRoute`.
///
func makeTabCoordinator(
errorReporter: ErrorReporter,
itemListDelegate: ItemListCoordinatorDelegate,
rootNavigator: RootNavigator,
tabNavigator: TabNavigator,
) -> AnyCoordinator<TabRoute, Void>
@ -29,11 +29,13 @@ protocol TabModule: AnyObject {
extension DefaultAppModule: TabModule {
func makeTabCoordinator(
errorReporter: ErrorReporter,
itemListDelegate: ItemListCoordinatorDelegate,
rootNavigator: RootNavigator,
tabNavigator: TabNavigator,
) -> AnyCoordinator<TabRoute, Void> {
TabCoordinator(
errorReporter: errorReporter,
itemListDelegate: itemListDelegate,
module: self,
rootNavigator: rootNavigator,
tabNavigator: tabNavigator,

View File

@ -11,8 +11,8 @@ final class TutorialCoordinator: Coordinator, HasStackNavigator {
/// The module types required for creating child coordinators.
typealias Module = DefaultAppModule
typealias Services = HasErrorReporter
& HasErrorAlertServices.ErrorAlertServices
typealias Services = HasErrorAlertServices.ErrorAlertServices
& HasErrorReporter
& HasStateService
// MARK: Private Properties
@ -84,4 +84,3 @@ final class TutorialCoordinator: Coordinator, HasStackNavigator {
extension TutorialCoordinator: HasErrorAlertServices {
var errorAlertServices: ErrorAlertServices { services }
}

View File

@ -21,6 +21,9 @@ enum ItemListAction: Equatable {
///
case itemPressed(_ item: ItemListItem)
/// The user tapped the go to settings button in the flight recorder banner.
case navigateToFlightRecorderSettings
/// The user has started or stopped searching.
case searchStateChanged(isSearching: Bool)

View File

@ -15,6 +15,9 @@ enum ItemListEffect: Equatable {
///
case copyPressed(_ item: ItemListItem)
/// The flight recorder toast banner was dismissed.
case dismissFlightRecorderToastBanner
/// The Move to Bitwarden item button was pressed.
///
/// - Parameter item: The item that should be moved.
@ -27,6 +30,9 @@ enum ItemListEffect: Equatable {
/// Searches based on the keyword.
case search(String)
/// Stream the active flight recorder log.
case streamFlightRecorderLog
/// Stream the vault list for the user.
case streamItemList
}

View File

@ -19,6 +19,7 @@ final class ItemListProcessor: StateProcessor<ItemListState, ItemListAction, Ite
& HasCameraService
& HasConfigService
& HasErrorReporter
& HasFlightRecorder
& HasNotificationCenterService
& HasPasteboardService
& HasTOTPExpirationManagerFactory
@ -99,6 +100,8 @@ final class ItemListProcessor: StateProcessor<ItemListState, ItemListAction, Ite
else { return }
await generateAndCopyTotpCode(totpKey: totpKey)
}
case .dismissFlightRecorderToastBanner:
await dismissFlightRecorderToastBanner()
case let .moveToBitwardenPressed(item):
guard case let .totp(model) = item.itemType else { return }
await moveItemToBitwarden(item: model.itemView)
@ -107,6 +110,8 @@ final class ItemListProcessor: StateProcessor<ItemListState, ItemListAction, Ite
await streamItemList()
case let .search(text):
await searchItems(for: text)
case .streamFlightRecorderLog:
await streamFlightRecorderLog()
case .streamItemList:
await streamItemList()
}
@ -127,6 +132,8 @@ final class ItemListProcessor: StateProcessor<ItemListState, ItemListAction, Ite
services.pasteboardService.copy(totpCode.code)
state.toast = Toast(title: Localizations.valueHasBeenCopied(Localizations.verificationCode))
case .navigateToFlightRecorderSettings:
coordinator.navigate(to: .flightRecorderSettings)
case let .searchStateChanged(isSearching: isSearching):
guard isSearching else {
state.searchText = ""
@ -159,6 +166,12 @@ final class ItemListProcessor: StateProcessor<ItemListState, ItemListAction, Ite
}
}
/// Dismisses the flight recorder toast banner.
///
private func dismissFlightRecorderToastBanner() async {
await services.flightRecorder.setFlightRecorderBannerDismissed()
}
/// Initializes the TOTP expiration managers so the TOTP codes are refreshed automatically.
///
func initTotpExpirationManagers() {
@ -347,6 +360,14 @@ final class ItemListProcessor: StateProcessor<ItemListState, ItemListAction, Ite
}
}
/// Streams the flight recorder enabled status.
///
private func streamFlightRecorderLog() async {
for await log in await services.flightRecorder.activeLogPublisher().values {
state.flightRecorderToastBanner.activeLog = log
}
}
/// Stream the items list.
private func streamItemList() async {
do {

View File

@ -20,6 +20,7 @@ class ItemListProcessorTests: BitwardenTestCase { // swiftlint:disable:this type
var configService: MockConfigService!
var coordinator: MockCoordinator<ItemListRoute, ItemListEvent>!
var errorReporter: MockErrorReporter!
var flightRecorder: MockFlightRecorder!
var notificationCenterService: MockNotificationCenterService!
var pasteboardService: MockPasteboardService!
var totpService: MockTOTPService!
@ -40,6 +41,7 @@ class ItemListProcessorTests: BitwardenTestCase { // swiftlint:disable:this type
configService = MockConfigService()
coordinator = MockCoordinator()
errorReporter = MockErrorReporter()
flightRecorder = MockFlightRecorder()
notificationCenterService = MockNotificationCenterService()
pasteboardService = MockPasteboardService()
totpService = MockTOTPService()
@ -59,6 +61,7 @@ class ItemListProcessorTests: BitwardenTestCase { // swiftlint:disable:this type
cameraService: cameraService,
configService: configService,
errorReporter: errorReporter,
flightRecorder: flightRecorder,
notificationCenterService: notificationCenterService,
pasteboardService: pasteboardService,
totpExpirationManagerFactory: totpExpirationManagerFactory,
@ -81,6 +84,7 @@ class ItemListProcessorTests: BitwardenTestCase { // swiftlint:disable:this type
cameraService = nil
configService = nil
errorReporter = nil
flightRecorder = nil
notificationCenterService = nil
pasteboardService = nil
totpService = nil
@ -300,6 +304,15 @@ class ItemListProcessorTests: BitwardenTestCase { // swiftlint:disable:this type
XCTAssertNil(pasteboardService.copiedString)
}
/// `perform(_:)` with `.dismissFlightRecorderToastBanner` hides the flight recorder toast banner.
@MainActor
func test_perform_dismissFlightRecorderToastBanner() async {
await subject.perform(.dismissFlightRecorderToastBanner)
XCTAssertFalse(subject.state.flightRecorderToastBanner.isToastBannerVisible)
XCTAssertTrue(flightRecorder.setFlightRecorderBannerDismissedCalled)
}
/// `perform(:_)` with `.moveToBitwardenPressed()` with a local item stores the item in the shared
/// store and launches the Bitwarden app via the new item deep link.
@MainActor
@ -451,6 +464,24 @@ class ItemListProcessorTests: BitwardenTestCase { // swiftlint:disable:this type
XCTAssertEqual(subject.state.searchResults, [firstItemRefreshed])
}
/// `perform(_:)` with `.streamFlightRecorderLog` streams the flight recorder log and displays
/// the flight recorder banner if the user hasn't dismissed it previously.
@MainActor
func test_perform_streamFlightRecorderLog() async throws {
let task = Task {
await subject.perform(.streamFlightRecorderLog)
}
defer { task.cancel() }
flightRecorder.activeLogSubject.send(FlightRecorderData.LogMetadata(duration: .eightHours, startDate: .now))
try await waitForAsync { self.subject.state.flightRecorderToastBanner.isToastBannerVisible }
XCTAssertEqual(subject.state.flightRecorderToastBanner.isToastBannerVisible, true)
flightRecorder.activeLogSubject.send(nil)
try await waitForAsync { !self.subject.state.flightRecorderToastBanner.isToastBannerVisible }
XCTAssertEqual(subject.state.flightRecorderToastBanner.isToastBannerVisible, false)
}
/// `perform(_:)` with `.streamItemList` starts streaming vault items. When there are no shared
/// account items, does not show a toast.
@MainActor
@ -639,6 +670,14 @@ class ItemListProcessorTests: BitwardenTestCase { // swiftlint:disable:this type
XCTAssertNil(subject.state.url)
}
/// `receive(_:)` with `.navigateToFlightRecorderSettings` navigates to the flight recorder settings.
@MainActor
func test_receive_navigateToFlightRecorderSettings() {
subject.receive(.navigateToFlightRecorderSettings)
XCTAssertEqual(coordinator.routes.last, .flightRecorderSettings)
}
/// `refreshTOTPCodes(for:)` does nothing if state.loadingState is nil
@MainActor
func test_refreshTOTPCodes_forItemsReturnsEmpty() {

View File

@ -12,6 +12,9 @@ struct ItemListState: Equatable {
loadingState.data.isEmptyOrNil
}
/// The state for the flight recorder toast banner displayed in the item list.
var flightRecorderToastBanner = FlightRecorderToastBannerState()
/// The base url used to fetch icons.
var iconBaseURL: URL?

View File

@ -41,6 +41,16 @@ class ItemListViewTests: BitwardenTestCase {
// MARK: Tests
@MainActor
func disabletest_snapshot_flightRecorderToastBanner() {
processor.state.loadingState = .data([])
processor.state.flightRecorderToastBanner.activeLog = FlightRecorderData.LogMetadata(
duration: .twentyFourHours,
startDate: Date(year: 2025, month: 4, day: 3),
)
assertSnapshot(of: subject, as: .defaultPortrait)
}
/// Test a snapshot of the ItemListView previews.
func disabletest_snapshot_ItemListView_previews() {
for preview in ItemListView_Previews._allPreviews {

View File

@ -78,4 +78,17 @@ class ItemListViewTests: BitwardenTestCase {
XCTAssertEqual(processor.effects.last, .closeCard(.passwordManagerSync))
}
/// Tapping the go to settings button in the flight recorder toast banner dispatches the
/// `.navigateToFlightRecorderSettings` action.
@MainActor
func test_flightRecorderToastBannerGoToSettings_tap() async throws {
processor.state.flightRecorderToastBanner.activeLog = FlightRecorderData.LogMetadata(
duration: .eightHours,
startDate: Date(year: 2025, month: 4, day: 3),
)
let button = try subject.inspect().find(button: Localizations.goToSettings)
try button.tap()
XCTAssertEqual(processor.dispatchedActions, [.navigateToFlightRecorderSettings])
}
}

View File

@ -7,7 +7,7 @@ import SwiftUI
// MARK: - SearchableItemListView
/// A view that displays the items in a single vault group.
private struct SearchableItemListView: View {
private struct SearchableItemListView: View { // swiftlint:disable:this type_body_length
// MARK: Properties
/// A flag indicating if the search bar is focused.
@ -56,11 +56,17 @@ private struct SearchableItemListView: View {
get: \.toast,
send: ItemListAction.toastShown,
))
.flightRecorderToastBanner(
activeLog: store.state.flightRecorderToastBanner.activeLog,
isVisible: store.bindingAsync(
get: \.flightRecorderToastBanner.isToastBannerVisible,
perform: { _ in .dismissFlightRecorderToastBanner },
),
goToSettingsAction: {
store.send(.navigateToFlightRecorderSettings)
},
)
.animation(.default, value: isSearching)
.toast(store.binding(
get: \.toast,
send: ItemListAction.toastShown,
))
.onChange(of: store.state.url) { newValue in
guard let url = newValue else { return }
openURL(url)
@ -381,6 +387,9 @@ struct ItemListView: View {
.task {
await store.perform(.appeared)
}
.task {
await store.perform(.streamFlightRecorderLog)
}
}
}

View File

@ -2,6 +2,20 @@ import BitwardenKit
import OSLog
import SwiftUI
// MARK: - ItemListCoordinatorDelegate
/// A delegate of `ItemListCoordinator` that is notified when navigation events occur that
/// require coordination with other parts of the app.
///
@MainActor
public protocol ItemListCoordinatorDelegate: AnyObject {
/// Called when the user needs to switch to the settings tab and navigate to a `SettingsRoute`.
///
/// - Parameter route: The route to navigate to in the settings tab.
///
func switchToSettingsTab(route: SettingsRoute)
}
// MARK: - ItemListCoordinator
/// A coordinator that manages navigation on the Item List screen.
@ -13,13 +27,16 @@ final class ItemListCoordinator: Coordinator, HasStackNavigator {
& ItemListModule
& NavigatorBuilderModule
typealias Services = HasTimeProvider
& HasErrorAlertServices.ErrorAlertServices
& ItemListProcessor.Services
typealias Services = HasErrorAlertServices.ErrorAlertServices
& HasTimeProvider
& HasTOTPExpirationManagerFactory
& ItemListProcessor.Services
// MARK: - Private Properties
/// The delegate for this coordinator, notified for navigation to other parts of the app.
private weak var delegate: ItemListCoordinatorDelegate?
/// The module used by this coordinator to create child coordinators.
private let module: Module
@ -36,15 +53,18 @@ final class ItemListCoordinator: Coordinator, HasStackNavigator {
/// Creates a new `ItemListCoordinator`.
///
/// - Parameters:
/// - delegate: The delegate for this coordinator, notified for navigation to other parts of the app.
/// - module: The module used by this coordinator to create child coordinators.
/// - services: The services used by this coordinator.
/// - stackNavigator: The stack navigator that is managed by this coordinator.
///
init(
delegate: ItemListCoordinatorDelegate,
module: Module,
services: Services,
stackNavigator: StackNavigator,
) {
self.delegate = delegate
self.module = module
self.services = services
self.stackNavigator = stackNavigator
@ -66,6 +86,8 @@ final class ItemListCoordinator: Coordinator, HasStackNavigator {
break
case let .editItem(item):
showItem(route: .editAuthenticatorItem(item), delegate: context as? AuthenticatorItemOperationDelegate)
case .flightRecorderSettings:
delegate?.switchToSettingsTab(route: .settings)
case .list:
showList()
case .setupTotpManual:
@ -142,4 +164,3 @@ final class ItemListCoordinator: Coordinator, HasStackNavigator {
extension ItemListCoordinator: HasErrorAlertServices {
var errorAlertServices: ErrorAlertServices { services }
}

View File

@ -0,0 +1,75 @@
import BitwardenKit
import BitwardenKitMocks
import Testing
@testable import AuthenticatorShared
@MainActor
struct ItemListCoordinatorTests {
// MARK: Properties
let delegate = MockItemListCoordinatorDelegate()
let module = MockAppModule()
let stackNavigator = MockStackNavigator()
let subject: ItemListCoordinator
let totpExpirationManagerFactory = MockTOTPExpirationManagerFactory()
// MARK: Initialization
init() {
totpExpirationManagerFactory.createResults = [
MockTOTPExpirationManager(),
MockTOTPExpirationManager(),
]
subject = ItemListCoordinator(
delegate: delegate,
module: module,
services: ServiceContainer.withMocks(
totpExpirationManagerFactory: totpExpirationManagerFactory,
),
stackNavigator: stackNavigator,
)
}
// MARK: Tests
/// `navigate(to:)` with `.editItem` presents the edit item view.
@Test
func navigateTo_editItem() throws {
let item = AuthenticatorItemView.fixture()
subject.navigate(to: .editItem(item: item), context: nil)
#expect(module.authenticatorItemCoordinator.isStarted)
#expect(module.authenticatorItemCoordinator.routes == [.editAuthenticatorItem(item)])
}
/// `navigate(to:)` with `.flightRecorderSettings` calls the delegate's `switchToSettingsTab` method.
@Test
func navigateTo_flightRecorderSettings() {
subject.navigate(to: .flightRecorderSettings, context: nil)
#expect(delegate.switchToSettingsTabRoute == .settings)
}
/// `navigate(to:)` with `.list` pushes the item list view onto the stack.
@Test
func navigateTo_list() throws {
subject.navigate(to: .list, context: nil)
let action = try #require(stackNavigator.actions.last)
#expect(action.type == .replaced)
#expect(action.view is ItemListView)
}
}
// MARK: - MockItemListCoordinatorDelegate
class MockItemListCoordinatorDelegate: ItemListCoordinatorDelegate {
var switchToSettingsTabRoute: SettingsRoute?
func switchToSettingsTab(route: SettingsRoute) {
switchToSettingsTabRoute = route
}
}

View File

@ -14,15 +14,18 @@ protocol ItemListModule {
/// - Returns: A coordinator that can navigate to an `ItemListRoute`
///
func makeItemListCoordinator(
delegate: ItemListCoordinatorDelegate,
stackNavigator: StackNavigator,
) -> AnyCoordinator<ItemListRoute, ItemListEvent>
}
extension DefaultAppModule: ItemListModule {
func makeItemListCoordinator(
delegate: ItemListCoordinatorDelegate,
stackNavigator: StackNavigator,
) -> AnyCoordinator<ItemListRoute, ItemListEvent> {
ItemListCoordinator(
delegate: delegate,
module: self,
services: services,
stackNavigator: stackNavigator,

View File

@ -11,6 +11,9 @@ public enum ItemListRoute: Equatable, Hashable {
/// A route to the edit item screen
case editItem(item: AuthenticatorItemView)
/// A route to the flight recorder settings in the settings tab.
case flightRecorderSettings
/// A route to the base item list screen.
case list

View File

@ -8,21 +8,26 @@ import UIKit
class MockAppModule:
AppModule,
AuthModule,
AuthenticatorItemModule,
DebugMenuModule,
FileSelectionModule,
FlightRecorderModule,
ItemListModule,
NavigatorBuilderModule,
TutorialModule,
TabModule {
SettingsModule,
TabModule,
TutorialModule {
var appCoordinator = MockCoordinator<AppRoute, AppEvent>()
var authCoordinator = MockCoordinator<AuthRoute, AuthEvent>()
var authRouter = MockRouter<AuthEvent, AuthRoute>(routeForEvent: { _ in .vaultUnlock })
var authenticatorItemCoordinator = MockCoordinator<AuthenticatorItemRoute, AuthenticatorItemEvent>()
var debugMenuCoordinator = MockCoordinator<DebugMenuRoute, Void>()
var fileSelectionDelegate: FileSelectionDelegate?
var fileSelectionCoordinator = MockCoordinator<FileSelectionRoute, FileSelectionEvent>()
var flightRecorderCoordinator = MockCoordinator<FlightRecorderRoute, Void>()
var itemListCoordinator = MockCoordinator<ItemListRoute, ItemListEvent>()
var itemListCoordinatorDelegate: ItemListCoordinatorDelegate?
var settingsCoordinator = MockCoordinator<SettingsRoute, SettingsEvent>()
var tabCoordinator = MockCoordinator<TabRoute, Void>()
var tutorialCoordinator = MockCoordinator<TutorialRoute, TutorialEvent>()
@ -45,6 +50,12 @@ class MockAppModule:
authRouter.asAnyRouter()
}
func makeAuthenticatorItemCoordinator(
stackNavigator _: StackNavigator,
) -> AnyCoordinator<AuthenticatorItemRoute, AuthenticatorItemEvent> {
authenticatorItemCoordinator.asAnyCoordinator()
}
func makeDebugMenuCoordinator(
stackNavigator: StackNavigator,
) -> AnyCoordinator<DebugMenuRoute, Void> {
@ -66,9 +77,17 @@ class MockAppModule:
}
func makeItemListCoordinator(
delegate: ItemListCoordinatorDelegate,
stackNavigator _: StackNavigator,
) -> AnyCoordinator<ItemListRoute, ItemListEvent> {
itemListCoordinator.asAnyCoordinator()
itemListCoordinatorDelegate = delegate
return itemListCoordinator.asAnyCoordinator()
}
func makeSettingsCoordinator(
stackNavigator _: StackNavigator,
) -> AnyCoordinator<SettingsRoute, SettingsEvent> {
settingsCoordinator.asAnyCoordinator()
}
func makeNavigationController() -> UINavigationController {
@ -77,6 +96,7 @@ class MockAppModule:
func makeTabCoordinator(
errorReporter _: ErrorReporter,
itemListDelegate _: ItemListCoordinatorDelegate,
rootNavigator _: RootNavigator,
tabNavigator _: TabNavigator,
) -> AnyCoordinator<TabRoute, Void> {