Skip to content

Commit

Permalink
Added tests for typing indicators (WIP).
Browse files Browse the repository at this point in the history
  • Loading branch information
maratal committed Dec 16, 2024
1 parent 5a47cd5 commit 27b5428
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 6 deletions.
2 changes: 2 additions & 0 deletions Sources/AblyChat/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public protocol RealtimeChannelsProtocol: ARTRealtimeChannelsProtocol, Sendable
/// Expresses the requirements of the object returned by ``RealtimeChannelsProtocol.get(_:)``.
public protocol RealtimeChannelProtocol: ARTRealtimeChannelProtocol, Sendable {}

public protocol RealtimePresenceProtocol: ARTRealtimePresenceProtocol, Sendable {}

public protocol ConnectionProtocol: ARTConnectionProtocol, Sendable {}

/// Like (a subset of) `ARTRealtimeChannelOptions` but with value semantics. (It’s unfortunate that `ARTRealtimeChannelOptions` doesn’t have a `-copy` method.)
Expand Down
2 changes: 1 addition & 1 deletion Tests/AblyChatTests/DefaultRoomOccupancyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Testing
struct DefaultRoomOccupancyTests {
// @spec CHA-O1
@Test
func init_channelNameIsSetAsChatMessagesChannelName() async throws {
func channelNameIsSetAsChatMessagesChannelName() async throws {
// Given
let realtime = MockRealtime.create()
let chatAPI = ChatAPI(realtime: realtime)
Expand Down
101 changes: 101 additions & 0 deletions Tests/AblyChatTests/DefaultRoomTypingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import Ably
@testable import AblyChat
import Testing

struct DefaultRoomTypingTests {
// @spec CHA-T1
@Test
func channelNameIsSetAsChatMessagesChannelName() async throws {
// Given
let channel = MockRealtimeChannel(name: "basketball::$chat::$typingIndicators")
let featureChannel = MockFeatureChannel(channel: channel)

// When
let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger(), timeout: 5)

// Then
#expect(defaultTyping.channel.name == "basketball::$chat::$typingIndicators")
}

// @spec CHA-T2
@Test
func retrieveCurrentlyTypingClientIDs() async throws {
// Given
let typingPresence = MockRealtimePresence(["client1", "client2"].map { .init(clientId: $0) })
let channel = MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", mockPresence: typingPresence)
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(()))
let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "mockClientId", logger: TestLogger(), timeout: 5)

// When
let typingInfo = try await defaultTyping.get()

// Then
#expect(typingInfo.sorted() == ["client1", "client2"])
}

// @spec CHA-T4
// @spec CHA-T5
@Test
func usersMayIndicateThatTheyHaveStartedOrStoppedTyping() async throws {
// Given
let typingPresence = MockRealtimePresence([])
let channel = MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", mockPresence: typingPresence)
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(()))
let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "client1", logger: TestLogger(), timeout: 5)

// CHA-T4

// When
try await defaultTyping.start()

// Then
var typingInfo = try await defaultTyping.get()
#expect(typingInfo == ["client1"])

// CHA-T5

// When
try await defaultTyping.stop()

// Then
typingInfo = try await defaultTyping.get()
#expect(typingInfo.isEmpty)
}

// @spec CHA-T6
@Test
func usersMaySubscribeToTypingEvents() async throws {
// Given
let typingPresence = MockRealtimePresence([])
let channel = MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", mockPresence: typingPresence)
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(()))
let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "client1", logger: TestLogger(), timeout: 5)

// When
let subscription = await defaultTyping.subscribe()
subscription.emit(TypingEvent(currentlyTyping: ["client1"]))

// Then
let typingEvent = try #require(await subscription.first { _ in true })
#expect(typingEvent.currentlyTyping == ["client1"])
}

// @spec CHA-T7
@Test
func onDiscontinuity() async throws {
// Given
let typingPresence = MockRealtimePresence([])
let channel = MockRealtimeChannel(name: "basketball::$chat::$typingIndicators", mockPresence: typingPresence)
let featureChannel = MockFeatureChannel(channel: channel, resultOfWaitToBeAblePerformPresenceOperations: .success(()))
let defaultTyping = DefaultTyping(featureChannel: featureChannel, roomID: "basketball", clientID: "client1", logger: TestLogger(), timeout: 5)

// When: The feature channel emits a discontinuity through `onDiscontinuity`
let featureChannelDiscontinuity = DiscontinuityEvent(error: ARTErrorInfo.createUnknownError()) // arbitrary error
let discontinuitySubscription = await defaultTyping.onDiscontinuity()
await featureChannel.emitDiscontinuity(featureChannelDiscontinuity)

// Then: The DefaultOccupancy instance emits this discontinuity through `onDiscontinuity`
let discontinuity = try #require(await discontinuitySubscription.first { _ in true })
#expect(discontinuity == featureChannelDiscontinuity)
}
}
7 changes: 7 additions & 0 deletions Tests/AblyChatTests/Helpers/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ func isChatError(_ maybeError: (any Error)?, withCodeAndStatusCode codeAndStatus
return ablyError.message == message
}()
}

extension ARTPresenceMessage {
convenience init(clientId: String) {
self.init()
self.clientId = clientId
}
}
11 changes: 6 additions & 5 deletions Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ import Ably
import AblyChat

final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
var presence: ARTRealtimePresenceProtocol {
fatalError("Not implemented")
}

private let attachSerial: String?
private let channelSerial: String?
private let _name: String?
private let mockPresence: MockRealtimePresence!

var properties: ARTChannelProperties { .init(attachSerial: attachSerial, channelSerial: channelSerial) }

var presence: ARTRealtimePresenceProtocol { mockPresence }

// I don't see why the nonisolated(unsafe) keyword would cause a problem when used for tests in this context.
nonisolated(unsafe) var lastMessagePublishedName: String?
nonisolated(unsafe) var lastMessagePublishedData: Any?
Expand All @@ -22,13 +21,15 @@ final class MockRealtimeChannel: NSObject, RealtimeChannelProtocol {
properties: ARTChannelProperties = .init(),
state _: ARTRealtimeChannelState = .suspended,
attachResult: AttachOrDetachResult? = nil,
detachResult: AttachOrDetachResult? = nil
detachResult: AttachOrDetachResult? = nil,
mockPresence: MockRealtimePresence! = nil
) {
_name = name
self.attachResult = attachResult
self.detachResult = detachResult
attachSerial = properties.attachSerial
channelSerial = properties.channelSerial
self.mockPresence = mockPresence
}

/// A threadsafe counter that starts at zero.
Expand Down
105 changes: 105 additions & 0 deletions Tests/AblyChatTests/Mocks/MockRealtimePresence.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Ably
import AblyChat

final class MockRealtimePresence: NSObject, @unchecked Sendable, RealtimePresenceProtocol {
let syncComplete: Bool
private var members: [ARTPresenceMessage]

init(syncComplete: Bool = true, _ members: [ARTPresenceMessage]) {
self.syncComplete = syncComplete
self.members = members
}

func get(_ callback: @escaping ARTPresenceMessagesCallback) {
callback(members, nil)
}

func get(_: ARTRealtimePresenceQuery, callback _: @escaping ARTPresenceMessagesCallback) {
fatalError("Not implemented")
}

func enter(_: Any?) {
fatalError("Not implemented")
}

func enter(_: Any?, callback _: ARTCallback? = nil) {
fatalError("Not implemented")
}

func update(_: Any?) {
fatalError("Not implemented")
}

func update(_: Any?, callback _: ARTCallback? = nil) {
fatalError("Not implemented")
}

func leave(_: Any?) {
fatalError("Not implemented")
}

func leave(_: Any?, callback _: ARTCallback? = nil) {
fatalError("Not implemented")
}

func enterClient(_ clientId: String, data _: Any?) {
members.append(ARTPresenceMessage(clientId: clientId))
}

func enterClient(_ clientId: String, data _: Any?, callback: ARTCallback? = nil) {
members.append(ARTPresenceMessage(clientId: clientId))
callback?(nil)
}

func updateClient(_: String, data _: Any?) {
fatalError("Not implemented")
}

func updateClient(_: String, data _: Any?, callback _: ARTCallback? = nil) {
fatalError("Not implemented")
}

func leaveClient(_ clientId: String, data _: Any?) {
members.removeAll { $0.clientId == clientId }
}

func leaveClient(_ clientId: String, data _: Any?, callback _: ARTCallback? = nil) {
members.removeAll { $0.clientId == clientId }
}

func subscribe(_: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
ARTEventListener()
}

func subscribe(attachCallback _: ARTCallback?, callback _: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
ARTEventListener()
}

func subscribe(_: ARTPresenceAction, callback _: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
ARTEventListener()
}

func subscribe(_: ARTPresenceAction, onAttach _: ARTCallback?, callback _: @escaping ARTPresenceMessageCallback) -> ARTEventListener? {
ARTEventListener()
}

func unsubscribe() {
fatalError("Not implemented")
}

func unsubscribe(_: ARTEventListener) {
fatalError("Not implemented")
}

func unsubscribe(_: ARTPresenceAction, listener _: ARTEventListener) {
fatalError("Not implemented")
}

func history(_: @escaping ARTPaginatedPresenceCallback) {
fatalError("Not implemented")
}

func history(_: ARTRealtimeHistoryQuery?, callback _: @escaping ARTPaginatedPresenceCallback) throws {
fatalError("Not implemented")
}
}

0 comments on commit 27b5428

Please sign in to comment.