Skip to content

Commit 610064c

Browse files
committed
Added typing tests.
1 parent 444de76 commit 610064c

File tree

5 files changed

+450
-26
lines changed

5 files changed

+450
-26
lines changed

Sources/AblyChat/DefaultTyping.swift

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ internal final class DefaultTyping: Typing {
66
private let clientID: String
77
private let logger: InternalLogger
88
private let timeout: TimeInterval
9+
private let maxPresenseGetRetryDuration: TimeInterval // Max duration as specified in CHA-T6c1
910
private let timerManager = TimerManager()
1011

11-
internal init(featureChannel: FeatureChannel, roomID: String, clientID: String, logger: InternalLogger, timeout: TimeInterval) {
12+
internal init(featureChannel: FeatureChannel, roomID: String, clientID: String, logger: InternalLogger, timeout: TimeInterval, maxPresenseGetRetryDuration: TimeInterval = 30.0) {
1213
self.roomID = roomID
1314
self.featureChannel = featureChannel
1415
self.clientID = clientID
1516
self.logger = logger
1617
self.timeout = timeout
18+
self.maxPresenseGetRetryDuration = maxPresenseGetRetryDuration
1719
}
1820

1921
internal nonisolated var channel: any RealtimeChannelProtocol {
@@ -32,18 +34,21 @@ internal final class DefaultTyping: Typing {
3234
logger.log(message: "Received presence message: \(message)", level: .debug)
3335
Task {
3436
let currentEventID = await eventTracker.updateEventID()
35-
let maxRetryDuration: TimeInterval = 30.0 // Max duration as specified in CHA-T6c1
3637
let baseDelay: TimeInterval = 1.0 // Initial retry delay
3738
let maxDelay: TimeInterval = 5.0 // Maximum delay between retries
3839

3940
var totalElapsedTime: TimeInterval = 0
4041
var delay: TimeInterval = baseDelay
4142

42-
while totalElapsedTime < maxRetryDuration {
43+
while totalElapsedTime < maxPresenseGetRetryDuration {
4344
do {
4445
// (CHA-T6c) When a presence event is received from the realtime client, the Chat client will perform a presence.get() operation to get the current presence set. This guarantees that we get a fully synced presence set. This is then used to emit the typing clients to the subscriber.
4546
let latestTypingMembers = try await get()
46-
47+
#if DEBUG
48+
for subscription in testPresenceGetTypingEventSubscriptions {
49+
subscription.emit(.init())
50+
}
51+
#endif
4752
// (CHA-T6c2) If multiple presence events are received resulting in concurrent presence.get() calls, then we guarantee that only the “latest” event is emitted. That is to say, if presence event A and B occur in that order, then only the typing event generated by B’s call to presence.get() will be emitted to typing subscribers.
4853
let isLatestEvent = await eventTracker.isLatestEvent(currentEventID)
4954
guard isLatestEvent else {
@@ -67,9 +72,19 @@ internal final class DefaultTyping: Typing {
6772

6873
// Exponential backoff (double the delay)
6974
delay = min(delay * 2, maxDelay)
75+
#if DEBUG
76+
for subscription in testPresenceGetRetryTypingEventSubscriptions {
77+
subscription.emit(.init())
78+
}
79+
#endif
7080
}
7181
}
72-
logger.log(message: "Failed to fetch presence set after \(maxRetryDuration) seconds. Giving up.", level: .error)
82+
#if DEBUG
83+
for subscription in testPresenceGetRetryTypingEventSubscriptions {
84+
subscription.unsubscribe()
85+
}
86+
#endif
87+
logger.log(message: "Failed to fetch presence set after \(maxPresenseGetRetryDuration) seconds. Giving up.", level: .error)
7388
}
7489
}
7590
return subscription
@@ -153,6 +168,11 @@ internal final class DefaultTyping: Typing {
153168
// (CHA-T5b) If typing is in progress, he CHA-T3 timeout is cancelled. The client then leaves presence.
154169
await timerManager.cancelTimer()
155170
channel.presence.leaveClient(clientID, data: nil)
171+
#if DEBUG
172+
for subscription in testStopTypingEventSubscriptions {
173+
subscription.emit(.init())
174+
}
175+
#endif
156176
} else {
157177
// (CHA-T5a) If typing is not in progress, this operation is no-op.
158178
logger.log(message: "User is not typing. No need to leave presence.", level: .debug)
@@ -202,12 +222,67 @@ internal final class DefaultTyping: Typing {
202222
try await stop()
203223
}
204224
}
225+
#if DEBUG
226+
for subscription in testStartTypingEventSubscriptions {
227+
subscription.emit(.init())
228+
}
229+
#endif
205230
}
206231
}
207232
}
208233
}
234+
#if DEBUG
235+
/// The `DefaultTyping` emits a `TestTypingEvent` each time ``start`` or ``stop`` is called.
236+
internal struct TestTypingEvent: Equatable {
237+
let timestamp = Date()
238+
}
239+
240+
/// Subscription of typing start events for testing purposes.
241+
private var testStartTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []
242+
243+
/// Subscription of typing stop events for testing purposes.
244+
private var testStopTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []
245+
246+
/// Subscription of presence get events for testing purposes.
247+
private var testPresenceGetTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []
248+
249+
/// Subscription of retry presence get events for testing purposes.
250+
private var testPresenceGetRetryTypingEventSubscriptions: [Subscription<TestTypingEvent>] = []
251+
252+
/// Returns a subscription which emits typing start events for testing purposes.
253+
internal func testsOnly_subscribeToStartTestTypingEvents() -> Subscription<TestTypingEvent> {
254+
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
255+
testStartTypingEventSubscriptions.append(subscription)
256+
return subscription
257+
}
258+
259+
/// Returns a subscription which emits typing stop events for testing purposes.
260+
internal func testsOnly_subscribeToStopTestTypingEvents() -> Subscription<TestTypingEvent> {
261+
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
262+
testStopTypingEventSubscriptions.append(subscription)
263+
return subscription
264+
}
265+
266+
/// Returns a subscription which emits presence get events for testing purposes.
267+
internal func testsOnly_subscribeToPresenceGetTypingEvents() -> Subscription<TestTypingEvent> {
268+
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
269+
testPresenceGetTypingEventSubscriptions.append(subscription)
270+
return subscription
271+
}
272+
273+
/// Returns a subscription which emits retry presence get events for testing purposes.
274+
internal func testsOnly_subscribeToPresenceGetRetryTypingEvents() -> Subscription<TestTypingEvent> {
275+
let subscription = Subscription<TestTypingEvent>(bufferingPolicy: .unbounded)
276+
testPresenceGetRetryTypingEventSubscriptions.append(subscription)
277+
return subscription
278+
}
279+
#endif
209280
}
210281

282+
#if DEBUG
283+
extension DefaultTyping: @unchecked Sendable { }
284+
#endif
285+
211286
private final actor EventTracker {
212287
private var latestEventID: UUID = .init()
213288

Tests/AblyChatTests/DefaultRoomPresenceTests.swift

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ struct DefaultRoomPresenceTests {
2626
@Test
2727
func usersMayEnterPresence() async throws {
2828
// Given
29-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
29+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
3030
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
3131
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(()))
3232
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "client2", logger: TestLogger())
@@ -51,7 +51,7 @@ struct DefaultRoomPresenceTests {
5151
let lifecycleManager = await RoomLifecycleHelper.createManager(contributors: [contributor])
5252

5353
// Given: A DefaultPresence with DefaultFeatureChannel and MockRoomLifecycleContributor
54-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
54+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
5555
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
5656
let featureChannel = DefaultFeatureChannel(channel: channel, contributor: contributor, roomLifecycleManager: lifecycleManager)
5757
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger())
@@ -85,7 +85,7 @@ struct DefaultRoomPresenceTests {
8585
let lifecycleManager = await RoomLifecycleHelper.createManager(contributors: [contributor])
8686

8787
// Given: A DefaultPresence with DefaultFeatureChannel and MockRoomLifecycleContributor
88-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
88+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
8989
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
9090
let featureChannel = DefaultFeatureChannel(channel: channel, contributor: contributor, roomLifecycleManager: lifecycleManager)
9191
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger())
@@ -121,7 +121,7 @@ struct DefaultRoomPresenceTests {
121121
@Test
122122
func failToEnterPresenceWhenRoomInInvalidState() async throws {
123123
// Given
124-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
124+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
125125
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
126126
let error = ARTErrorInfo(chatError: .presenceOperationRequiresRoomAttach(feature: .presence))
127127
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .failure(error))
@@ -147,7 +147,7 @@ struct DefaultRoomPresenceTests {
147147
@Test
148148
func usersMayUpdatePresence() async throws {
149149
// Given
150-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
150+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
151151
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
152152
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(()))
153153
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "client1", logger: TestLogger())
@@ -171,7 +171,7 @@ struct DefaultRoomPresenceTests {
171171
let lifecycleManager = await RoomLifecycleHelper.createManager(contributors: [contributor])
172172

173173
// Given: A DefaultPresence with DefaultFeatureChannel and MockRoomLifecycleContributor
174-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
174+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
175175
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
176176
let featureChannel = DefaultFeatureChannel(channel: channel, contributor: contributor, roomLifecycleManager: lifecycleManager)
177177
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger())
@@ -205,7 +205,7 @@ struct DefaultRoomPresenceTests {
205205
let lifecycleManager = await RoomLifecycleHelper.createManager(contributors: [contributor])
206206

207207
// Given: A DefaultPresence with DefaultFeatureChannel and MockRoomLifecycleContributor
208-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
208+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
209209
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
210210
let featureChannel = DefaultFeatureChannel(channel: channel, contributor: contributor, roomLifecycleManager: lifecycleManager)
211211
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger())
@@ -241,7 +241,7 @@ struct DefaultRoomPresenceTests {
241241
@Test
242242
func failToUpdatePresenceWhenRoomInInvalidState() async throws {
243243
// Given
244-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
244+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
245245
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
246246
let error = ARTErrorInfo(chatError: .presenceOperationRequiresRoomAttach(feature: .presence))
247247
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .failure(error))
@@ -266,7 +266,7 @@ struct DefaultRoomPresenceTests {
266266
@Test
267267
func usersMayLeavePresence() async throws {
268268
// Given
269-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
269+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
270270
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
271271
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(()))
272272
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "client1", logger: TestLogger())
@@ -285,7 +285,7 @@ struct DefaultRoomPresenceTests {
285285
@Test
286286
func ifUserIsPresent() async throws {
287287
// Given
288-
let realtimePresence = MockRealtimePresence(["client1", "client2"].map { .init(clientId: $0) })
288+
let realtimePresence = MockRealtimePresence(members: ["client1", "client2"].map { .init(clientId: $0) })
289289
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
290290
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(())) // CHA-PR6d
291291
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger())
@@ -306,7 +306,7 @@ struct DefaultRoomPresenceTests {
306306
@Test
307307
func retrieveAllTheMembersOfThePresenceSet() async throws {
308308
// Given
309-
let realtimePresence = MockRealtimePresence(["client1", "client2"].map { .init(clientId: $0) })
309+
let realtimePresence = MockRealtimePresence(members: ["client1", "client2"].map { .init(clientId: $0) })
310310
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
311311
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(()))
312312
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger())
@@ -322,7 +322,7 @@ struct DefaultRoomPresenceTests {
322322
@Test
323323
func failToRetrieveAllTheMembersOfThePresenceSetWhenRoomInInvalidState() async throws {
324324
// Given
325-
let realtimePresence = MockRealtimePresence(["client1", "client2"].map { .init(clientId: $0) })
325+
let realtimePresence = MockRealtimePresence(members: ["client1", "client2"].map { .init(clientId: $0) })
326326
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
327327
let error = ARTErrorInfo(chatError: .presenceOperationRequiresRoomAttach(feature: .presence))
328328
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .failure(error))
@@ -349,7 +349,7 @@ struct DefaultRoomPresenceTests {
349349
let lifecycleManager = await RoomLifecycleHelper.createManager(contributors: [contributor])
350350

351351
// Given: A DefaultPresence with DefaultFeatureChannel and MockRoomLifecycleContributor
352-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
352+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
353353
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
354354
let featureChannel = DefaultFeatureChannel(channel: channel, contributor: contributor, roomLifecycleManager: lifecycleManager)
355355
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger())
@@ -383,7 +383,7 @@ struct DefaultRoomPresenceTests {
383383
let lifecycleManager = await RoomLifecycleHelper.createManager(contributors: [contributor])
384384

385385
// Given: A DefaultPresence with DefaultFeatureChannel and MockRoomLifecycleContributor
386-
let realtimePresence = MockRealtimePresence(["client1"].map { .init(clientId: $0) })
386+
let realtimePresence = MockRealtimePresence(members: ["client1"].map { .init(clientId: $0) })
387387
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
388388
let featureChannel = DefaultFeatureChannel(channel: channel, contributor: contributor, roomLifecycleManager: lifecycleManager)
389389
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger())
@@ -423,7 +423,7 @@ struct DefaultRoomPresenceTests {
423423
@Test
424424
func usersMaySubscribeToAllPresenceEvents() async throws {
425425
// Given
426-
let realtimePresence = MockRealtimePresence(["client1", "client2"].map { .init(clientId: $0) })
426+
let realtimePresence = MockRealtimePresence(members: ["client1", "client2"].map { .init(clientId: $0) })
427427
let channel = MockRealtimeChannel(name: "basketball::$chat::$chatMessages", mockPresence: realtimePresence)
428428
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(())) // CHA-PR6d
429429
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger())
@@ -485,7 +485,7 @@ struct DefaultRoomPresenceTests {
485485
@Test
486486
func onDiscontinuity() async throws {
487487
// Given
488-
let realtimePresence = MockRealtimePresence([])
488+
let realtimePresence = MockRealtimePresence(members: [])
489489
let channel = MockRealtimeChannel(mockPresence: realtimePresence)
490490
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(()))
491491
let defaultPresence = await DefaultPresence(featureChannel: featureChannel, roomID: "basketball", clientID: "client1", logger: TestLogger())

0 commit comments

Comments
 (0)