Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix label color when presenting. [#1077](https://github.com/GetStream/stream-video-swift/pull/1077)
- Ensure CallKit push token updates and invalidation mutate `deviceToken` on the main actor to avoid Swift concurrency/actor-isolation issues. [#1076](https://github.com/GetStream/stream-video-swift/pull/1076)
- Ensure CallKit joins keep the answer action completion alive until WebRTC has configured the audio device module. [#1081](https://github.com/GetStream/stream-video-swift/pull/1081)
- Update incoming call acceptance to move `CallViewModel` into `.joining` before the call finishes entering, so the joining UI appears immediately. [#1079](https://github.com/GetStream/stream-video-swift/pull/1079)

# [1.43.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.43.0)
_February 27, 2026_
Expand Down
5 changes: 5 additions & 0 deletions Sources/StreamVideoSwiftUI/CallViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,11 @@ open class CallViewModel: ObservableObject {
do {
hasAcceptedCall = true
try await call.accept()

// Mirror `joinCall` so the incoming UI is dismissed before
// `enterCall` finishes the async join flow.
await MainActor.run { self.setCallingState(.joining) }

enterCall(
call: call,
callType: callType,
Expand Down
29 changes: 29 additions & 0 deletions StreamVideoSwiftUITests/CallViewModel_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright © 2026 Stream.io Inc. All rights reserved.
//

import Combine
@testable import StreamVideo
@testable import StreamVideoSwiftUI
import StreamWebRTC
Expand Down Expand Up @@ -405,6 +406,34 @@ final class CallViewModel_Tests: XCTestCase, @unchecked Sendable {
await assertCallingState(.inCall)
}

func test_incomingCall_acceptCall_updatesCallingStateToJoiningBeforeInCall() async throws {
// Given
await prepareIncomingCallScenario()
let joiningStateExpectation = expectation(
description: "CallingState becomes joining"
)
joiningStateExpectation.assertForOverFulfill = false
var cancellable: AnyCancellable?

// Capture the transient state because `acceptCall` continues into the
// async `enterCall` flow immediately after the acceptance request.
cancellable = subject.$callingState
.dropFirst()
.sink { state in
if state == .joining {
joiningStateExpectation.fulfill()
}
}
defer { cancellable?.cancel() }

// When
subject.acceptCall(callType: callType, callId: callId)

// Then
await fulfillment(of: [joiningStateExpectation], timeout: defaultTimeout)
await assertCallingState(.inCall)
}
Comment on lines +409 to +435
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assert the transition order explicitly.

This only proves that .joining is emitted at some point before the final assertion. A sequence like .inCall → .joining → .inCall would still pass, so the regression on intermediate-state ordering is not fully covered. Record the emitted states and assert the first relevant transition is [.joining, .inCall].

Suggested test tightening
-        let joiningStateExpectation = expectation(
-            description: "CallingState becomes joining"
-        )
-        joiningStateExpectation.assertForOverFulfill = false
+        let stateSequenceExpectation = expectation(
+            description: "CallingState emits joining before inCall"
+        )
         var cancellable: AnyCancellable?
+        var emittedStates: [CallingState] = []

-        // Capture the transient state because `acceptCall` continues into the
-        // async `enterCall` flow immediately after the acceptance request.
         cancellable = subject.$callingState
             .dropFirst()
             .sink { state in
-                if state == .joining {
-                    joiningStateExpectation.fulfill()
+                emittedStates.append(state)
+                if Array(emittedStates.suffix(2)) == [.joining, .inCall] {
+                    stateSequenceExpectation.fulfill()
                 }
             }
         defer { cancellable?.cancel() }
@@
-        await fulfillment(of: [joiningStateExpectation], timeout: defaultTimeout)
+        await fulfillment(of: [stateSequenceExpectation], timeout: defaultTimeout)
+        XCTAssertEqual(Array(emittedStates.prefix(2)), [.joining, .inCall])
         await assertCallingState(.inCall)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@StreamVideoSwiftUITests/CallViewModel_Tests.swift` around lines 409 - 435,
The test test_incomingCall_acceptCall_updatesCallingStateToJoiningBeforeInCall
currently only verifies that .joining is emitted sometime before the final
assertCallingState(.inCall) and can miss ordering regressions; modify the test
to record the sequence of emitted callingState values from subject.$callingState
(use dropFirst() as before), collect them into an array until the final .inCall
is observed (or use an expectation that fulfills when .inCall is seen), then
assert that the observed transition sequence contains the ordered subsequence
[.joining, .inCall] (i.e., the first relevant states are [.joining, .inCall])
after calling subject.acceptCall(callType: callType, callId: callId) and before
calling assertCallingState(.inCall).


func test_incomingCall_acceptedFromSameUserElsewhere_callingStateChangesToIdle() async throws {
// Given
await prepareIncomingCallScenario()
Expand Down
Loading