mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Fix for flaky tests (#5869)
* possible logging tests fix * fix for flaky tests * use timelimit for max test time * pr suggestions * remove comment
This commit is contained in:
@@ -487,6 +487,7 @@
|
|||||||
4EAC427267424192964B16B3 /* AppSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13BE9781699FB510E9263192 /* AppSettingsHook.swift */; };
|
4EAC427267424192964B16B3 /* AppSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13BE9781699FB510E9263192 /* AppSettingsHook.swift */; };
|
||||||
4ED764A24F2A715C25CF07F1 /* TimelineMediaPreviewFileExportPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30856520F3263D0E195710D7 /* TimelineMediaPreviewFileExportPicker.swift */; };
|
4ED764A24F2A715C25CF07F1 /* TimelineMediaPreviewFileExportPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30856520F3263D0E195710D7 /* TimelineMediaPreviewFileExportPicker.swift */; };
|
||||||
4F2DF6138E87A4B8C2488CA3 /* VoiceMessageCacheProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */; };
|
4F2DF6138E87A4B8C2488CA3 /* VoiceMessageCacheProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */; };
|
||||||
|
4F51C7C8131D060108B80575 /* NavigationCoordinators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79351405CD99312A182B2488 /* NavigationCoordinators.swift */; };
|
||||||
4FDC8A9764CFDA90CE035725 /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB2253D36E81E045E1CB432 /* Duration.swift */; };
|
4FDC8A9764CFDA90CE035725 /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB2253D36E81E045E1CB432 /* Duration.swift */; };
|
||||||
4FE688FE9375B2FBF424146A /* TextBasedRoomTimelineViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */; };
|
4FE688FE9375B2FBF424146A /* TextBasedRoomTimelineViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */; };
|
||||||
4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; };
|
4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; };
|
||||||
@@ -2338,6 +2339,7 @@
|
|||||||
78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = "<group>"; };
|
78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||||
78BBDF7A05CF53B5CDC13682 /* landscape_test_video.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = landscape_test_video.mov; sourceTree = "<group>"; };
|
78BBDF7A05CF53B5CDC13682 /* landscape_test_video.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = landscape_test_video.mov; sourceTree = "<group>"; };
|
||||||
|
79351405CD99312A182B2488 /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = "<group>"; };
|
||||||
796CBD0C56FA0D3AEDAB255B /* SessionVerificationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenCoordinator.swift; sourceTree = "<group>"; };
|
796CBD0C56FA0D3AEDAB255B /* SessionVerificationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
79A106D76741914F82664959 /* StateStoreViewModelV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModelV2.swift; sourceTree = "<group>"; };
|
79A106D76741914F82664959 /* StateStoreViewModelV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModelV2.swift; sourceTree = "<group>"; };
|
||||||
7A03E073077D92AA19C43DCF /* IdentityConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenCoordinator.swift; sourceTree = "<group>"; };
|
7A03E073077D92AA19C43DCF /* IdentityConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
@@ -3284,6 +3286,14 @@
|
|||||||
path = RoomNotificationSettingsScreen;
|
path = RoomNotificationSettingsScreen;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
0345B849A6A802EB2F72C44A /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
79351405CD99312A182B2488 /* NavigationCoordinators.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
0371482D36C95ABAF9D4C651 /* Recorder */ = {
|
0371482D36C95ABAF9D4C651 /* Recorder */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -4793,6 +4803,7 @@
|
|||||||
children = (
|
children = (
|
||||||
B91AD590B0B40718A0AA0C61 /* DeferredFulfillment.swift */,
|
B91AD590B0B40718A0AA0C61 /* DeferredFulfillment.swift */,
|
||||||
9E21DC54FCC2A7F78532923F /* WaitingConfirmation.swift */,
|
9E21DC54FCC2A7F78532923F /* WaitingConfirmation.swift */,
|
||||||
|
0345B849A6A802EB2F72C44A /* Extensions */,
|
||||||
);
|
);
|
||||||
path = TestUtilities;
|
path = TestUtilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -8043,6 +8054,7 @@
|
|||||||
1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */,
|
1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */,
|
||||||
DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */,
|
DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */,
|
||||||
3F791C4E0F7B373DB217EC90 /* NSItemProviderTests.swift in Sources */,
|
3F791C4E0F7B373DB217EC90 /* NSItemProviderTests.swift in Sources */,
|
||||||
|
4F51C7C8131D060108B80575 /* NavigationCoordinators.swift in Sources */,
|
||||||
981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */,
|
981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */,
|
||||||
69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */,
|
69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */,
|
||||||
4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */,
|
4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */,
|
||||||
|
|||||||
@@ -329,25 +329,3 @@ struct ChatsTabFlowCoordinatorTests {
|
|||||||
withExtendedLifetime(previousDetailRootCoordinator) { }
|
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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -741,11 +741,7 @@ final class ComposerToolbarViewModelTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// There are 2 violations, ensure that resolving the first one is not enough
|
let mockSubject = CurrentValueSubject<[IdentityStatusChange], Never>([])
|
||||||
let mockSubject = CurrentValueSubject<[IdentityStatusChange], Never>([
|
|
||||||
IdentityStatusChange(userId: "@alice:localhost", changedTo: .verificationViolation),
|
|
||||||
IdentityStatusChange(userId: "@bob:localhost", changedTo: .verificationViolation)
|
|
||||||
])
|
|
||||||
|
|
||||||
roomProxyMock.identityStatusChangesPublisher = mockSubject.asCurrentValuePublisher()
|
roomProxyMock.identityStatusChangesPublisher = mockSubject.asCurrentValuePublisher()
|
||||||
|
|
||||||
@@ -761,12 +757,16 @@ final class ComposerToolbarViewModelTests {
|
|||||||
composerDraftService: draftServiceMock)
|
composerDraftService: draftServiceMock)
|
||||||
|
|
||||||
var fulfillment = deferFulfillment(viewModel.context.$viewState, message: "Composer is disabled") { $0.canSend == false }
|
var fulfillment = deferFulfillment(viewModel.context.$viewState, message: "Composer is disabled") { $0.canSend == false }
|
||||||
mockSubject.send([IdentityStatusChange(userId: "@alice:localhost", changedTo: .verificationViolation)])
|
mockSubject.send([
|
||||||
|
IdentityStatusChange(userId: "@alice:localhost", changedTo: .verificationViolation),
|
||||||
|
IdentityStatusChange(userId: "@bob:localhost", changedTo: .verificationViolation)
|
||||||
|
])
|
||||||
try await fulfillment.fulfill()
|
try await fulfillment.fulfill()
|
||||||
|
|
||||||
fulfillment = deferFulfillment(viewModel.context.$viewState, message: "Composer is still disabled") { $0.canSend == false }
|
// There are 2 violations, ensure that resolving the first one is not enough.
|
||||||
|
let failure = deferFailure(viewModel.context.$viewState, timeout: .seconds(1), message: "Composer should still be disabled") { $0.canSend == true }
|
||||||
mockSubject.send([IdentityStatusChange(userId: "@alice:localhost", changedTo: .pinned)])
|
mockSubject.send([IdentityStatusChange(userId: "@alice:localhost", changedTo: .pinned)])
|
||||||
try await fulfillment.fulfill()
|
try await failure.fulfill()
|
||||||
|
|
||||||
fulfillment = deferFulfillment(viewModel.context.$viewState, message: "Composer is now enabled") { $0.canSend == true }
|
fulfillment = deferFulfillment(viewModel.context.$viewState, message: "Composer is now enabled") { $0.canSend == true }
|
||||||
mockSubject.send([IdentityStatusChange(userId: "@bob:localhost", changedTo: .pinned)])
|
mockSubject.send([IdentityStatusChange(userId: "@bob:localhost", changedTo: .pinned)])
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import Clocks
|
|||||||
import PushKit
|
import PushKit
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
|
// The TestClock advances by yielding to background tasks which can be starved on a busy CI runner.
|
||||||
|
@Suite(.timeLimit(.minutes(2)))
|
||||||
@MainActor
|
@MainActor
|
||||||
final class ElementCallServiceTests {
|
final class ElementCallServiceTests {
|
||||||
private var callProvider: CXProviderMock!
|
private var callProvider: CXProviderMock!
|
||||||
@@ -111,27 +113,28 @@ final class ElementCallServiceTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func lifetimeIsCapped() async {
|
func lifetimeIsCapped() async throws {
|
||||||
await confirmation { confirmation in
|
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
||||||
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
|
|
||||||
if reason == .unanswered {
|
|
||||||
confirmation()
|
|
||||||
} else {
|
|
||||||
Issue.record("Call should have ended as unanswered")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#expect(!callProvider.reportNewIncomingCallWithUpdateCompletionCalled)
|
let (endedCalls, endedCallsContinuation) = AsyncStream<CXCallEndedReason>.makeStream()
|
||||||
|
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
|
||||||
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 300)
|
endedCallsContinuation.yield(reason)
|
||||||
|
|
||||||
service.pushRegistry(pushRegistry,
|
|
||||||
didReceiveIncomingPushWith: pushPayload,
|
|
||||||
for: .voIP) { }
|
|
||||||
|
|
||||||
// Advance past the max timeout but below the 300
|
|
||||||
await testClock.advance(by: .seconds(100))
|
|
||||||
}
|
}
|
||||||
|
let deferredEndedCall = deferFulfillment(endedCalls, timeout: .seconds(30)) { _ in true }
|
||||||
|
|
||||||
|
let pushPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 300)
|
||||||
|
|
||||||
|
service.pushRegistry(pushRegistry,
|
||||||
|
didReceiveIncomingPushWith: pushPayload,
|
||||||
|
for: .voIP) { }
|
||||||
|
|
||||||
|
await waitForScheduledSleep()
|
||||||
|
|
||||||
|
// Advance past the max timeout but below the 300
|
||||||
|
await testClock.advance(by: .seconds(100))
|
||||||
|
|
||||||
|
let reason = try await deferredEndedCall.fulfill()
|
||||||
|
#expect(reason == .unanswered, "Call should have ended as unanswered")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -142,20 +145,20 @@ final class ElementCallServiceTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func timeoutClearsIncomingCallStateBeforeNextPush() async {
|
func timeoutClearsIncomingCallStateBeforeNextPush() async throws {
|
||||||
// Drive push #1 to its 60s unanswered timeout
|
// Drive push #1 to its 60s unanswered timeout
|
||||||
await confirmation { confirmation in
|
let (endedCalls, endedCallsContinuation) = AsyncStream<CXCallEndedReason>.makeStream()
|
||||||
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
|
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
|
||||||
if reason == .unanswered {
|
endedCallsContinuation.yield(reason)
|
||||||
confirmation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let firstPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 60)
|
|
||||||
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: firstPayload, for: .voIP) { }
|
|
||||||
|
|
||||||
await testClock.advance(by: .seconds(70))
|
|
||||||
}
|
}
|
||||||
|
let deferredEndedCall = deferFulfillment(endedCalls, timeout: .seconds(30)) { $0 == .unanswered }
|
||||||
|
|
||||||
|
let firstPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 60)
|
||||||
|
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: firstPayload, for: .voIP) { }
|
||||||
|
|
||||||
|
await waitForScheduledSleep()
|
||||||
|
await testClock.advance(by: .seconds(70))
|
||||||
|
try await deferredEndedCall.fulfill()
|
||||||
|
|
||||||
callProvider.reportCallWithEndedAtReasonClosure = nil
|
callProvider.reportCallWithEndedAtReasonClosure = nil
|
||||||
let firstCallUUID = callProvider.reportNewIncomingCallWithUpdateCompletionReceivedArguments?.uuid
|
let firstCallUUID = callProvider.reportNewIncomingCallWithUpdateCompletionReceivedArguments?.uuid
|
||||||
@@ -189,6 +192,9 @@ final class ElementCallServiceTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure the cancellation below exercises a scheduled timer rather than one that never started.
|
||||||
|
await waitForScheduledSleep()
|
||||||
|
|
||||||
// Simulate the answer flow handing off to setupCallSession, which must cancel
|
// Simulate the answer flow handing off to setupCallSession, which must cancel
|
||||||
// the pending endUnansweredCallTask as part of clearing the incoming state.
|
// the pending endUnansweredCallTask as part of clearing the incoming state.
|
||||||
await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome")
|
await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome")
|
||||||
@@ -235,6 +241,15 @@ final class ElementCallServiceTests {
|
|||||||
#expect(service.ongoingCallRoomIDPublisher.value == "!room:example.com")
|
#expect(service.ongoingCallRoomIDPublisher.value == "!room:example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Waits until the unanswered-call timer is sleeping on the test clock — it runs in an
|
||||||
|
/// unstructured task, so advancing the clock before that would never wake it.
|
||||||
|
/// Note: `checkSuspension()` throws when a sleep is scheduled, which is the state we want.
|
||||||
|
private func waitForScheduledSleep() async {
|
||||||
|
while await (try? testClock.checkSuspension()) != nil {
|
||||||
|
await Task.yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func expectImmediatelyEndedCallReported(forPayload payload: PKPushPayloadMock,
|
private func expectImmediatelyEndedCallReported(forPayload payload: PKPushPayloadMock,
|
||||||
expectedReason: CXCallEndedReason) async {
|
expectedReason: CXCallEndedReason) async {
|
||||||
let baselineNewIncomingCount = callProvider.reportNewIncomingCallWithUpdateCompletionCallsCount
|
let baselineNewIncomingCount = callProvider.reportNewIncomingCallWithUpdateCompletionCallsCount
|
||||||
|
|||||||
@@ -331,6 +331,11 @@ final class LoggingTests {
|
|||||||
/// to start with a fresh state (as calling ``Tracing.deleteLogFiles`` would trigger the bug).
|
/// to start with a fresh state (as calling ``Tracing.deleteLogFiles`` would trigger the bug).
|
||||||
private func setupTest(name: String = #function, redirectTracingFileWriter: Bool = true) throws {
|
private func setupTest(name: String = #function, redirectTracingFileWriter: Bool = true) throws {
|
||||||
let testDirectory = URL.appGroupLogsDirectory.appending(component: name, directoryHint: .isDirectory)
|
let testDirectory = URL.appGroupLogsDirectory.appending(component: name, directoryHint: .isDirectory)
|
||||||
|
|
||||||
|
// CI retries re-run the suite on the same simulator, so clear out any log files left
|
||||||
|
// behind by a previous attempt. No writer points at the directory yet, so this is safe.
|
||||||
|
try? FileManager.default.removeItem(at: testDirectory)
|
||||||
|
|
||||||
Tracing.logsDirectoryOverride = testDirectory
|
Tracing.logsDirectoryOverride = testDirectory
|
||||||
try? FileManager.default.createDirectory(at: testDirectory, withIntermediateDirectories: true)
|
try? FileManager.default.createDirectory(at: testDirectory, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
|||||||
@@ -56,19 +56,21 @@ final class MediaUploadPreviewScreenViewModelTests {
|
|||||||
try await send()
|
try await send()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test(.timeLimit(.minutes(2))) // Transcoding videos can be slow on a busy CI runner.
|
||||||
func videoUploadWithoutCaption() async throws {
|
func videoUploadWithoutCaption() async throws {
|
||||||
setUpViewModel(urls: [videoURL], expectedCaption: nil)
|
setUpViewModel(urls: [videoURL], expectedCaption: nil)
|
||||||
context.caption = .init("")
|
context.caption = .init("")
|
||||||
try await send()
|
// The preprocessor transcodes the video for real which can take a while on a busy CI runner.
|
||||||
|
try await send(timeout: .seconds(60))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test(.timeLimit(.minutes(2))) // Transcoding videos can be slow on a busy CI runner.
|
||||||
func videoUploadWithCaption() async throws {
|
func videoUploadWithCaption() async throws {
|
||||||
let caption = "Check out this video!"
|
let caption = "Check out this video!"
|
||||||
setUpViewModel(urls: [videoURL], expectedCaption: caption)
|
setUpViewModel(urls: [videoURL], expectedCaption: caption)
|
||||||
context.caption = .init(string: caption)
|
context.caption = .init(string: caption)
|
||||||
try await send()
|
// The preprocessor transcodes the video for real which can take a while on a busy CI runner.
|
||||||
|
try await send(timeout: .seconds(60))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -306,10 +308,10 @@ final class MediaUploadPreviewScreenViewModelTests {
|
|||||||
return .success(())
|
return .success(())
|
||||||
}
|
}
|
||||||
|
|
||||||
private func send() async throws {
|
private func send(timeout: Duration = .seconds(10)) async throws {
|
||||||
#expect(!context.viewState.shouldDisableInteraction, "Attempting to send when interaction is disabled.")
|
#expect(!context.viewState.shouldDisableInteraction, "Attempting to send when interaction is disabled.")
|
||||||
|
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
let deferred = deferFulfillment(viewModel.actions, timeout: timeout) { $0 == .dismiss }
|
||||||
context.send(viewAction: .send)
|
context.send(viewAction: .send)
|
||||||
|
|
||||||
#expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
#expect(context.viewState.shouldDisableInteraction, "The interaction should be disabled while sending.")
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ final class MediaUploadingPreprocessorTests {
|
|||||||
#expect(isEqual(audioInfo.size ?? 0, 194_811, within: 100))
|
#expect(isEqual(audioInfo.size ?? 0, 194_811, within: 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test(.timeLimit(.minutes(2))) // Transcoding videos can be slow on a busy CI runner.
|
||||||
func landscapeMovVideoProcessing() async throws {
|
func landscapeMovVideoProcessing() async throws {
|
||||||
let url = try #require(Bundle(for: Self.self).url(forResource: "landscape_test_video.mov", withExtension: nil), "Failed retrieving test asset")
|
let url = try #require(Bundle(for: Self.self).url(forResource: "landscape_test_video.mov", withExtension: nil), "Failed retrieving test asset")
|
||||||
|
|
||||||
@@ -74,8 +74,12 @@ final class MediaUploadingPreprocessorTests {
|
|||||||
#expect(isEqual(videoInfo.thumbnailInfo?.size ?? 0, 183_093, within: 100))
|
#expect(isEqual(videoInfo.thumbnailInfo?.size ?? 0, 183_093, within: 100))
|
||||||
#expect(videoInfo.thumbnailInfo?.width == 800)
|
#expect(videoInfo.thumbnailInfo?.width == 800)
|
||||||
#expect(videoInfo.thumbnailInfo?.height == 450)
|
#expect(videoInfo.thumbnailInfo?.height == 450)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(.timeLimit(.minutes(2))) // Transcoding videos can be slow on a busy CI runner.
|
||||||
|
func landscapeMovVideoOptimizedProcessing() async throws {
|
||||||
|
let url = try #require(Bundle(for: Self.self).url(forResource: "landscape_test_video.mov", withExtension: nil), "Failed retrieving test asset")
|
||||||
|
|
||||||
// Repeat with optimised media setting
|
|
||||||
appSettings.optimizeMediaUploads = true
|
appSettings.optimizeMediaUploads = true
|
||||||
|
|
||||||
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||||
@@ -95,7 +99,7 @@ final class MediaUploadingPreprocessorTests {
|
|||||||
#expect(isEqual(optimizedVideoInfo.duration ?? 0, 30, within: 100))
|
#expect(isEqual(optimizedVideoInfo.duration ?? 0, 30, within: 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test(.timeLimit(.minutes(2))) // Transcoding videos can be slow on a busy CI runner.
|
||||||
func portraitMp4VideoProcessing() async throws {
|
func portraitMp4VideoProcessing() async throws {
|
||||||
let url = try #require(Bundle(for: Self.self).url(forResource: "portrait_test_video.mp4", withExtension: nil), "Failed retrieving test asset")
|
let url = try #require(Bundle(for: Self.self).url(forResource: "portrait_test_video.mp4", withExtension: nil), "Failed retrieving test asset")
|
||||||
|
|
||||||
@@ -129,8 +133,12 @@ final class MediaUploadingPreprocessorTests {
|
|||||||
#expect(isEqual(videoInfo.thumbnailInfo?.size ?? 0, 40976, within: 100))
|
#expect(isEqual(videoInfo.thumbnailInfo?.size ?? 0, 40976, within: 100))
|
||||||
#expect(videoInfo.thumbnailInfo?.width == 337)
|
#expect(videoInfo.thumbnailInfo?.width == 337)
|
||||||
#expect(videoInfo.thumbnailInfo?.height == 600)
|
#expect(videoInfo.thumbnailInfo?.height == 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(.timeLimit(.minutes(2))) // Transcoding videos can be slow on a busy CI runner.
|
||||||
|
func portraitMp4VideoOptimizedProcessing() async throws {
|
||||||
|
let url = try #require(Bundle(for: Self.self).url(forResource: "portrait_test_video.mp4", withExtension: nil), "Failed retrieving test asset")
|
||||||
|
|
||||||
// Repeat with optimised media setting
|
|
||||||
appSettings.optimizeMediaUploads = true
|
appSettings.optimizeMediaUploads = true
|
||||||
|
|
||||||
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
guard case let .success(optimizedResult) = await mediaUploadingPreprocessor.processMedia(at: url, maxUploadSize: maxUploadSize),
|
||||||
|
|||||||
@@ -175,8 +175,9 @@ struct RoomDetailsScreenViewModelTests {
|
|||||||
|
|
||||||
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
||||||
|
|
||||||
|
#expect(!context.viewState.isProcessingIgnoreRequest)
|
||||||
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
||||||
transitionValues: [false, true, false])
|
transitionValues: [true, false])
|
||||||
|
|
||||||
context.send(viewAction: .ignoreConfirmed)
|
context.send(viewAction: .ignoreConfirmed)
|
||||||
|
|
||||||
@@ -206,8 +207,9 @@ struct RoomDetailsScreenViewModelTests {
|
|||||||
|
|
||||||
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
||||||
|
|
||||||
|
#expect(!context.viewState.isProcessingIgnoreRequest)
|
||||||
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
||||||
transitionValues: [false, true, false])
|
transitionValues: [true, false])
|
||||||
|
|
||||||
context.send(viewAction: .ignoreConfirmed)
|
context.send(viewAction: .ignoreConfirmed)
|
||||||
|
|
||||||
@@ -236,8 +238,9 @@ struct RoomDetailsScreenViewModelTests {
|
|||||||
|
|
||||||
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
||||||
|
|
||||||
|
#expect(!context.viewState.isProcessingIgnoreRequest)
|
||||||
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
||||||
transitionValues: [false, true, false])
|
transitionValues: [true, false])
|
||||||
|
|
||||||
context.send(viewAction: .unignoreConfirmed)
|
context.send(viewAction: .unignoreConfirmed)
|
||||||
|
|
||||||
@@ -267,8 +270,9 @@ struct RoomDetailsScreenViewModelTests {
|
|||||||
|
|
||||||
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
#expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient))
|
||||||
|
|
||||||
|
#expect(!context.viewState.isProcessingIgnoreRequest)
|
||||||
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest),
|
||||||
transitionValues: [false, true, false])
|
transitionValues: [true, false])
|
||||||
|
|
||||||
context.send(viewAction: .unignoreConfirmed)
|
context.send(viewAction: .unignoreConfirmed)
|
||||||
|
|
||||||
|
|||||||
@@ -550,20 +550,3 @@ final class RoomFlowCoordinatorTests {
|
|||||||
private enum RoomType {
|
private enum RoomType {
|
||||||
case invited(roomID: String)
|
case invited(roomID: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sendable projections of the stack coordinator's non-Sendable coordinator references,
|
|
||||||
// allowing the tests to `observe` them and wait for navigation with `deferFulfillment`.
|
|
||||||
private extension NavigationStackCoordinator {
|
|
||||||
var rootCoordinatorID: ObjectIdentifier? {
|
|
||||||
rootCoordinator.map { ObjectIdentifier($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The identity of the topmost coordinator on the stack.
|
|
||||||
var topCoordinatorID: ObjectIdentifier? {
|
|
||||||
stackCoordinators.last.map { ObjectIdentifier($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var sheetCoordinatorID: ObjectIdentifier? {
|
|
||||||
sheetCoordinator.map { ObjectIdentifier($0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -298,8 +298,9 @@ struct RoomNotificationSettingsScreenViewModelTests {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
#expect(!viewModel.context.viewState.deletingCustomSetting)
|
||||||
let deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.deletingCustomSetting),
|
let deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.deletingCustomSetting),
|
||||||
transitionValues: [false, true, false])
|
transitionValues: [true, false])
|
||||||
|
|
||||||
viewModel.context.send(viewAction: .deleteCustomSettingTapped)
|
viewModel.context.send(viewAction: .deleteCustomSettingTapped)
|
||||||
|
|
||||||
|
|||||||
@@ -48,22 +48,15 @@ struct SpaceScreenViewModelTests {
|
|||||||
#expect(!response.isEmpty, "There should be some test rooms.")
|
#expect(!response.isEmpty, "There should be some test rooms.")
|
||||||
|
|
||||||
// When the pagination is triggered.
|
// When the pagination is triggered.
|
||||||
var deferred = deferFulfillment(spaceRoomListProxy.paginationStatePublisher) { $0 == .loading }
|
let deferredState = deferFulfillment(context.observe(\.viewState.paginationState), transitionValues: [.paginating, .endReached])
|
||||||
paginationStateSubject.send(.idle(endReached: false)) // Invert the default to allow paginate to be called.
|
paginationStateSubject.send(.idle(endReached: false)) // Invert the default to allow paginate to be called.
|
||||||
try await deferred.fulfill()
|
try await deferredState.fulfill()
|
||||||
|
|
||||||
// Then the screen should show a paginating indicator.
|
// Then a single pagination should be made and the space rooms should be populated.
|
||||||
#expect(context.viewState.paginationState == .paginating)
|
// The rooms arrive on a separate publisher to the pagination state, so wait for them too.
|
||||||
|
let deferredRooms = deferFulfillment(context.observe(\.viewState.rooms)) { [response] in $0.map(\.id) == response.map(\.id) }
|
||||||
|
try await deferredRooms.fulfill()
|
||||||
#expect(spaceRoomListProxy.paginateCallsCount == 1)
|
#expect(spaceRoomListProxy.paginateCallsCount == 1)
|
||||||
|
|
||||||
// When waiting for the pagination to finish.
|
|
||||||
deferred = deferFulfillment(spaceRoomListProxy.paginationStatePublisher) { $0 == .idle(endReached: true) }
|
|
||||||
try await deferred.fulfill()
|
|
||||||
|
|
||||||
// Then no more pagination requests should be made the the space rooms should be populated.
|
|
||||||
#expect(context.viewState.paginationState == .endReached)
|
|
||||||
#expect(spaceRoomListProxy.paginateCallsCount == 1)
|
|
||||||
#expect(context.viewState.rooms.map(\.id) == response.map(\.id))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -153,16 +146,18 @@ struct SpaceScreenViewModelTests {
|
|||||||
|
|
||||||
let selectedSpace = try #require(mockSpaceRooms.first { $0.isSpace && $0.state != .joined }, "There should be a space to select.")
|
let selectedSpace = try #require(mockSpaceRooms.first { $0.isSpace && $0.state != .joined }, "There should be a space to select.")
|
||||||
|
|
||||||
let deferredState = deferFulfillment(viewModel.context.observe(\.viewState.joiningRoomIDs), transitionValues: [[selectedSpace.id], []])
|
// Assert the transient joining state inside the closure: it can be cleared again before an observer is serviced.
|
||||||
|
await waitForConfirmation("Join room") { confirmation in
|
||||||
try await confirmation("Join room") { confirm in
|
clientProxy.joinRoomViaClosure = { [context] _, _ in
|
||||||
clientProxy.joinRoomViaClosure = { _, _ in
|
await MainActor.run { #expect(context.viewState.joiningRoomIDs == [selectedSpace.id]) }
|
||||||
confirm()
|
confirmation()
|
||||||
return .success(())
|
return .success(())
|
||||||
}
|
}
|
||||||
viewModel.context.send(viewAction: .spaceAction(.join(selectedSpace)))
|
viewModel.context.send(viewAction: .spaceAction(.join(selectedSpace)))
|
||||||
try await deferredState.fulfill()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deferredJoined = deferFulfillment(viewModel.context.observe(\.viewState.joiningRoomIDs)) { $0.isEmpty }
|
||||||
|
try await deferredJoined.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -171,16 +166,18 @@ struct SpaceScreenViewModelTests {
|
|||||||
|
|
||||||
let selectedRoom = try #require(mockSpaceRooms.first { !$0.isSpace }, "There should be a room to select.")
|
let selectedRoom = try #require(mockSpaceRooms.first { !$0.isSpace }, "There should be a room to select.")
|
||||||
|
|
||||||
let deferredState = deferFulfillment(viewModel.context.observe(\.viewState.joiningRoomIDs), transitionValues: [[selectedRoom.id], []])
|
// Assert the transient joining state inside the closure: it can be cleared again before an observer is serviced.
|
||||||
|
await waitForConfirmation("Join room") { confirmation in
|
||||||
try await confirmation("Join room") { confirm in
|
clientProxy.joinRoomViaClosure = { [context] _, _ in
|
||||||
clientProxy.joinRoomViaClosure = { _, _ in
|
await MainActor.run { #expect(context.viewState.joiningRoomIDs == [selectedRoom.id]) }
|
||||||
confirm()
|
confirmation()
|
||||||
return .success(())
|
return .success(())
|
||||||
}
|
}
|
||||||
viewModel.context.send(viewAction: .spaceAction(.join(selectedRoom)))
|
viewModel.context.send(viewAction: .spaceAction(.join(selectedRoom)))
|
||||||
try await deferredState.fulfill()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deferredJoined = deferFulfillment(viewModel.context.observe(\.viewState.joiningRoomIDs)) { $0.isEmpty }
|
||||||
|
try await deferredJoined.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
@testable import ElementX
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Sendable projections of the navigation coordinators' non-Sendable coordinator references,
|
||||||
|
// allowing the tests to `observe` them and wait for navigation with `deferFulfillment`.
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NavigationStackCoordinator {
|
||||||
|
var rootCoordinatorID: ObjectIdentifier? {
|
||||||
|
rootCoordinator.map { ObjectIdentifier($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The identity of the topmost coordinator on the stack.
|
||||||
|
var topCoordinatorID: ObjectIdentifier? {
|
||||||
|
stackCoordinators.last.map { ObjectIdentifier($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var sheetCoordinatorID: ObjectIdentifier? {
|
||||||
|
sheetCoordinator.map { ObjectIdentifier($0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NavigationTabCoordinator {
|
||||||
|
var sheetCoordinatorID: ObjectIdentifier? {
|
||||||
|
sheetCoordinator.map { ObjectIdentifier($0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -181,11 +181,15 @@ struct TimelineMediaPreviewViewModelTests {
|
|||||||
|
|
||||||
// When confirming the redaction.
|
// When confirming the redaction.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
let deferred = deferFulfillment(viewModel.actions) { $0 == .dismiss }
|
||||||
context.send(viewAction: .redactConfirmation(item: mediaItem))
|
|
||||||
|
// The redaction runs in an unstructured task, so wait for the call rather than asserting after the dismiss.
|
||||||
|
await waitForConfirmation { confirmation in
|
||||||
|
timelineController.redactClosure = { _ in confirmation() }
|
||||||
|
context.send(viewAction: .redactConfirmation(item: mediaItem))
|
||||||
|
}
|
||||||
|
|
||||||
// Then the item should be redacted and the view should be dismissed.
|
// Then the item should be redacted and the view should be dismissed.
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
#expect(timelineController.redactCalled)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ struct UserSessionFlowCoordinatorTests {
|
|||||||
#expect((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
#expect((tabCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator)
|
||||||
#expect(detailCoordinator == nil)
|
#expect(detailCoordinator == nil)
|
||||||
|
|
||||||
try await process(route: .room(roomID: "1", via: []), expectedChatsState: .roomList(detailState: .room(roomID: "1")))
|
try await process(route: .room(roomID: "1", via: []),
|
||||||
|
expectedUserSessionState: .tabBar,
|
||||||
|
expectedChatsState: .roomList(detailState: .room(roomID: "1")))
|
||||||
#expect(tabCoordinator?.sheetCoordinator == nil)
|
#expect(tabCoordinator?.sheetCoordinator == nil)
|
||||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||||
#expect(detailCoordinator != nil)
|
#expect(detailCoordinator != nil)
|
||||||
@@ -148,9 +150,13 @@ struct UserSessionFlowCoordinatorTests {
|
|||||||
try await process(route: .share(sharePayload),
|
try await process(route: .share(sharePayload),
|
||||||
expectedChatsState: .roomList(detailState: .room(roomID: "2")))
|
expectedChatsState: .roomList(detailState: .room(roomID: "2")))
|
||||||
|
|
||||||
|
let splitCoordinator = try #require(chatsSplitCoordinator)
|
||||||
|
let deferredSheet = deferFulfillment(splitCoordinator.observe(\.sheetCoordinatorID)) { $0 != nil }
|
||||||
|
try await deferredSheet.fulfill()
|
||||||
|
|
||||||
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
#expect(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
|
||||||
#expect(tabCoordinator?.sheetCoordinator == nil)
|
#expect(tabCoordinator?.sheetCoordinator == nil)
|
||||||
#expect((chatsSplitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
#expect((splitCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -240,21 +246,22 @@ struct UserSessionFlowCoordinatorTests {
|
|||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private mutating func process(route: AppRoute,
|
private func process(route: AppRoute,
|
||||||
expectedUserSessionState: UserSessionFlowCoordinator.State? = nil,
|
expectedUserSessionState: UserSessionFlowCoordinator.State? = nil,
|
||||||
expectedChatsState: ChatsTabFlowCoordinatorStateMachine.State? = nil) async throws {
|
expectedChatsState: ChatsTabFlowCoordinatorStateMachine.State? = nil) 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 = chatsSplitCoordinator?.detailRootCoordinator
|
||||||
|
let previousDetailRootCoordinatorID = previousDetailRootCoordinator.map { ObjectIdentifier($0) }
|
||||||
|
|
||||||
let deferredUserSession: DeferredFulfillment<UserSessionFlowCoordinator.State>? = if let expectedUserSessionState {
|
let deferredUserSession: DeferredFulfillment<UserSessionFlowCoordinator.State>? = if let expectedUserSessionState {
|
||||||
deferFulfillment(stateMachineFactory.userSessionFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)) {
|
deferFulfillment(stateMachineFactory.userSessionFlowStatePublisher) { $0 == expectedUserSessionState }
|
||||||
$0 == expectedUserSessionState
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let deferredChatsState: DeferredFulfillment<ChatsTabFlowCoordinatorStateMachine.State>? = if let expectedChatsState {
|
let deferredChatsState: DeferredFulfillment<ChatsTabFlowCoordinatorStateMachine.State>? = if let expectedChatsState {
|
||||||
deferFulfillment(stateMachineFactory.chatsTabFlowStatePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)) {
|
deferFulfillment(stateMachineFactory.chatsTabFlowStatePublisher) { $0 == expectedChatsState }
|
||||||
$0 == expectedChatsState
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
@@ -262,6 +269,36 @@ struct UserSessionFlowCoordinatorTests {
|
|||||||
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
|
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
|
||||||
try await deferredUserSession?.fulfill()
|
try await deferredUserSession?.fulfill()
|
||||||
try await deferredChatsState?.fulfill()
|
try await deferredChatsState?.fulfill()
|
||||||
|
|
||||||
|
// The state machines' states change before the coordinators have updated their stacks,
|
||||||
|
// so also wait for the navigation side effects implied by the expected states.
|
||||||
|
switch expectedUserSessionState {
|
||||||
|
case .settingsScreen:
|
||||||
|
let tabCoordinator = try #require(tabCoordinator)
|
||||||
|
let deferredSheet = deferFulfillment(tabCoordinator.observe(\.sheetCoordinatorID)) { $0 != nil }
|
||||||
|
try await deferredSheet.fulfill()
|
||||||
|
case .tabBar:
|
||||||
|
let tabCoordinator = try #require(tabCoordinator)
|
||||||
|
let deferredSheet = deferFulfillment(tabCoordinator.observe(\.sheetCoordinatorID)) { $0 == nil }
|
||||||
|
try await deferredSheet.fulfill()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch expectedChatsState {
|
||||||
|
case .roomList(detailState: .some(.room)):
|
||||||
|
let splitCoordinator = try #require(chatsSplitCoordinator)
|
||||||
|
let deferredDetail = deferFulfillment(splitCoordinator.observe(\.detailRootCoordinatorID)) { $0 != nil && $0 != previousDetailRootCoordinatorID }
|
||||||
|
try await deferredDetail.fulfill()
|
||||||
|
case .shareExtensionRoomList:
|
||||||
|
let splitCoordinator = try #require(chatsSplitCoordinator)
|
||||||
|
let deferredSheet = deferFulfillment(splitCoordinator.observe(\.sheetCoordinatorID)) { $0 != nil }
|
||||||
|
try await deferredSheet.fulfill()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
withExtendedLifetime(previousDetailRootCoordinator) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Other services retract indicators, so this filters based on the reachability ID.
|
/// Other services retract indicators, so this filters based on the reachability ID.
|
||||||
|
|||||||
Reference in New Issue
Block a user