Skip to content
Draft
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
36 changes: 0 additions & 36 deletions StreamVideoTests/Call/Call_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -370,42 +370,6 @@ final class Call_Tests: StreamVideoTestCase, @unchecked Sendable {
)
}

// MARK: - Duration
Copy link
Copy Markdown
Contributor Author

@ipavlidakis ipavlidakis Apr 16, 2026

Choose a reason for hiding this comment

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

That same behavior is already covered directly in CallState_Tests.swift (line 224), CallState_Tests.swift (line 237), and CallState_Tests.swift (line 278).


func test_call_duration() async throws {
let call = streamVideo?.call(callType: callType, callId: callId)
let startedAt = Date(timeIntervalSinceNow: -75)

call?.state.update(
from: CallResponse.dummy(
cid: callCid,
session: .dummy(
startedAt: startedAt
)
)
)

XCTAssertEqual(call?.state.startedAt, startedAt)
XCTAssertEqual(
call?.state.duration ?? 0,
Date().timeIntervalSince(startedAt),
accuracy: 1
)

call?.state.update(
from: CallResponse.dummy(
cid: callCid,
session: .dummy(
endedAt: Date(),
startedAt: startedAt
)
)
)

XCTAssertNil(call?.state.startedAt)
XCTAssertEqual(call?.state.duration, 0)
}

// MARK: - setIncomingVideoQualitySettings

func test_setIncomingVideoQualitySettings_updatesCallState() async throws {
Expand Down
34 changes: 21 additions & 13 deletions StreamVideoTests/IntegrationTests/Call_IntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ final class Call_IntegrationTests: XCTestCase, @unchecked Sendable {
.callFlow(id: .unique, type: .default, userId: .unique)
.perform { try await $0.client.setDevice(id: deviceId) }
.perform { try await $0.client.deleteDevice(id: deviceId) }
.perform { try await $0.client.listDevices() }
.assert { $0.value.isEmpty }
.assertEventually { try await $0.client.listDevices().isEmpty }
}

func test_deleteDevice_whenAVoIPDeviceIsremoved_thenListDevicesShoulBeUpdatedAsExpected() async throws {
Expand All @@ -69,8 +68,7 @@ final class Call_IntegrationTests: XCTestCase, @unchecked Sendable {
.callFlow(id: .unique, type: .default, userId: .unique)
.perform { try await $0.client.setVoipDevice(id: deviceId) }
.perform { try await $0.client.deleteDevice(id: deviceId) }
.perform { try await $0.client.listDevices() }
.assert { $0.value.isEmpty }
.assertEventually { try await $0.client.listDevices().isEmpty }
}

// MARK: Create
Expand Down Expand Up @@ -501,9 +499,14 @@ final class Call_IntegrationTests: XCTestCase, @unchecked Sendable {

group.addTask {
try await user2CallFlow
.perform { $0.client.subscribe(for: CallRingEvent.self) }
.assertEventually { (event: CallRingEvent) in event.call.id == callId }
.perform { try await $0.call.get() }
.assertEventually {
do {
_ = try await $0.call.get()
return true
} catch {
return false
}
}
.perform { try await $0.call.accept() }
.assertEventuallyInMainActor { $0.call.state.session?.acceptedBy[user2] != nil }
}
Expand Down Expand Up @@ -640,9 +643,12 @@ final class Call_IntegrationTests: XCTestCase, @unchecked Sendable {
backstage: .init(enabled: true)
) }
.perform { try await $0.call.join() }
.assertEventuallyInMainActor { $0.call.state.sessionId.isEmpty == false }
.assertEventuallyInMainActor { $0.call.state.backstage }

try await otherHostCallFlow
.perform { try await $0.call.join() }
.assertEventuallyInMainActor { $0.call.state.sessionId.isEmpty == false }

try await helpers
.callFlow(id: callId, type: .livestream, userId: participant, environment: "demo")
Expand Down Expand Up @@ -816,10 +822,10 @@ final class Call_IntegrationTests: XCTestCase, @unchecked Sendable {
.helpers
.callFlow(id: callId, type: .audioRoom, userId: participant, environment: "demo")
.perform { try await $0.call.join(callSettings: .init(audioOn: false, videoOn: false)) }
.assertEventuallyInMainActor { $0.call.state.participants.endIndex == 2 }
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) == false }
.assertEventuallyInMainActor { $0.call.state.callSettings.audioOn == false }
.perform { try await $0.call.microphone.toggle() }
.delay(0.5)
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) == false }
.assertEventuallyInMainActor { $0.call.state.callSettings.audioOn == false }
}
Expand Down Expand Up @@ -854,12 +860,12 @@ final class Call_IntegrationTests: XCTestCase, @unchecked Sendable {
group.addTask {
try await participantCallFlow
.perform { try await $0.call.join(callSettings: .init(audioOn: false, videoOn: false)) }
.assertEventuallyInMainActor { $0.call.state.participants.endIndex == 2 }
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) == false }
.assertEventuallyInMainActor { $0.call.state.callSettings.audioOn == false }
.perform { try await $0.call.request(permissions: [.sendAudio]) }
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) }
.perform { try await $0.call.microphone.toggle() }
.delay(0.5)
.assertEventuallyInMainActor { $0.call.state.callSettings.audioOn }
}

Expand Down Expand Up @@ -897,12 +903,12 @@ final class Call_IntegrationTests: XCTestCase, @unchecked Sendable {
.helpers
.callFlow(id: callId, type: .audioRoom, userId: participant, environment: "demo")
.perform { try await $0.call.join(callSettings: .init(audioOn: false, videoOn: false)) }
.assertEventuallyInMainActor { $0.call.state.participants.endIndex == 2 }
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) == false }
.assertEventuallyInMainActor { $0.call.state.callSettings.audioOn == false }
.perform { try await $0.call.request(permissions: [.sendAudio]) }
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) == false }
.perform { try await $0.call.microphone.toggle() }
.delay(0.5)
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) == false }
.assertEventuallyInMainActor { $0.call.state.callSettings.audioOn == false }
}
Expand Down Expand Up @@ -933,7 +939,11 @@ final class Call_IntegrationTests: XCTestCase, @unchecked Sendable {
.assertEventuallyInMainActor { $0.call.state.permissionRequests.endIndex == 1 }
.tryMap { await $0.call.state.permissionRequests.first }
.perform { try await $0.call.grant(request: $0.value) }
.delay(2)
.assertEventuallyInMainActor {
$0.call.state.participants.first {
$0.userId == participant
}?.hasAudio == true
}
.perform { try await $0.call.revoke(permissions: [.sendAudio], for: participant) }
}

Expand All @@ -943,15 +953,13 @@ final class Call_IntegrationTests: XCTestCase, @unchecked Sendable {
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) == false }
.perform { try await $0.call.request(permissions: [.sendAudio]) }
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) }
.delay(1) // Wait for the call state to be updated
.perform { try await $0.call.microphone.toggle() }
.assertEventuallyInMainActor { $0.call.state.callSettings.audioOn }
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) == false }
.assertEventuallyInMainActor { $0.call.state.callSettings.audioOn == false }
.performWithoutValueOverride { $0.call.updateCallSettingsManagers(with: await $0.call.state.callSettings) }
.assertEventuallyInMainActor { $0.call.microphone.status == .disabled }
.perform { try await $0.call.microphone.toggle() }
.delay(0.5)
.assertEventuallyInMainActor { $0.call.currentUserHasCapability(.sendAudio) == false }
.assertEventuallyInMainActor { $0.call.state.callSettings.audioOn == false }
}
Expand Down
6 changes: 3 additions & 3 deletions StreamVideoTests/TestUtils/XCTestCase+Wait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import XCTest
extension XCTestCase {

func wait(for interval: TimeInterval) async {
let waitExpectation = expectation(description: "Waiting for \(interval) seconds...")
waitExpectation.isInverted = true
await fulfillment(of: [waitExpectation], timeout: interval)
guard interval > 0 else { return }
let nanoseconds = UInt64((interval * 1_000_000_000).rounded())
try? await Task.sleep(nanoseconds: nanoseconds)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ final class VideoProximityPolicy_Tests: XCTestCase, @unchecked Sendable {

subject.didUpdateProximity(.near, on: mockCall)

await wait(for: 0.25)
XCTAssertEqual(mockCallController.timesCalled(.changeVideoState), 0)
XCTAssertEqual(mockCall.state.incomingVideoQualitySettings, .disabled(group: .all))
await assertState(
incomingVideoQualitySettings: .disabled(group: .all),
timesCalledChangeVideoState: 0
)
}

func test_didUpdateProximity_near_videoTrue_incomingVideoQualitySettingsNone_incomingVideoQualitySettingsAndCameraDisabled(
Expand All @@ -50,9 +51,10 @@ final class VideoProximityPolicy_Tests: XCTestCase, @unchecked Sendable {

subject.didUpdateProximity(.near, on: mockCall)

await wait(for: 0.25)
XCTAssertEqual(mockCallController.timesCalled(.changeVideoState), 1)
XCTAssertEqual(mockCall.state.incomingVideoQualitySettings, .disabled(group: .all))
await assertState(
incomingVideoQualitySettings: .disabled(group: .all),
timesCalledChangeVideoState: 1
)
}

func test_didUpdateProximity_near_videoFalse_incomingVideoQualitySettingsOtherThanNone_incomingVideoQualitySettingsAndCameraDisabled(
Expand All @@ -62,9 +64,10 @@ final class VideoProximityPolicy_Tests: XCTestCase, @unchecked Sendable {

subject.didUpdateProximity(.near, on: mockCall)

await wait(for: 0.25)
XCTAssertEqual(mockCallController.timesCalled(.changeVideoState), 0)
XCTAssertEqual(mockCall.state.incomingVideoQualitySettings, .disabled(group: .all))
await assertState(
incomingVideoQualitySettings: .disabled(group: .all),
timesCalledChangeVideoState: 0
)
}

func test_didUpdateProximity_far_noCachedValue_nothingHappens() async {
Expand All @@ -73,36 +76,45 @@ final class VideoProximityPolicy_Tests: XCTestCase, @unchecked Sendable {

subject.didUpdateProximity(.far, on: mockCall)

await wait(for: 0.25)
XCTAssertEqual(mockCallController.timesCalled(.changeVideoState), 0)
XCTAssertEqual(mockCall.state.incomingVideoQualitySettings, .manual(group: .all, targetSize: .quarter))
await assertState(
incomingVideoQualitySettings: .manual(group: .all, targetSize: .quarter),
timesCalledChangeVideoState: 0
)
}

func test_didUpdateProximity_far_cachedValueWithIncomingQualitySettingsAndVideoOff_incomingVideoQualitySettingsUpdated() async {
mockCall.state.callSettings = .init(videoOn: false)
mockCall.state.incomingVideoQualitySettings = .manual(group: .all, targetSize: .quarter)

subject.didUpdateProximity(.near, on: mockCall)
await wait(for: 0.25)
await assertState(
incomingVideoQualitySettings: .disabled(group: .all),
timesCalledChangeVideoState: 0
)
subject.didUpdateProximity(.far, on: mockCall)

await wait(for: 0.25)
XCTAssertEqual(mockCallController.timesCalled(.changeVideoState), 0)
XCTAssertEqual(mockCall.state.incomingVideoQualitySettings, .manual(group: .all, targetSize: .quarter))
await assertState(
incomingVideoQualitySettings: .manual(group: .all, targetSize: .quarter),
timesCalledChangeVideoState: 0
)
}

func test_didUpdateProximity_far_cachedValueWithoutIncomingQualitySettingsAndVideoOn_videoWasUpdated() async {
mockCall.state.callSettings = .init(videoOn: true)
mockCall.state.incomingVideoQualitySettings = .none

subject.didUpdateProximity(.near, on: mockCall)
await wait(for: 0.25)
await assertState(
incomingVideoQualitySettings: .disabled(group: .all),
timesCalledChangeVideoState: 1
)
subject.didUpdateProximity(.far, on: mockCall)

await wait(for: 0.25)
XCTAssertEqual(mockCallController.timesCalled(.changeVideoState), 2)
XCTAssertEqual(mockCallController.recordedInputPayload(Bool.self, for: .changeVideoState)?.last, true)
XCTAssertEqual(mockCall.state.incomingVideoQualitySettings, .none)
await assertState(
incomingVideoQualitySettings: .none,
timesCalledChangeVideoState: 2,
lastChangeVideoStateValue: true
)
}

func test_didUpdateProximity_far_cachedValueWithIncomingQualitySettingsAndVideoOn_incomingVideoQualitySettingsAndVideoWereUpdated(
Expand All @@ -111,12 +123,53 @@ final class VideoProximityPolicy_Tests: XCTestCase, @unchecked Sendable {
mockCall.state.incomingVideoQualitySettings = .manual(group: .all, targetSize: .quarter)

subject.didUpdateProximity(.near, on: mockCall)
await wait(for: 0.25)
await assertState(
incomingVideoQualitySettings: .disabled(group: .all),
timesCalledChangeVideoState: 1
)
subject.didUpdateProximity(.far, on: mockCall)

await wait(for: 0.25)
XCTAssertEqual(mockCallController.timesCalled(.changeVideoState), 2)
XCTAssertEqual(mockCallController.recordedInputPayload(Bool.self, for: .changeVideoState)?.last, true)
XCTAssertEqual(mockCall.state.incomingVideoQualitySettings, .manual(group: .all, targetSize: .quarter))
await assertState(
incomingVideoQualitySettings: .manual(group: .all, targetSize: .quarter),
timesCalledChangeVideoState: 2,
lastChangeVideoStateValue: true
)
}

// MARK: - Private helpers

private func assertState(
incomingVideoQualitySettings expectedIncomingVideoQualitySettings: IncomingVideoQualitySettings,
timesCalledChangeVideoState expectedTimesCalledChangeVideoState: Int,
lastChangeVideoStateValue expectedLastChangeVideoStateValue: Bool? = nil,
file: StaticString = #filePath,
line: UInt = #line
) async {
await fulfilmentInMainActor(timeout: 2, filePath: file, line: line) {
self.mockCall.state.incomingVideoQualitySettings == expectedIncomingVideoQualitySettings
&& self.mockCallController.timesCalled(.changeVideoState) == expectedTimesCalledChangeVideoState
}
Comment on lines +141 to +151
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

assertState no longer observes the “nothing happens” cases.

Line 148 uses fulfilmentInMainActor, which completes on the first successful predicate evaluation. For the zero-call scenarios in this file, that condition is often already true immediately, so the helper returns without any grace window and a delayed changeVideoState / state mutation would be missed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@StreamVideoTests/Utils/Proximity/Policies/VideoProximityPolicy_Tests.swift`
around lines 141 - 151, The current assertState uses fulfilmentInMainActor which
returns as soon as the predicate is true, so zero-call expectations can pass
immediately and miss later changes; update assertState to handle the
expectedTimesCalledChangeVideoState == 0 case by first capturing the current
incomingVideoQualitySettings and timesCalledChangeVideoState (via
mockCall.state.incomingVideoQualitySettings and
mockCallController.timesCalled(.changeVideoState)), then wait a short grace
period (e.g. 100–200ms) on the main actor and assert that both values did not
change during that window; keep the existing behaviour for
expectedTimesCalledChangeVideoState > 0 (use fulfilmentInMainActor as before to
wait until the predicate becomes true).


XCTAssertEqual(
mockCallController.timesCalled(.changeVideoState),
expectedTimesCalledChangeVideoState,
file: file,
line: line
)
XCTAssertEqual(
mockCall.state.incomingVideoQualitySettings,
expectedIncomingVideoQualitySettings,
file: file,
line: line
)

if let expectedLastChangeVideoStateValue {
XCTAssertEqual(
mockCallController.recordedInputPayload(Bool.self, for: .changeVideoState)?.last,
expectedLastChangeVideoStateValue,
file: file,
line: line
)
}
}
}
10 changes: 8 additions & 2 deletions StreamVideoTests/Utils/Timers/TimerPublisher_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,19 @@ final class TimerPublisher_Tests: XCTestCase, @unchecked Sendable {
func test_receive_whenResubscribed_timerResumes() async {
let subject = TimerPublisher(interval: 0.2)

let firstValueExpectation = expectation(description: "Should receive first value")
let expectation = expectation(description: "Should receive values after resubscription")

var cancellable = subject
.log(.debug) { "Received value: \($0.millisecondsSince1970)" }
.sink { [weak self] in self?.receivedDates.append($0) }
.sink { [weak self] in
self?.receivedDates.append($0)
if self?.receivedDates.count == 1 {
firstValueExpectation.fulfill()
}
}

await wait(for: 0.25)
await fulfillment(of: [firstValueExpectation], timeout: 1)
cancellable.cancel()

XCTAssertEqual(receivedDates.count, 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ final class WebRTCCoordinatorStateMachine_DisconnectedStageTests: XCTestCase, @u
)
transitionExpectation.isInverted = true
}
transitionExpectation.assertForOverFulfill = false

subject.transition = { target in
Task {
Expand All @@ -487,6 +488,9 @@ final class WebRTCCoordinatorStateMachine_DisconnectedStageTests: XCTestCase, @u
transitionExpectation
.expectationDescription =
"Expectation to land on id:\(expectedTarget) but instead landed on id:\(target.id)."
transitionExpectation.fulfill()
} else if expectedTarget == nil {
transitionExpectation.fulfill()
}
}
}
Expand All @@ -498,7 +502,10 @@ final class WebRTCCoordinatorStateMachine_DisconnectedStageTests: XCTestCase, @u
}

group.addTask {
await self.fulfillment(of: [transitionExpectation], timeout: defaultTimeout)
await self.fulfillment(
of: [transitionExpectation],
timeout: transitionExpectation.isInverted ? 2 : defaultTimeout
)
if transitionExpectation.isInverted {
await validationHandler(subject!)
}
Expand Down
Loading
Loading