Use deferFulfillment in flow coordinator tests (#5861)

* use deferFulfillment in coordinator tests

* pr suggestions
This commit is contained in:
Mauro
2026-07-13 11:52:22 +02:00
committed by GitHub
parent 97990b2877
commit e2c573b1eb
2 changed files with 204 additions and 85 deletions
@@ -173,10 +173,12 @@ struct ChatsTabFlowCoordinatorTests {
chatsTabFlowCoordinator.handleAppRoute(.roomDetails(roomID: "1"), animated: true)
try await unexpectedFulfillment.fulfill()
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
#expect(detailNavigationStack?.stackCoordinators.count == 1)
#expect(detailNavigationStack?.stackCoordinators.first is RoomDetailsScreenCoordinator)
#expect(detailCoordinator != nil)
let detailStack = try #require(detailNavigationStack)
let deferredPush = deferFulfillment(detailStack.observe(\.stackCoordinators.count)) { $0 == 1 }
try await deferredPush.fulfill()
#expect(detailStack.rootCoordinator is RoomScreenCoordinator)
#expect(detailStack.stackCoordinators.first is RoomDetailsScreenCoordinator)
}
@Test
@@ -211,11 +213,12 @@ struct ChatsTabFlowCoordinatorTests {
#expect(detailCoordinator != nil)
chatsTabFlowCoordinator.handleAppRoute(.childRoom(roomID: "2", via: []), animated: true)
try await Task.sleep(for: .milliseconds(100))
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
#expect(detailNavigationStack?.stackCoordinators.count == 1)
#expect(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator)
#expect(detailCoordinator != nil)
let detailStack = try #require(detailNavigationStack)
let deferredPush = deferFulfillment(detailStack.observe(\.stackCoordinators.count)) { $0 == 1 }
try await deferredPush.fulfill()
#expect(detailStack.rootCoordinator is RoomScreenCoordinator)
#expect(detailStack.stackCoordinators.first is RoomScreenCoordinator)
try await process(route: .room(roomID: "3", via: []), expectedState: .roomList(detailState: .room(roomID: "3")))
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
@@ -235,11 +238,13 @@ struct ChatsTabFlowCoordinatorTests {
// A child event route should push a new room screen onto the stack and focus on the event.
chatsTabFlowCoordinator.handleAppRoute(.childEvent(eventID: "2", roomID: "2", via: []), animated: true)
try await Task.sleep(for: .milliseconds(100))
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
#expect(detailNavigationStack?.stackCoordinators.count == 1)
#expect(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator)
#expect(detailCoordinator != nil)
let detailStack = try #require(detailNavigationStack)
let deferredPush = deferFulfillment(detailStack.observe(\.stackCoordinators.count)) { $0 == 1 }
try await deferredPush.fulfill()
#expect(detailStack.rootCoordinator is RoomScreenCoordinator)
#expect(detailStack.stackCoordinators.count == 1)
#expect(detailStack.stackCoordinators.first is RoomScreenCoordinator)
#expect(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount == 2)
#expect(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID == "2")
@@ -270,6 +275,9 @@ struct ChatsTabFlowCoordinatorTests {
try await process(route: .share(sharePayload),
expectedState: .roomList(detailState: .room(roomID: "2")))
let deferredSheet = deferFulfillment(splitCoordinator.observe(\.sheetCoordinatorID)) { $0 != nil }
try await deferredSheet.fulfill()
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
#expect((splitCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
}
@@ -289,12 +297,57 @@ struct ChatsTabFlowCoordinatorTests {
// MARK: - Private
private mutating func process(route: AppRoute, expectedState: ChatsTabFlowCoordinatorStateMachine.State) async throws {
// Sometimes the state machine's state changes before the coordinators have updated the stack.
let delayedPublisher = stateMachineFactory.chatsTabFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)
private func process(route: AppRoute, expectedState: ChatsTabFlowCoordinatorStateMachine.State) async throws {
// Keep the previous root coordinator alive while waiting, otherwise a newly presented
// coordinator could be allocated at the same address and be mistaken for it below.
let previousDetailRootCoordinator = splitCoordinator.detailRootCoordinator
let previousDetailRootCoordinatorID = previousDetailRootCoordinator.map { ObjectIdentifier($0) }
let deferred = deferFulfillment(delayedPublisher) { $0 == expectedState }
let deferred = deferFulfillment(stateMachineFactory.chatsTabFlowStatePublisher) { $0 == expectedState }
chatsTabFlowCoordinator.handleAppRoute(route, animated: true)
try await deferred.fulfill()
// The state machine's state changes before the coordinators have updated the stack,
// so also wait for the navigation side effect implied by the expected state.
switch expectedState {
case .roomList(detailState: .some(.room)):
let deferredDetail = deferFulfillment(splitCoordinator.observe(\.detailRootCoordinatorID)) { $0 != nil && $0 != previousDetailRootCoordinatorID }
try await deferredDetail.fulfill()
case .roomList(detailState: nil):
let deferredDetail = deferFulfillment(splitCoordinator.observe(\.detailCoordinatorID)) { $0 == nil }
try await deferredDetail.fulfill()
case .userProfileScreen:
let deferredDetail = deferFulfillment(splitCoordinator.observe(\.detailRootCoordinatorID)) { $0 == nil }
try await deferredDetail.fulfill()
let deferredSheet = deferFulfillment(splitCoordinator.observe(\.sheetCoordinatorID)) { $0 != nil }
try await deferredSheet.fulfill()
default:
break
}
withExtendedLifetime(previousDetailRootCoordinator) { }
}
}
// Sendable projections of the split coordinator's non-Sendable coordinator references,
// allowing the tests to `observe` them and wait for navigation with `deferFulfillment`.
private extension NavigationSplitCoordinator {
var detailCoordinatorID: ObjectIdentifier? {
detailCoordinator.map { ObjectIdentifier($0) }
}
/// The root coordinator inside the detail navigation stack.
var detailRootCoordinator: (any CoordinatorProtocol)? {
(detailCoordinator as? NavigationStackCoordinator)?.rootCoordinator
}
/// The identity of the root coordinator inside the detail navigation stack.
var detailRootCoordinatorID: ObjectIdentifier? {
detailRootCoordinator.map { ObjectIdentifier($0) }
}
var sheetCoordinatorID: ObjectIdentifier? {
sheetCoordinator.map { ObjectIdentifier($0) }
}
}