From deaf54546da5caa504337d1a6066095cfceac237 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 25 Sep 2025 15:46:02 +0100 Subject: [PATCH 1/3] Refactor BookmarkFolderList, BookmarkList, FeedList and MemberList to use StateLayerEventPublisher --- .../PaginatedLists/BookmarkFolderList.swift | 20 +- .../BookmarkFolderListState+Observer.swift | 30 --- .../BookmarkFolderListState.swift | 54 +++--- .../PaginatedLists/BookmarkList.swift | 20 +- .../BookmarkListState+Observer.swift | 32 --- .../PaginatedLists/BookmarkListState.swift | 69 ++++--- .../StateLayer/PaginatedLists/FeedList.swift | 20 +- .../FeedListState+Observer.swift | 28 --- .../PaginatedLists/FeedListState.swift | 51 +++-- .../PaginatedLists/MemberList.swift | 20 +- .../MemberListState+Observer.swift | 34 ---- .../PaginatedLists/MemberListState.swift | 67 ++++--- .../BookmarkFolderListState_Tests.swift | 118 ++++++++++++ .../StateLayer/BookmarkListState_Tests.swift | 152 +++++++++++++++ .../StateLayer/FeedListState_Tests.swift | 96 +++++++++ .../StateLayer/FeedList_Tests.swift | 16 +- .../StateLayer/Feed_Tests.swift | 8 +- .../StateLayer/MemberListState_Tests.swift | 182 ++++++++++++++++++ .../BookmarkFolderData+Testing.swift | 25 +++ .../BookmarkFolderResponse+Testing.swift | 6 +- .../TestTools/FeedData+Testing.swift | 49 +++++ .../TestTools/FeedMemberData+Testing.swift | 32 +++ .../FeedMemberResponse+Testing.swift | 19 +- .../TestTools/FeedResponse+Testing.swift | 32 +-- ...QueryBookmarkFoldersResponse+Testing.swift | 2 +- .../QueryFeedMembersResponse+Testing.swift | 2 +- .../QueryFeedsResponse+Testing.swift | 6 +- .../TestTools/UserData+Testing.swift | 49 +++++ 28 files changed, 914 insertions(+), 325 deletions(-) delete mode 100644 Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState+Observer.swift delete mode 100644 Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState+Observer.swift delete mode 100644 Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState+Observer.swift delete mode 100644 Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState+Observer.swift create mode 100644 Tests/StreamFeedsTests/StateLayer/BookmarkFolderListState_Tests.swift create mode 100644 Tests/StreamFeedsTests/StateLayer/BookmarkListState_Tests.swift create mode 100644 Tests/StreamFeedsTests/StateLayer/FeedListState_Tests.swift create mode 100644 Tests/StreamFeedsTests/StateLayer/MemberListState_Tests.swift create mode 100644 Tests/StreamFeedsTests/TestTools/BookmarkFolderData+Testing.swift create mode 100644 Tests/StreamFeedsTests/TestTools/FeedData+Testing.swift create mode 100644 Tests/StreamFeedsTests/TestTools/FeedMemberData+Testing.swift create mode 100644 Tests/StreamFeedsTests/TestTools/UserData+Testing.swift diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderList.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderList.swift index 29a2f41..b212849 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderList.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderList.swift @@ -8,28 +8,28 @@ import StreamCore public final class BookmarkFolderList: Sendable { @MainActor private let stateBuilder: StateBuilder private let bookmarksRepository: BookmarksRepository - + init(query: BookmarkFoldersQuery, client: FeedsClient) { bookmarksRepository = client.bookmarksRepository self.query = query - let events = client.eventsMiddleware - stateBuilder = StateBuilder { BookmarkFolderListState(query: query, events: events) } + let eventPublisher = client.stateLayerEventPublisher + stateBuilder = StateBuilder { BookmarkFolderListState(query: query, eventPublisher: eventPublisher) } } public let query: BookmarkFoldersQuery - + // MARK: - Accessing the State - + /// An observable object representing the current state of the bookmark list. @MainActor public var state: BookmarkFolderListState { stateBuilder.state } - + // MARK: - Paginating the List of BookmarkFolders - + @discardableResult public func get() async throws -> [BookmarkFolderData] { try await queryBookmarkFolders(with: query) } - + public func queryMoreBookmarkFolders(limit: Int? = nil) async throws -> [BookmarkFolderData] { let nextQuery: BookmarkFoldersQuery? = await state.access { state in guard let next = state.pagination?.next else { return nil } @@ -44,9 +44,9 @@ public final class BookmarkFolderList: Sendable { guard let nextQuery else { return [] } return try await queryBookmarkFolders(with: nextQuery) } - + // MARK: - Private - + private func queryBookmarkFolders(with query: BookmarkFoldersQuery) async throws -> [BookmarkFolderData] { let result = try await bookmarksRepository.queryBookmarkFolders(with: query) await state.didPaginate( diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState+Observer.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState+Observer.swift deleted file mode 100644 index 3b72032..0000000 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState+Observer.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -import Foundation -import StreamCore - -extension BookmarkFolderListState { - final class WebSocketObserver: WSEventsSubscriber { - private let handlers: BookmarkFolderListState.ChangeHandlers - - init(subscribing events: WSEventsSubscribing, handlers: BookmarkFolderListState.ChangeHandlers) { - self.handlers = handlers - events.add(subscriber: self) - } - - // MARK: - Event Subscription - - func onEvent(_ event: any Event) async { - switch event { - case let event as BookmarkFolderDeletedEvent: - await handlers.bookmarkFolderRemoved(event.bookmarkFolder.id) - case let event as BookmarkFolderUpdatedEvent: - await handlers.bookmarkFolderUpdated(event.bookmarkFolder.toModel()) - default: - break - } - } - } -} diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift index 0bc9cbc..656b61f 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift @@ -7,30 +7,29 @@ import Foundation import StreamCore @MainActor public class BookmarkFolderListState: ObservableObject { - private var webSocketObserver: WebSocketObserver? - lazy var changeHandlers: ChangeHandlers = makeChangeHandlers() - - init(query: BookmarkFoldersQuery, events: WSEventsSubscribing) { + private var eventSubscription: StateLayerEventPublisher.Subscription? + + init(query: BookmarkFoldersQuery, eventPublisher: StateLayerEventPublisher) { self.query = query - webSocketObserver = WebSocketObserver(subscribing: events, handlers: changeHandlers) + subscribe(to: eventPublisher) } - + public let query: BookmarkFoldersQuery - + /// All the paginated folders. @Published public private(set) var folders: [BookmarkFolderData] = [] - + // MARK: - Pagination State - + /// Last pagination information. public private(set) var pagination: PaginationData? - + /// Indicates whether there are more bookmark folders available to load. public var canLoadMore: Bool { pagination?.next != nil } - + /// The configuration used for the last query. private(set) var queryConfig: QueryConfiguration? - + var bookmarksSorting: [Sort] { if let sort = queryConfig?.sort, !sort.isEmpty { return sort @@ -42,26 +41,27 @@ import StreamCore // MARK: - Updating the State extension BookmarkFolderListState { - struct ChangeHandlers { - let bookmarkFolderRemoved: @MainActor (String) -> Void - let bookmarkFolderUpdated: @MainActor (BookmarkFolderData) -> Void - } - - private func makeChangeHandlers() -> ChangeHandlers { - ChangeHandlers( - bookmarkFolderRemoved: { [weak self] id in - self?.folders.remove(byId: id) - }, - bookmarkFolderUpdated: { [weak self] folder in - self?.folders.replace(byId: folder) + private func subscribe(to publisher: StateLayerEventPublisher) { + eventSubscription = publisher.subscribe { [weak self] event in + switch event { + case .bookmarkFolderDeleted(let folder): + await self?.access { state in + state.folders.remove(byId: folder.id) + } + case .bookmarkFolderUpdated(let folder): + await self?.access { state in + state.folders.replace(byId: folder) + } + default: + break } - ) + } } - + func access(_ actions: @MainActor (BookmarkFolderListState) -> T) -> T { actions(self) } - + func didPaginate( with response: PaginationResult, for queryConfig: QueryConfiguration diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkList.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkList.swift index 92368d6..f5d8c74 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkList.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkList.swift @@ -8,28 +8,28 @@ import StreamCore public final class BookmarkList: Sendable { @MainActor private let stateBuilder: StateBuilder private let bookmarksRepository: BookmarksRepository - + init(query: BookmarksQuery, client: FeedsClient) { bookmarksRepository = client.bookmarksRepository self.query = query - let events = client.eventsMiddleware - stateBuilder = StateBuilder { BookmarkListState(query: query, events: events) } + let eventPublisher = client.stateLayerEventPublisher + stateBuilder = StateBuilder { BookmarkListState(query: query, eventPublisher: eventPublisher) } } public let query: BookmarksQuery - + // MARK: - Accessing the State - + /// An observable object representing the current state of the bookmark list. @MainActor public var state: BookmarkListState { stateBuilder.state } - + // MARK: - Paginating the List of Bookmarks - + @discardableResult public func get() async throws -> [BookmarkData] { try await queryBookmarks(with: query) } - + public func queryMoreBookmarks(limit: Int? = nil) async throws -> [BookmarkData] { let nextQuery: BookmarksQuery? = await state.access { state in guard let next = state.pagination?.next else { return nil } @@ -44,9 +44,9 @@ public final class BookmarkList: Sendable { guard let nextQuery else { return [] } return try await queryBookmarks(with: nextQuery) } - + // MARK: - Private - + private func queryBookmarks(with query: BookmarksQuery) async throws -> [BookmarkData] { let result = try await bookmarksRepository.queryBookmarks(with: query) await state.didPaginate( diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState+Observer.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState+Observer.swift deleted file mode 100644 index 85a8019..0000000 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState+Observer.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -import Foundation -import StreamCore - -extension BookmarkListState { - final class WebSocketObserver: WSEventsSubscriber { - private let handlers: BookmarkListState.ChangeHandlers - - init(subscribing events: WSEventsSubscribing, handlers: BookmarkListState.ChangeHandlers) { - self.handlers = handlers - events.add(subscriber: self) - } - - // MARK: - Event Subscription - - func onEvent(_ event: any Event) async { - switch event { - case let event as BookmarkFolderDeletedEvent: - await handlers.bookmarkFolderRemoved(event.bookmarkFolder.id) - case let event as BookmarkFolderUpdatedEvent: - await handlers.bookmarkFolderUpdated(event.bookmarkFolder.toModel()) - case let event as BookmarkUpdatedEvent: - await handlers.bookmarkUpdated(event.bookmark.toModel()) - default: - break - } - } - } -} diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState.swift index 0cc731f..c82612e 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkListState.swift @@ -7,30 +7,29 @@ import Foundation import StreamCore @MainActor public class BookmarkListState: ObservableObject { - private var webSocketObserver: WebSocketObserver? - lazy var changeHandlers: ChangeHandlers = makeChangeHandlers() - - init(query: BookmarksQuery, events: WSEventsSubscribing) { + private var eventSubscription: StateLayerEventPublisher.Subscription? + + init(query: BookmarksQuery, eventPublisher: StateLayerEventPublisher) { self.query = query - webSocketObserver = WebSocketObserver(subscribing: events, handlers: changeHandlers) + subscribe(to: eventPublisher) } - + public let query: BookmarksQuery - + /// All the paginated bookmarks. @Published public private(set) var bookmarks: [BookmarkData] = [] - + // MARK: - Pagination State - + /// Last pagination information. public private(set) var pagination: PaginationData? - + /// Indicates whether there are more bookmarks available to load. public var canLoadMore: Bool { pagination?.next != nil } - + /// The configuration used for the last query. private(set) var queryConfig: QueryConfiguration? - + var bookmarkFoldersSorting: [Sort] { if let sort = queryConfig?.sort, !sort.isEmpty { return sort @@ -42,41 +41,39 @@ import StreamCore // MARK: - Updating the State extension BookmarkListState { - /// Handlers for various state change events. - /// - /// These handlers are called when WebSocket events are received and automatically update the state accordingly. - struct ChangeHandlers { - let bookmarkFolderRemoved: @MainActor (String) -> Void - let bookmarkFolderUpdated: @MainActor (BookmarkFolderData) -> Void - let bookmarkUpdated: @MainActor (BookmarkData) -> Void - } - - private func makeChangeHandlers() -> ChangeHandlers { - ChangeHandlers( - bookmarkFolderRemoved: { [weak self] folderId in - self?.updateBookmarkFolder(with: folderId, changes: { $0.folder = nil }) - }, - bookmarkFolderUpdated: { [weak self] folder in - self?.updateBookmarkFolder(with: folder.id, changes: { $0.folder = folder }) - }, - bookmarkUpdated: { [weak self] feed in - // Only update, do not insert - self?.bookmarks.replace(byId: feed) + private func subscribe(to publisher: StateLayerEventPublisher) { + eventSubscription = publisher.subscribe { [weak self] event in + switch event { + case .bookmarkFolderDeleted(let folder): + await self?.access { state in + state.updateBookmarkFolder(with: folder.id, changes: { $0.folder = nil }) + } + case .bookmarkFolderUpdated(let folder): + await self?.access { state in + state.updateBookmarkFolder(with: folder.id, changes: { $0.folder = folder }) + } + case .bookmarkUpdated(let bookmark): + await self?.access { state in + // Only update, do not insert + state.bookmarks.replace(byId: bookmark) + } + default: + break } - ) + } } - + private func updateBookmarkFolder(with id: String, changes: (inout BookmarkData) -> Void) { guard let index = bookmarks.firstIndex(where: { $0.folder?.id == id }) else { return } var bookmark = bookmarks[index] changes(&bookmark) bookmarks[index] = bookmark } - + func access(_ actions: @MainActor (BookmarkListState) -> T) -> T { actions(self) } - + func didPaginate( with response: PaginationResult, for queryConfig: QueryConfiguration diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedList.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedList.swift index a00b1e4..ad29721 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedList.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedList.swift @@ -8,28 +8,28 @@ import StreamCore public final class FeedList: Sendable { @MainActor private let stateBuilder: StateBuilder private let feedsRepository: FeedsRepository - + init(query: FeedsQuery, client: FeedsClient) { feedsRepository = client.feedsRepository self.query = query - let events = client.eventsMiddleware - stateBuilder = StateBuilder { FeedListState(query: query, events: events) } + let eventPublisher = client.stateLayerEventPublisher + stateBuilder = StateBuilder { FeedListState(query: query, eventPublisher: eventPublisher) } } public let query: FeedsQuery - + // MARK: - Accessing the State - + /// An observable object representing the current state of the feed list. @MainActor public var state: FeedListState { stateBuilder.state } - + // MARK: - Paginating the List of Feeds - + @discardableResult public func get() async throws -> [FeedData] { try await queryFeeds(with: query) } - + public func queryMoreFeeds(limit: Int? = nil) async throws -> [FeedData] { let nextQuery: FeedsQuery? = await state.access { state in guard let next = state.pagination?.next else { return nil } @@ -45,9 +45,9 @@ public final class FeedList: Sendable { guard let nextQuery else { return [] } return try await queryFeeds(with: nextQuery) } - + // MARK: - Private - + private func queryFeeds(with query: FeedsQuery) async throws -> [FeedData] { let result = try await feedsRepository.queryFeeds(with: query) await state.didPaginate( diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState+Observer.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState+Observer.swift deleted file mode 100644 index da5f2e9..0000000 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState+Observer.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -import Foundation -import StreamCore - -extension FeedListState { - final class WebSocketObserver: WSEventsSubscriber { - private let handlers: FeedListState.ChangeHandlers - - init(subscribing events: WSEventsSubscribing, handlers: FeedListState.ChangeHandlers) { - self.handlers = handlers - events.add(subscriber: self) - } - - // MARK: - Event Subscription - - func onEvent(_ event: any Event) async { - switch event { - case let event as FeedUpdatedEvent: - await handlers.feedUpdated(event.feed.toModel()) - default: - break - } - } - } -} diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState.swift index 9123f79..d6c16c9 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/FeedListState.swift @@ -7,30 +7,29 @@ import Foundation import StreamCore @MainActor public class FeedListState: ObservableObject { - private var webSocketObserver: WebSocketObserver? - lazy var changeHandlers: ChangeHandlers = makeChangeHandlers() - - init(query: FeedsQuery, events: WSEventsSubscribing) { + private var eventSubscription: StateLayerEventPublisher.Subscription? + + init(query: FeedsQuery, eventPublisher: StateLayerEventPublisher) { self.query = query - webSocketObserver = WebSocketObserver(subscribing: events, handlers: changeHandlers) + subscribe(to: eventPublisher) } - + public let query: FeedsQuery - + /// All the paginated feeds. @Published public private(set) var feeds: [FeedData] = [] - + // MARK: - Pagination State - + /// Last pagination information. public private(set) var pagination: PaginationData? - + /// Indicates whether there are more feeds available to load. public var canLoadMore: Bool { pagination?.next != nil } - + /// The configuration used for the last activities query. private(set) var queryConfig: QueryConfiguration? - + var feedsSorting: [Sort] { if let sort = queryConfig?.sort, !sort.isEmpty { return sort @@ -42,26 +41,24 @@ import StreamCore // MARK: - Updating the State extension FeedListState { - /// Handlers for various state change events. - /// - /// These handlers are called when WebSocket events are received and automatically update the state accordingly. - struct ChangeHandlers { - let feedUpdated: @MainActor (FeedData) -> Void - } - - private func makeChangeHandlers() -> ChangeHandlers { - ChangeHandlers( - feedUpdated: { [weak self] feed in - // Only update, do not insert - self?.feeds.replace(byId: feed) + private func subscribe(to publisher: StateLayerEventPublisher) { + eventSubscription = publisher.subscribe { [weak self] event in + switch event { + case .feedUpdated(let feed, _): + await self?.access { state in + // Only update, do not insert + state.feeds.replace(byId: feed) + } + default: + break } - ) + } } - + func access(_ actions: @MainActor (FeedListState) -> T) -> T { actions(self) } - + func didPaginate( with response: PaginationResult, for queryConfig: QueryConfiguration diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberList.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberList.swift index 8eef478..36b1938 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberList.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberList.swift @@ -13,12 +13,12 @@ import StreamCore public final class MemberList: Sendable { @MainActor private let stateBuilder: StateBuilder private let feedsRepository: FeedsRepository - + init(query: MembersQuery, client: FeedsClient) { feedsRepository = client.feedsRepository self.query = query - let events = client.eventsMiddleware - stateBuilder = StateBuilder { MemberListState(query: query, events: events) } + let eventPublisher = client.stateLayerEventPublisher + stateBuilder = StateBuilder { MemberListState(query: query, eventPublisher: eventPublisher) } } /// The query configuration used to fetch members. @@ -26,18 +26,18 @@ public final class MemberList: Sendable { /// This contains the feed ID, filters, sorting options, and pagination parameters /// that define which members are retrieved and how they are ordered. public let query: MembersQuery - + // MARK: - Accessing the State - + /// An observable object representing the current state of the member list. /// /// This property provides access to the current members, pagination state, /// and other state information. The state is automatically updated when /// new members are loaded or when real-time updates are received. @MainActor public var state: MemberListState { stateBuilder.state } - + // MARK: - Paginating the List of Members - + /// Fetches the initial list of members based on the current query configuration. /// /// This method loads the first page of members according to the query's filters, @@ -50,7 +50,7 @@ public final class MemberList: Sendable { public func get() async throws -> [FeedMemberData] { try await queryMembers(with: query) } - + /// Loads the next page of members if more are available. /// /// This method fetches additional members using the pagination information @@ -78,9 +78,9 @@ public final class MemberList: Sendable { guard let nextQuery else { return [] } return try await queryMembers(with: nextQuery) } - + // MARK: - Private - + private func queryMembers(with query: MembersQuery) async throws -> [FeedMemberData] { let response = try await feedsRepository.queryFeedMembers( feedGroupId: query.feed.group, diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState+Observer.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState+Observer.swift deleted file mode 100644 index 86954ee..0000000 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState+Observer.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -import Foundation -import StreamCore - -extension MemberListState { - final class WebSocketObserver: WSEventsSubscriber { - private let handlers: MemberListState.ChangeHandlers - private let feed: FeedId - - init(feed: FeedId, subscribing events: WSEventsSubscribing, handlers: MemberListState.ChangeHandlers) { - self.handlers = handlers - self.feed = feed - events.add(subscriber: self) - } - - // MARK: - Event Subscription - - func onEvent(_ event: any Event) async { - switch event { - case let event as FeedMemberRemovedEvent: - guard event.fid == feed.rawValue else { return } - await handlers.memberRemoved(event.memberId) - case let event as FeedMemberUpdatedEvent: - guard event.fid == feed.rawValue else { return } - await handlers.memberUpdated(event.member.toModel()) - default: - break - } - } - } -} diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState.swift index 82ec3e2..499d6f5 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/MemberListState.swift @@ -12,47 +12,46 @@ import StreamCore /// and provides real-time updates when members are added, removed, or modified. /// It automatically handles WebSocket events to keep the member list synchronized. @MainActor public class MemberListState: ObservableObject { - private var webSocketObserver: WebSocketObserver? - lazy var changeHandlers: ChangeHandlers = makeChangeHandlers() - - init(query: MembersQuery, events: WSEventsSubscribing) { + private var eventSubscription: StateLayerEventPublisher.Subscription? + + init(query: MembersQuery, eventPublisher: StateLayerEventPublisher) { self.query = query - webSocketObserver = WebSocketObserver(feed: query.feed, subscribing: events, handlers: changeHandlers) + subscribe(to: eventPublisher) } - + /// The original query configuration used to fetch members. /// /// This contains the feed ID, filters, and sorting options that were used /// to create the initial member list. public let query: MembersQuery - + /// All the paginated members currently loaded. /// /// This array contains all members that have been fetched across multiple /// pagination requests. The members are automatically sorted according to /// the current sorting configuration. @Published public private(set) var members: [FeedMemberData] = [] - + // MARK: - Pagination State - + /// Last pagination information from the most recent request. /// /// Contains the `next` and `previous` cursor values that can be used /// to fetch additional pages of members. public private(set) var pagination: PaginationData? - + /// Indicates whether there are more members available to load. /// /// Returns `true` if there are additional members that can be fetched /// using the pagination information, `false` otherwise. public var canLoadMore: Bool { pagination?.next != nil } - + /// The configuration used for the last query. /// /// Contains the filter and sort parameters that were applied to the /// most recent member fetch operation. private(set) var queryConfig: QueryConfiguration? - + var membersSorting: [Sort] { if let sort = queryConfig?.sort, !sort.isEmpty { return sort @@ -64,33 +63,43 @@ import StreamCore // MARK: - Updating the State extension MemberListState { - struct ChangeHandlers { - let memberRemoved: @MainActor (String) -> Void - let memberUpdated: @MainActor (FeedMemberData) -> Void - } - - private func makeChangeHandlers() -> ChangeHandlers { - ChangeHandlers( - memberRemoved: { [weak self] memberId in - guard let index = self?.members.firstIndex(where: { $0.id == memberId }) else { return } - self?.members.remove(at: index) - }, - memberUpdated: { [weak self] member in - self?.members.replace(byId: member) + private func subscribe(to publisher: StateLayerEventPublisher) { + eventSubscription = publisher.subscribe { [weak self] event in + switch event { + case .feedMemberDeleted(let memberId, let feedId): + guard feedId == self?.query.feed else { return } + await self?.access { state in + guard let index = state.members.firstIndex(where: { $0.id == memberId }) else { return } + state.members.remove(at: index) + } + case .feedMemberUpdated(let member, let feedId): + guard feedId == self?.query.feed else { return } + await self?.access { state in + state.members.replace(byId: member) + } + case .feedMemberBatchUpdate(let updates, let feedId): + guard feedId == self?.query.feed else { return } + await self?.access { state in + // Skip added because the it might not belong to this list + state.members.replace(byIds: updates.updated) + state.members.remove(byIds: updates.removedIds) + } + default: + break } - ) + } } - + func access(_ actions: @MainActor (MemberListState) -> T) -> T { actions(self) } - + func applyUpdates(_ updates: ModelUpdates) { // Skip added because the it might not belong to this list members.replace(byIds: updates.updated) members.remove(byIds: updates.removedIds) } - + func didPaginate( with response: PaginationResult, for queryConfig: QueryConfiguration diff --git a/Tests/StreamFeedsTests/StateLayer/BookmarkFolderListState_Tests.swift b/Tests/StreamFeedsTests/StateLayer/BookmarkFolderListState_Tests.swift new file mode 100644 index 0000000..64b59ea --- /dev/null +++ b/Tests/StreamFeedsTests/StateLayer/BookmarkFolderListState_Tests.swift @@ -0,0 +1,118 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import StreamCore +@testable import StreamFeeds +import Testing + +struct BookmarkFolderListState_Tests { + // MARK: - Actions + + @Test func getUpdatesState() async throws { + let client = defaultClientWithBookmarkFolderResponses() + let bookmarkFolderList = client.bookmarkFolderList(for: BookmarkFoldersQuery()) + let folders = try await bookmarkFolderList.get() + let stateFolders = await bookmarkFolderList.state.folders + #expect(folders.count == 2) + #expect(stateFolders.count == 2) + #expect(stateFolders.map(\.id) == ["folder-1", "folder-2"]) + #expect(folders.map(\.id) == stateFolders.map(\.id)) + await #expect(bookmarkFolderList.state.canLoadMore == true) + await #expect(bookmarkFolderList.state.pagination?.next == "next-cursor") + } + + @Test func queryMoreBookmarkFoldersUpdatesState() async throws { + let client = defaultClientWithBookmarkFolderResponses([ + QueryBookmarkFoldersResponse.dummy( + bookmarkFolders: [ + .dummy(id: "folder-3", name: "Test Folder 3"), + .dummy(id: "folder-4", name: "Test Folder 4") + ], + next: "next-cursor-2" + ) + ]) + let bookmarkFolderList = client.bookmarkFolderList(for: BookmarkFoldersQuery()) + + // Initial load + _ = try await bookmarkFolderList.get() + let initialState = await bookmarkFolderList.state.folders + #expect(initialState.count == 2) + #expect(initialState.map(\.id) == ["folder-1", "folder-2"]) + + // Load more + let moreFolders = try await bookmarkFolderList.queryMoreBookmarkFolders() + let updatedState = await bookmarkFolderList.state.folders + #expect(moreFolders.count == 2) + #expect(moreFolders.map(\.id) == ["folder-3", "folder-4"]) + #expect(updatedState.count == 4) + await #expect(bookmarkFolderList.state.canLoadMore == true) + await #expect(bookmarkFolderList.state.pagination?.next == "next-cursor-2") + } + + // MARK: - WebSocket Events + + @Test func bookmarkFolderUpdatedEventUpdatesState() async throws { + let client = defaultClientWithBookmarkFolderResponses() + let bookmarkFolderList = client.bookmarkFolderList(for: BookmarkFoldersQuery()) + + // Initial load + _ = try await bookmarkFolderList.get() + let initialState = await bookmarkFolderList.state.folders + #expect(initialState.count == 2) + #expect(initialState.first { $0.id == "folder-1" }?.name == "Test Folder 1") + + // Send bookmark folder updated event + let updatedFolder = BookmarkFolderResponse.dummy(id: "folder-1", name: "Updated Folder Name").toModel() + await client.stateLayerEventPublisher.sendEvent(.bookmarkFolderUpdated(updatedFolder)) + + // Wait a bit for the event to be processed + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + let updatedState = await bookmarkFolderList.state.folders + #expect(updatedState.count == 2) + #expect(updatedState.first { $0.id == "folder-1" }?.name == "Updated Folder Name") + } + + @Test func bookmarkFolderDeletedEventUpdatesState() async throws { + let client = defaultClientWithBookmarkFolderResponses() + let bookmarkFolderList = client.bookmarkFolderList(for: BookmarkFoldersQuery()) + + // Initial load + _ = try await bookmarkFolderList.get() + let initialState = await bookmarkFolderList.state.folders + #expect(initialState.count == 2) + #expect(initialState.map(\.id) == ["folder-1", "folder-2"]) + + // Send bookmark folder deleted event + let deletedFolder = BookmarkFolderResponse.dummy(id: "folder-1", name: "Test Folder 1").toModel() + await client.stateLayerEventPublisher.sendEvent(.bookmarkFolderDeleted(deletedFolder)) + + // Wait a bit for the event to be processed + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + let updatedState = await bookmarkFolderList.state.folders + #expect(updatedState.count == 1) + #expect(updatedState.map(\.id) == ["folder-2"]) + } + + // MARK: - Helper Methods + + private func defaultClientWithBookmarkFolderResponses( + _ additionalPayloads: [any Encodable] = [] + ) -> FeedsClient { + FeedsClient.mock( + apiTransport: .withPayloads( + [ + QueryBookmarkFoldersResponse.dummy( + bookmarkFolders: [ + .dummy(id: "folder-1", name: "Test Folder 1"), + .dummy(id: "folder-2", name: "Test Folder 2") + ], + next: "next-cursor" + ) + ] + additionalPayloads + ) + ) + } +} diff --git a/Tests/StreamFeedsTests/StateLayer/BookmarkListState_Tests.swift b/Tests/StreamFeedsTests/StateLayer/BookmarkListState_Tests.swift new file mode 100644 index 0000000..23ed4a1 --- /dev/null +++ b/Tests/StreamFeedsTests/StateLayer/BookmarkListState_Tests.swift @@ -0,0 +1,152 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import StreamCore +@testable import StreamFeeds +import Testing + +struct BookmarkListState_Tests { + // MARK: - Actions + + @Test func getUpdatesState() async throws { + let client = defaultClientWithBookmarkResponses() + let bookmarkList = client.bookmarkList(for: BookmarksQuery()) + let bookmarks = try await bookmarkList.get() + let stateBookmarks = await bookmarkList.state.bookmarks + #expect(bookmarks.count == 2) + #expect(stateBookmarks.count == 2) + #expect(stateBookmarks.map(\.id) == ["activity-1user-1", "activity-2user-1"]) + #expect(bookmarks.map(\.id) == stateBookmarks.map(\.id)) + await #expect(bookmarkList.state.canLoadMore == true) + await #expect(bookmarkList.state.pagination?.next == "next-cursor") + } + + @Test func queryMoreBookmarksUpdatesState() async throws { + let client = defaultClientWithBookmarkResponses([ + QueryBookmarksResponse.dummy( + bookmarks: [ + .dummy(activity: .dummy(id: "activity-3"), user: .dummy(id: "user-1")), + .dummy(activity: .dummy(id: "activity-4"), user: .dummy(id: "user-1")) + ], + next: "next-cursor-2" + ) + ]) + let bookmarkList = client.bookmarkList(for: BookmarksQuery()) + + // Initial load + _ = try await bookmarkList.get() + let initialState = await bookmarkList.state.bookmarks + #expect(initialState.count == 2) + #expect(initialState.map(\.id) == ["activity-1user-1", "activity-2user-1"]) + + // Load more + let moreBookmarks = try await bookmarkList.queryMoreBookmarks() + let updatedState = await bookmarkList.state.bookmarks + #expect(moreBookmarks.count == 2) + #expect(moreBookmarks.map(\.id) == ["activity-3user-1", "activity-4user-1"]) + #expect(updatedState.count == 4) + await #expect(bookmarkList.state.canLoadMore == true) + await #expect(bookmarkList.state.pagination?.next == "next-cursor-2") + } + + // MARK: - WebSocket Events + + @Test func bookmarkUpdatedEventUpdatesState() async throws { + let client = defaultClientWithBookmarkResponses() + let bookmarkList = client.bookmarkList(for: BookmarksQuery()) + + // Initial load + _ = try await bookmarkList.get() + let initialState = await bookmarkList.state.bookmarks + #expect(initialState.count == 2) + #expect(initialState.first { $0.id == "activity-1user-1" }?.activity.text == "Test activity content") + + // Send bookmark updated event + let updatedBookmark = BookmarkResponse.dummy( + activity: .dummy(id: "activity-1", text: "Updated activity content"), + folder: .dummy(id: "folder-1", name: "Test Folder"), + user: .dummy(id: "user-1") + ).toModel() + await client.stateLayerEventPublisher.sendEvent(.bookmarkUpdated(updatedBookmark)) + + // Wait a bit for the event to be processed + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + let updatedState = await bookmarkList.state.bookmarks + #expect(updatedState.count == 2) + #expect(updatedState.first { $0.id == "activity-1user-1" }?.activity.text == "Updated activity content") + } + + @Test func bookmarkFolderUpdatedEventUpdatesState() async throws { + let client = defaultClientWithBookmarkResponses() + let bookmarkList = client.bookmarkList(for: BookmarksQuery()) + + // Initial load + _ = try await bookmarkList.get() + let initialState = await bookmarkList.state.bookmarks + #expect(initialState.count == 2) + #expect(initialState.first { $0.id == "activity-1user-1" }?.folder?.name == "Test Folder") + + // Send bookmark folder updated event + let updatedFolder = BookmarkFolderResponse.dummy(id: "folder-1", name: "Updated Folder Name").toModel() + await client.stateLayerEventPublisher.sendEvent(.bookmarkFolderUpdated(updatedFolder)) + + // Wait a bit for the event to be processed + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + let updatedState = await bookmarkList.state.bookmarks + #expect(updatedState.count == 2) + #expect(updatedState.first { $0.id == "activity-1user-1" }?.folder?.name == "Updated Folder Name") + } + + @Test func bookmarkFolderDeletedEventUpdatesState() async throws { + let client = defaultClientWithBookmarkResponses() + let bookmarkList = client.bookmarkList(for: BookmarksQuery()) + + // Initial load + _ = try await bookmarkList.get() + let initialState = await bookmarkList.state.bookmarks + #expect(initialState.count == 2) + #expect(initialState.first { $0.id == "activity-1user-1" }?.folder?.id == "folder-1") + + // Send bookmark folder deleted event + let deletedFolder = BookmarkFolderResponse.dummy(id: "folder-1", name: "Test Folder").toModel() + await client.stateLayerEventPublisher.sendEvent(.bookmarkFolderDeleted(deletedFolder)) + + // Wait a bit for the event to be processed + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + let updatedState = await bookmarkList.state.bookmarks + #expect(updatedState.count == 2) + #expect(updatedState.first { $0.id == "activity-1user-1" }?.folder == nil) + } + + // MARK: - Helper Methods + + private func defaultClientWithBookmarkResponses( + _ additionalPayloads: [any Encodable] = [] + ) -> FeedsClient { + FeedsClient.mock( + apiTransport: .withPayloads( + [ + QueryBookmarksResponse.dummy( + bookmarks: [ + .dummy( + activity: .dummy(id: "activity-1"), + folder: .dummy(id: "folder-1", name: "Test Folder"), + user: .dummy(id: "user-1") + ), + .dummy( + activity: .dummy(id: "activity-2"), + folder: .dummy(id: "folder-1", name: "Test Folder"), + user: .dummy(id: "user-1") + ) + ], + next: "next-cursor" + ) + ] + additionalPayloads + ) + ) + } +} diff --git a/Tests/StreamFeedsTests/StateLayer/FeedListState_Tests.swift b/Tests/StreamFeedsTests/StateLayer/FeedListState_Tests.swift new file mode 100644 index 0000000..65b42d7 --- /dev/null +++ b/Tests/StreamFeedsTests/StateLayer/FeedListState_Tests.swift @@ -0,0 +1,96 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import StreamCore +@testable import StreamFeeds +import Testing + +struct FeedListState_Tests { + // MARK: - Actions + + @Test func getUpdatesState() async throws { + let client = defaultClientWithFeedResponses() + let feedList = client.feedList(for: FeedsQuery()) + let feeds = try await feedList.get() + let stateFeeds = await feedList.state.feeds + #expect(feeds.count == 2) + #expect(stateFeeds.count == 2) + #expect(stateFeeds.map(\.id) == ["feed-1", "feed-2"]) + #expect(feeds.map(\.id) == stateFeeds.map(\.id)) + await #expect(feedList.state.canLoadMore == true) + await #expect(feedList.state.pagination?.next == "next-cursor") + } + + @Test func queryMoreFeedsUpdatesState() async throws { + let client = defaultClientWithFeedResponses([ + QueryFeedsResponse.dummy( + feeds: [ + .dummy(id: "feed-3", name: "Test Feed 3"), + .dummy(id: "feed-4", name: "Test Feed 4") + ], + next: "next-cursor-2" + ) + ]) + let feedList = client.feedList(for: FeedsQuery()) + + // Initial load + _ = try await feedList.get() + let initialState = await feedList.state.feeds + #expect(initialState.count == 2) + #expect(initialState.map(\.id) == ["feed-1", "feed-2"]) + + // Load more + let moreFeeds = try await feedList.queryMoreFeeds() + let updatedState = await feedList.state.feeds + #expect(moreFeeds.count == 2) + #expect(moreFeeds.map(\.id) == ["feed-3", "feed-4"]) + #expect(updatedState.count == 4) + await #expect(feedList.state.canLoadMore == true) + await #expect(feedList.state.pagination?.next == "next-cursor-2") + } + + // MARK: - WebSocket Events + + @Test func feedUpdatedEventUpdatesState() async throws { + let client = defaultClientWithFeedResponses() + let feedList = client.feedList(for: FeedsQuery()) + + // Initial load + _ = try await feedList.get() + let initialState = await feedList.state.feeds + #expect(initialState.count == 2) + #expect(initialState.first { $0.id == "feed-1" }?.name == "Test Feed 1") + + // Send feed updated event + let updatedFeed = FeedResponse.dummy(id: "feed-1", name: "Updated Feed Name").toModel() + await client.stateLayerEventPublisher.sendEvent(.feedUpdated(updatedFeed, FeedId(rawValue: "user:feed-1"))) + + // Wait a bit for the event to be processed + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + let updatedState = await feedList.state.feeds + #expect(updatedState.count == 2) + #expect(updatedState.first { $0.id == "feed-1" }?.name == "Updated Feed Name") + } + + // MARK: - Helper Methods + + private func defaultClientWithFeedResponses( + _ additionalPayloads: [any Encodable] = [] + ) -> FeedsClient { + FeedsClient.mock( + apiTransport: .withPayloads( + [ + QueryFeedsResponse.dummy( + feeds: [ + .dummy(id: "feed-1", name: "Test Feed 1"), + .dummy(id: "feed-2", name: "Test Feed 2") + ], + next: "next-cursor" + ) + ] + additionalPayloads + ) + ) + } +} diff --git a/Tests/StreamFeedsTests/StateLayer/FeedList_Tests.swift b/Tests/StreamFeedsTests/StateLayer/FeedList_Tests.swift index 9b8a61d..7d18dd3 100644 --- a/Tests/StreamFeedsTests/StateLayer/FeedList_Tests.swift +++ b/Tests/StreamFeedsTests/StateLayer/FeedList_Tests.swift @@ -26,8 +26,8 @@ struct FeedList_Tests { let client = defaultClientWithFeedsResponses([ QueryFeedsResponse.dummy( feeds: [ - .dummy(createdAt: Date.fixed(), id: "feed-3", name: "Third Feed"), - .dummy(createdAt: Date.fixed(), id: "feed-4", name: "Fourth Feed") + .dummy(id: "feed-3", name: "Third Feed", createdAt: Date.fixed()), + .dummy(id: "feed-4", name: "Fourth Feed", createdAt: Date.fixed()) ], next: "next-cursor-2" ) @@ -57,8 +57,8 @@ struct FeedList_Tests { apiTransport: .withPayloads([ QueryFeedsResponse.dummy( feeds: [ - .dummy(createdAt: Date.fixed(), id: "feed-1", name: "First Feed"), - .dummy(createdAt: Date.fixed(), id: "feed-2", name: "Second Feed") + .dummy(id: "feed-1", name: "First Feed", createdAt: Date.fixed()), + .dummy(id: "feed-2", name: "Second Feed", createdAt: Date.fixed()) ], next: nil ) @@ -110,7 +110,7 @@ struct FeedList_Tests { // Send feed updated event await client.eventsMiddleware.sendEvent( FeedUpdatedEvent.dummy( - feed: .dummy(createdAt: Date.fixed(), id: "feed-1", name: "Updated First Feed"), + feed: .dummy(id: "feed-1", name: "Updated First Feed", createdAt: Date.fixed()), fid: "user:test" ) ) @@ -133,7 +133,7 @@ struct FeedList_Tests { // Send feed updated event for unrelated feed await client.eventsMiddleware.sendEvent( FeedUpdatedEvent.dummy( - feed: .dummy(createdAt: Date.fixed(), id: "unrelated-feed", name: "Unrelated Feed"), + feed: .dummy(id: "unrelated-feed", name: "Unrelated Feed", createdAt: Date.fixed()), fid: "user:other" ) ) @@ -154,8 +154,8 @@ struct FeedList_Tests { [ QueryFeedsResponse.dummy( feeds: [ - .dummy(createdAt: Date.fixed(), id: "feed-1", name: "First Feed"), - .dummy(createdAt: Date.fixed(), id: "feed-2", name: "Second Feed") + .dummy(id: "feed-1", name: "First Feed", createdAt: Date.fixed()), + .dummy(id: "feed-2", name: "Second Feed", createdAt: Date.fixed()) ], next: "next-cursor" ) diff --git a/Tests/StreamFeedsTests/StateLayer/Feed_Tests.swift b/Tests/StreamFeedsTests/StateLayer/Feed_Tests.swift index b6bc4ff..1644631 100644 --- a/Tests/StreamFeedsTests/StateLayer/Feed_Tests.swift +++ b/Tests/StreamFeedsTests/StateLayer/Feed_Tests.swift @@ -26,8 +26,8 @@ struct Feed_Tests { let client = defaultClientWithActivities(feed: feedId.rawValue, [ UpdateFeedResponse.dummy( feed: .dummy( - custom: customData, - name: "Updated Feed Name" + name: "Updated Feed Name", + custom: customData ) ) ]) @@ -1256,7 +1256,7 @@ struct Feed_Tests { // Send unmatching update first - should be ignored await client.eventsMiddleware.sendEvent( FeedUpdatedEvent.dummy( - feed: .dummy(feed: "user:someoneelse", name: "Ignored") + feed: .dummy(name: "Ignored", feed: "user:someoneelse") ) ) @@ -1265,7 +1265,7 @@ struct Feed_Tests { // Send matching update - should refresh feed data await client.eventsMiddleware.sendEvent( FeedUpdatedEvent.dummy( - feed: .dummy(feed: feedId.rawValue, name: "New Name") + feed: .dummy(name: "New Name", feed: feedId.rawValue) ) ) diff --git a/Tests/StreamFeedsTests/StateLayer/MemberListState_Tests.swift b/Tests/StreamFeedsTests/StateLayer/MemberListState_Tests.swift new file mode 100644 index 0000000..5ca5dbb --- /dev/null +++ b/Tests/StreamFeedsTests/StateLayer/MemberListState_Tests.swift @@ -0,0 +1,182 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import StreamCore +@testable import StreamFeeds +import Testing + +struct MemberListState_Tests { + // MARK: - Actions + + @Test func getUpdatesState() async throws { + let client = defaultClientWithMemberResponses() + let memberList = client.memberList(for: MembersQuery(feed: FeedId(rawValue: "user:test"))) + let members = try await memberList.get() + let stateMembers = await memberList.state.members + #expect(members.count == 2) + #expect(stateMembers.count == 2) + #expect(stateMembers.map(\.id) == ["member-1", "member-2"]) + #expect(members.map(\.id) == stateMembers.map(\.id)) + await #expect(memberList.state.canLoadMore == true) + await #expect(memberList.state.pagination?.next == "next-cursor") + } + + @Test func queryMoreMembersUpdatesState() async throws { + let client = defaultClientWithMemberResponses([ + QueryFeedMembersResponse.dummy( + members: [ + .dummy(id: "member-3", user: .dummy(id: "user-3")), + .dummy(id: "member-4", user: .dummy(id: "user-4")) + ], + next: "next-cursor-2" + ) + ]) + let memberList = client.memberList(for: MembersQuery(feed: FeedId(rawValue: "user:test"))) + + // Initial load + _ = try await memberList.get() + let initialState = await memberList.state.members + #expect(initialState.count == 2) + #expect(initialState.map(\.id) == ["member-1", "member-2"]) + + // Load more + let moreMembers = try await memberList.queryMoreMembers() + let updatedState = await memberList.state.members + #expect(moreMembers.count == 2) + #expect(moreMembers.map(\.id) == ["member-3", "member-4"]) + #expect(updatedState.count == 4) + await #expect(memberList.state.canLoadMore == true) + await #expect(memberList.state.pagination?.next == "next-cursor-2") + } + + // MARK: - WebSocket Events + + @Test func feedMemberUpdatedEventUpdatesState() async throws { + let feedId = FeedId(rawValue: "user:test") + let client = defaultClientWithMemberResponses() + let memberList = client.memberList(for: MembersQuery(feed: feedId)) + + // Initial load + _ = try await memberList.get() + let initialState = await memberList.state.members + #expect(initialState.count == 2) + #expect(initialState.first { $0.id == "member-1" }?.user.id == "user-1") + + // Send feed member updated event + let updatedMember = FeedMemberResponse.dummy( + id: "member-1", + user: .dummy(id: "user-1-updated") + ).toModel() + await client.stateLayerEventPublisher.sendEvent(.feedMemberUpdated(updatedMember, feedId)) + + // Wait a bit for the event to be processed + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + let updatedState = await memberList.state.members + #expect(updatedState.count == 2) + #expect(updatedState.first { $0.id == "member-1" }?.user.id == "user-1-updated") + } + + @Test func feedMemberDeletedEventUpdatesState() async throws { + let feedId = FeedId(rawValue: "user:test") + let client = defaultClientWithMemberResponses() + let memberList = client.memberList(for: MembersQuery(feed: feedId)) + + // Initial load + _ = try await memberList.get() + let initialState = await memberList.state.members + #expect(initialState.count == 2) + #expect(initialState.map(\.id) == ["member-1", "member-2"]) + + // Send feed member deleted event + await client.stateLayerEventPublisher.sendEvent(.feedMemberDeleted("member-1", feedId)) + + // Wait a bit for the event to be processed + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + let updatedState = await memberList.state.members + #expect(updatedState.count == 1) + #expect(updatedState.map(\.id) == ["member-2"]) + } + + @Test func feedMemberBatchUpdateEventUpdatesState() async throws { + let feedId = FeedId(rawValue: "user:test") + let client = defaultClientWithMemberResponses() + let memberList = client.memberList(for: MembersQuery(feed: feedId)) + + // Initial load + _ = try await memberList.get() + let initialState = await memberList.state.members + #expect(initialState.count == 2) + #expect(initialState.map(\.id) == ["member-1", "member-2"]) + + // Send feed member batch update event + let updatedMember = FeedMemberResponse.dummy( + id: "member-1", + user: .dummy(id: "user-1-updated") + ).toModel() + let updates = ModelUpdates( + added: [], + removedIds: ["member-2"], + updated: [updatedMember] + ) + await client.stateLayerEventPublisher.sendEvent(.feedMemberBatchUpdate(updates, feedId)) + + // Wait a bit for the event to be processed + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + let updatedState = await memberList.state.members + #expect(updatedState.count == 1) + #expect(updatedState.first?.id == "member-1") + #expect(updatedState.first?.user.id == "user-1-updated") + } + + @Test func feedMemberEventForDifferentFeedIsIgnored() async throws { + let feedId = FeedId(rawValue: "user:test") + let otherFeedId = FeedId(rawValue: "user:other") + let client = defaultClientWithMemberResponses() + let memberList = client.memberList(for: MembersQuery(feed: feedId)) + + // Initial load + _ = try await memberList.get() + let initialState = await memberList.state.members + #expect(initialState.count == 2) + #expect(initialState.map(\.id) == ["member-1", "member-2"]) + + // Send feed member updated event for different feed + let updatedMember = FeedMemberResponse.dummy( + id: "member-1", + user: .dummy(id: "user-1-updated") + ).toModel() + await client.stateLayerEventPublisher.sendEvent(.feedMemberUpdated(updatedMember, otherFeedId)) + + // Wait a bit for the event to be processed + try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds + + let updatedState = await memberList.state.members + #expect(updatedState.count == 2) + #expect(updatedState.map(\.id) == ["member-1", "member-2"]) + #expect(updatedState.first { $0.id == "member-1" }?.user.id == "user-1") // Should not be updated + } + + // MARK: - Helper Methods + + private func defaultClientWithMemberResponses( + _ additionalPayloads: [any Encodable] = [] + ) -> FeedsClient { + FeedsClient.mock( + apiTransport: .withPayloads( + [ + QueryFeedMembersResponse.dummy( + members: [ + .dummy(id: "member-1", user: .dummy(id: "user-1")), + .dummy(id: "member-2", user: .dummy(id: "user-2")) + ], + next: "next-cursor" + ) + ] + additionalPayloads + ) + ) + } +} diff --git a/Tests/StreamFeedsTests/TestTools/BookmarkFolderData+Testing.swift b/Tests/StreamFeedsTests/TestTools/BookmarkFolderData+Testing.swift new file mode 100644 index 0000000..0594074 --- /dev/null +++ b/Tests/StreamFeedsTests/TestTools/BookmarkFolderData+Testing.swift @@ -0,0 +1,25 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Foundation +import StreamCore +@testable import StreamFeeds + +extension BookmarkFolderData { + static func dummy( + id: String = "folder-1", + name: String = "Test Folder", + createdAt: Date = .fixed(), + updatedAt: Date = .fixed(), + custom: [String: RawJSON]? = nil + ) -> BookmarkFolderData { + BookmarkFolderData( + createdAt: createdAt, + custom: custom, + id: id, + name: name, + updatedAt: updatedAt + ) + } +} diff --git a/Tests/StreamFeedsTests/TestTools/BookmarkFolderResponse+Testing.swift b/Tests/StreamFeedsTests/TestTools/BookmarkFolderResponse+Testing.swift index fb32de5..b8e1714 100644 --- a/Tests/StreamFeedsTests/TestTools/BookmarkFolderResponse+Testing.swift +++ b/Tests/StreamFeedsTests/TestTools/BookmarkFolderResponse+Testing.swift @@ -8,11 +8,11 @@ import StreamCore extension BookmarkFolderResponse { static func dummy( - createdAt: Date = .fixed(), - custom: [String: RawJSON]? = nil, id: String = "folder-1", name: String = "Test Folder", - updatedAt: Date = .fixed() + createdAt: Date = .fixed(), + updatedAt: Date = .fixed(), + custom: [String: RawJSON]? = nil ) -> BookmarkFolderResponse { BookmarkFolderResponse( createdAt: createdAt, diff --git a/Tests/StreamFeedsTests/TestTools/FeedData+Testing.swift b/Tests/StreamFeedsTests/TestTools/FeedData+Testing.swift new file mode 100644 index 0000000..b1982bb --- /dev/null +++ b/Tests/StreamFeedsTests/TestTools/FeedData+Testing.swift @@ -0,0 +1,49 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Foundation +import StreamCore +@testable import StreamFeeds + +extension FeedData { + static func dummy( + id: String = "feed-1", + name: String = "Test Feed", + createdAt: Date = .fixed(), + updatedAt: Date = .fixed(), + createdBy: UserData = .dummy(id: "user-1"), + feed: FeedId = FeedId(rawValue: "user:feed-1"), + custom: [String: RawJSON]? = nil, + deletedAt: Date? = nil, + description: String = "Test feed description", + filterTags: [String]? = nil, + followerCount: Int = 0, + followingCount: Int = 0, + groupId: String = "user", + memberCount: Int = 0, + pinCount: Int = 0, + visibility: String? = nil, + ownCapabilities: [FeedOwnCapability]? = nil + ) -> FeedData { + FeedData( + createdAt: createdAt, + createdBy: createdBy, + custom: custom, + deletedAt: deletedAt, + description: description, + feed: feed, + filterTags: filterTags, + followerCount: followerCount, + followingCount: followingCount, + groupId: groupId, + id: id, + memberCount: memberCount, + name: name, + pinCount: pinCount, + updatedAt: updatedAt, + visibility: visibility, + ownCapabilities: ownCapabilities + ) + } +} diff --git a/Tests/StreamFeedsTests/TestTools/FeedMemberData+Testing.swift b/Tests/StreamFeedsTests/TestTools/FeedMemberData+Testing.swift new file mode 100644 index 0000000..9fbeba0 --- /dev/null +++ b/Tests/StreamFeedsTests/TestTools/FeedMemberData+Testing.swift @@ -0,0 +1,32 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Foundation +import StreamCore +@testable import StreamFeeds + +extension FeedMemberData { + static func dummy( + id: String = "member-1", + user: UserData = .dummy(id: "user-1"), + createdAt: Date = .fixed(), + updatedAt: Date = .fixed(), + custom: [String: RawJSON]? = nil, + inviteAcceptedAt: Date? = nil, + inviteRejectedAt: Date? = nil, + role: String = "member", + status: FeedMemberStatus = .member + ) -> FeedMemberData { + FeedMemberData( + createdAt: createdAt, + custom: custom, + inviteAcceptedAt: inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt, + role: role, + status: status, + updatedAt: updatedAt, + user: user + ) + } +} diff --git a/Tests/StreamFeedsTests/TestTools/FeedMemberResponse+Testing.swift b/Tests/StreamFeedsTests/TestTools/FeedMemberResponse+Testing.swift index c52650b..b6256d5 100644 --- a/Tests/StreamFeedsTests/TestTools/FeedMemberResponse+Testing.swift +++ b/Tests/StreamFeedsTests/TestTools/FeedMemberResponse+Testing.swift @@ -8,17 +8,24 @@ import StreamCore extension FeedMemberResponse { static func dummy( + id: String = "member-1", + user: UserResponse = .dummy(), createdAt: Date = .fixed(), updatedAt: Date = .fixed(), - user: UserResponse + custom: [String: RawJSON]? = nil, + inviteAcceptedAt: Date? = nil, + inviteRejectedAt: Date? = nil, + role: String = "member", + status: FeedMemberResponseStatus = .member ) -> FeedMemberResponse { FeedMemberResponse( createdAt: createdAt, - custom: nil, - inviteAcceptedAt: nil, - inviteRejectedAt: nil, - role: "user", - status: .member, + custom: custom, + inviteAcceptedAt: inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt, + membershipLevel: nil, + role: role, + status: status, updatedAt: updatedAt, user: user ) diff --git a/Tests/StreamFeedsTests/TestTools/FeedResponse+Testing.swift b/Tests/StreamFeedsTests/TestTools/FeedResponse+Testing.swift index fd0010e..fad5b19 100644 --- a/Tests/StreamFeedsTests/TestTools/FeedResponse+Testing.swift +++ b/Tests/StreamFeedsTests/TestTools/FeedResponse+Testing.swift @@ -8,24 +8,23 @@ import StreamCore extension FeedResponse { static func dummy( - createdAt: Date = Date.fixed(), - createdBy: UserResponse = UserResponse.dummy(), - custom: [String: RawJSON] = [:], + id: String = "feed-1", + name: String = "Test Feed", + createdAt: Date = .fixed(), + updatedAt: Date = .fixed(), + createdBy: UserResponse = .dummy(), + feed: String = "user:feed-1", + custom: [String: RawJSON]? = nil, deletedAt: Date? = nil, description: String = "Test feed description", - feed: String = "user:test", filterTags: [String]? = nil, - followerCount: Int = 50, - followingCount: Int = 25, + followerCount: Int = 0, + followingCount: Int = 0, groupId: String = "user", - id: String = "feed-123", - memberCount: Int = 1, - name: String = "Test Feed", - ownCapabilities: [FeedOwnCapability] = FeedOwnCapability.allCases, - ownFollows: [FollowResponse]? = nil, - pinCount: Int = 2, - updatedAt: Date = Date.fixed(), - visibility: String? = "public" + memberCount: Int = 0, + pinCount: Int = 0, + visibility: String? = nil, + ownCapabilities: [FeedOwnCapability]? = nil ) -> FeedResponse { FeedResponse( createdAt: createdAt, @@ -42,10 +41,11 @@ extension FeedResponse { memberCount: memberCount, name: name, ownCapabilities: ownCapabilities, - ownFollows: ownFollows, + ownFollows: nil, + ownMembership: nil, pinCount: pinCount, updatedAt: updatedAt, - visibility: "public" + visibility: visibility ) } } diff --git a/Tests/StreamFeedsTests/TestTools/QueryBookmarkFoldersResponse+Testing.swift b/Tests/StreamFeedsTests/TestTools/QueryBookmarkFoldersResponse+Testing.swift index 015ef29..ed92032 100644 --- a/Tests/StreamFeedsTests/TestTools/QueryBookmarkFoldersResponse+Testing.swift +++ b/Tests/StreamFeedsTests/TestTools/QueryBookmarkFoldersResponse+Testing.swift @@ -8,7 +8,7 @@ import StreamCore extension QueryBookmarkFoldersResponse { static func dummy( - bookmarkFolders: [BookmarkFolderResponse] = [.dummy()], + bookmarkFolders: [BookmarkFolderResponse] = [], duration: String = "1.23ms", next: String? = nil, prev: String? = nil diff --git a/Tests/StreamFeedsTests/TestTools/QueryFeedMembersResponse+Testing.swift b/Tests/StreamFeedsTests/TestTools/QueryFeedMembersResponse+Testing.swift index ae36a9d..d0d31b1 100644 --- a/Tests/StreamFeedsTests/TestTools/QueryFeedMembersResponse+Testing.swift +++ b/Tests/StreamFeedsTests/TestTools/QueryFeedMembersResponse+Testing.swift @@ -9,7 +9,7 @@ import StreamCore extension QueryFeedMembersResponse { static func dummy( duration: String = "1.23ms", - members: [FeedMemberResponse] = [.dummy(user: .dummy(id: "feed-member-1"))], + members: [FeedMemberResponse] = [], next: String? = nil, prev: String? = nil ) -> QueryFeedMembersResponse { diff --git a/Tests/StreamFeedsTests/TestTools/QueryFeedsResponse+Testing.swift b/Tests/StreamFeedsTests/TestTools/QueryFeedsResponse+Testing.swift index fa070df..919043b 100644 --- a/Tests/StreamFeedsTests/TestTools/QueryFeedsResponse+Testing.swift +++ b/Tests/StreamFeedsTests/TestTools/QueryFeedsResponse+Testing.swift @@ -8,9 +8,9 @@ import StreamCore extension QueryFeedsResponse { static func dummy( - duration: String = "0.123s", - feeds: [FeedResponse] = [FeedResponse.dummy()], - next: String? = "next-cursor", + duration: String = "1.23ms", + feeds: [FeedResponse] = [], + next: String? = nil, prev: String? = nil ) -> QueryFeedsResponse { QueryFeedsResponse( diff --git a/Tests/StreamFeedsTests/TestTools/UserData+Testing.swift b/Tests/StreamFeedsTests/TestTools/UserData+Testing.swift new file mode 100644 index 0000000..4abf1b9 --- /dev/null +++ b/Tests/StreamFeedsTests/TestTools/UserData+Testing.swift @@ -0,0 +1,49 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Foundation +import StreamCore +@testable import StreamFeeds + +extension UserData { + static func dummy( + id: String = "user-1", + name: String? = "Test User", + image: String? = nil, + createdAt: Date = .fixed(), + updatedAt: Date = .fixed(), + banned: Bool = false, + blockedUserIds: [String] = [], + custom: [String: RawJSON] = [:], + deactivatedAt: Date? = nil, + deletedAt: Date? = nil, + language: String = "en", + lastActive: Date? = nil, + online: Bool = false, + revokeTokensIssuedBefore: Date? = nil, + role: String = "user", + teams: [String] = [], + teamsRole: [String: String]? = nil + ) -> UserData { + UserData( + banned: banned, + blockedUserIds: blockedUserIds, + createdAt: createdAt, + custom: custom, + deactivatedAt: deactivatedAt, + deletedAt: deletedAt, + id: id, + image: image, + language: language, + lastActive: lastActive, + name: name, + online: online, + revokeTokensIssuedBefore: revokeTokensIssuedBefore, + role: role, + teams: teams, + teamsRole: teamsRole, + updatedAt: updatedAt + ) + } +} From cce4672815ca77e778bb2be58cef1c50e7409d40 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 25 Sep 2025 16:43:56 +0100 Subject: [PATCH 2/3] Resolve tests --- .../StateLayer/MemberListState_Tests.swift | 45 +++++++++---------- .../FeedMemberResponse+Testing.swift | 1 - fastlane/Scanfile | 2 - 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/Tests/StreamFeedsTests/StateLayer/MemberListState_Tests.swift b/Tests/StreamFeedsTests/StateLayer/MemberListState_Tests.swift index 5ca5dbb..11b55ed 100644 --- a/Tests/StreamFeedsTests/StateLayer/MemberListState_Tests.swift +++ b/Tests/StreamFeedsTests/StateLayer/MemberListState_Tests.swift @@ -16,7 +16,7 @@ struct MemberListState_Tests { let stateMembers = await memberList.state.members #expect(members.count == 2) #expect(stateMembers.count == 2) - #expect(stateMembers.map(\.id) == ["member-1", "member-2"]) + #expect(stateMembers.map(\.id) == ["user-1", "user-2"]) #expect(members.map(\.id) == stateMembers.map(\.id)) await #expect(memberList.state.canLoadMore == true) await #expect(memberList.state.pagination?.next == "next-cursor") @@ -26,8 +26,8 @@ struct MemberListState_Tests { let client = defaultClientWithMemberResponses([ QueryFeedMembersResponse.dummy( members: [ - .dummy(id: "member-3", user: .dummy(id: "user-3")), - .dummy(id: "member-4", user: .dummy(id: "user-4")) + .dummy(user: .dummy(id: "user-3")), + .dummy(user: .dummy(id: "user-4")) ], next: "next-cursor-2" ) @@ -38,13 +38,13 @@ struct MemberListState_Tests { _ = try await memberList.get() let initialState = await memberList.state.members #expect(initialState.count == 2) - #expect(initialState.map(\.id) == ["member-1", "member-2"]) + #expect(initialState.map(\.id) == ["user-1", "user-2"]) // Load more let moreMembers = try await memberList.queryMoreMembers() let updatedState = await memberList.state.members #expect(moreMembers.count == 2) - #expect(moreMembers.map(\.id) == ["member-3", "member-4"]) + #expect(moreMembers.map(\.id) == ["user-3", "user-4"]) #expect(updatedState.count == 4) await #expect(memberList.state.canLoadMore == true) await #expect(memberList.state.pagination?.next == "next-cursor-2") @@ -61,12 +61,11 @@ struct MemberListState_Tests { _ = try await memberList.get() let initialState = await memberList.state.members #expect(initialState.count == 2) - #expect(initialState.first { $0.id == "member-1" }?.user.id == "user-1") + #expect(initialState.first { $0.id == "user-1" }?.user.id == "user-1") // Send feed member updated event let updatedMember = FeedMemberResponse.dummy( - id: "member-1", - user: .dummy(id: "user-1-updated") + user: .dummy(id: "user-1", name: "Updated User Name") ).toModel() await client.stateLayerEventPublisher.sendEvent(.feedMemberUpdated(updatedMember, feedId)) @@ -75,7 +74,7 @@ struct MemberListState_Tests { let updatedState = await memberList.state.members #expect(updatedState.count == 2) - #expect(updatedState.first { $0.id == "member-1" }?.user.id == "user-1-updated") + #expect(updatedState.first { $0.id == "user-1" }?.user.name == "Updated User Name") } @Test func feedMemberDeletedEventUpdatesState() async throws { @@ -87,17 +86,17 @@ struct MemberListState_Tests { _ = try await memberList.get() let initialState = await memberList.state.members #expect(initialState.count == 2) - #expect(initialState.map(\.id) == ["member-1", "member-2"]) + #expect(initialState.map(\.id) == ["user-1", "user-2"]) // Send feed member deleted event - await client.stateLayerEventPublisher.sendEvent(.feedMemberDeleted("member-1", feedId)) + await client.stateLayerEventPublisher.sendEvent(.feedMemberDeleted("user-1", feedId)) // Wait a bit for the event to be processed try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds let updatedState = await memberList.state.members #expect(updatedState.count == 1) - #expect(updatedState.map(\.id) == ["member-2"]) + #expect(updatedState.map(\.id) == ["user-2"]) } @Test func feedMemberBatchUpdateEventUpdatesState() async throws { @@ -109,16 +108,15 @@ struct MemberListState_Tests { _ = try await memberList.get() let initialState = await memberList.state.members #expect(initialState.count == 2) - #expect(initialState.map(\.id) == ["member-1", "member-2"]) + #expect(initialState.map(\.id) == ["user-1", "user-2"]) // Send feed member batch update event let updatedMember = FeedMemberResponse.dummy( - id: "member-1", - user: .dummy(id: "user-1-updated") + user: .dummy(id: "user-1", name: "Updated User Name") ).toModel() let updates = ModelUpdates( added: [], - removedIds: ["member-2"], + removedIds: ["user-2"], updated: [updatedMember] ) await client.stateLayerEventPublisher.sendEvent(.feedMemberBatchUpdate(updates, feedId)) @@ -128,8 +126,8 @@ struct MemberListState_Tests { let updatedState = await memberList.state.members #expect(updatedState.count == 1) - #expect(updatedState.first?.id == "member-1") - #expect(updatedState.first?.user.id == "user-1-updated") + #expect(updatedState.first?.id == "user-1") + #expect(updatedState.first?.user.name == "Updated User Name") } @Test func feedMemberEventForDifferentFeedIsIgnored() async throws { @@ -142,11 +140,10 @@ struct MemberListState_Tests { _ = try await memberList.get() let initialState = await memberList.state.members #expect(initialState.count == 2) - #expect(initialState.map(\.id) == ["member-1", "member-2"]) + #expect(initialState.map(\.id) == ["user-1", "user-2"]) // Send feed member updated event for different feed let updatedMember = FeedMemberResponse.dummy( - id: "member-1", user: .dummy(id: "user-1-updated") ).toModel() await client.stateLayerEventPublisher.sendEvent(.feedMemberUpdated(updatedMember, otherFeedId)) @@ -156,8 +153,8 @@ struct MemberListState_Tests { let updatedState = await memberList.state.members #expect(updatedState.count == 2) - #expect(updatedState.map(\.id) == ["member-1", "member-2"]) - #expect(updatedState.first { $0.id == "member-1" }?.user.id == "user-1") // Should not be updated + #expect(updatedState.map(\.id) == ["user-1", "user-2"]) + #expect(updatedState.first { $0.id == "user-1" }?.user.id == "user-1") // Should not be updated } // MARK: - Helper Methods @@ -170,8 +167,8 @@ struct MemberListState_Tests { [ QueryFeedMembersResponse.dummy( members: [ - .dummy(id: "member-1", user: .dummy(id: "user-1")), - .dummy(id: "member-2", user: .dummy(id: "user-2")) + .dummy(user: .dummy(id: "user-1")), + .dummy(user: .dummy(id: "user-2")) ], next: "next-cursor" ) diff --git a/Tests/StreamFeedsTests/TestTools/FeedMemberResponse+Testing.swift b/Tests/StreamFeedsTests/TestTools/FeedMemberResponse+Testing.swift index b6256d5..34bee9b 100644 --- a/Tests/StreamFeedsTests/TestTools/FeedMemberResponse+Testing.swift +++ b/Tests/StreamFeedsTests/TestTools/FeedMemberResponse+Testing.swift @@ -8,7 +8,6 @@ import StreamCore extension FeedMemberResponse { static func dummy( - id: String = "member-1", user: UserResponse = .dummy(), createdAt: Date = .fixed(), updatedAt: Date = .fixed(), diff --git a/fastlane/Scanfile b/fastlane/Scanfile index 2c94dca..60ba582 100644 --- a/fastlane/Scanfile +++ b/fastlane/Scanfile @@ -1,7 +1,5 @@ code_coverage(true) -disable_concurrent_testing(true) - configuration("Debug") result_bundle(true) From 2baa7486edd91c76b9fedf5b7f66af7728fd3b88 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 25 Sep 2025 17:09:38 +0100 Subject: [PATCH 3/3] Fix failing test and resolve warnings --- .../StateLayer/PaginatedLists/BookmarkFolderListState.swift | 4 ++-- Tests/StreamFeedsTests/StateLayer/Feed_Tests.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift index 656b61f..502a94e 100644 --- a/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift +++ b/Sources/StreamFeeds/StateLayer/PaginatedLists/BookmarkFolderListState.swift @@ -45,11 +45,11 @@ extension BookmarkFolderListState { eventSubscription = publisher.subscribe { [weak self] event in switch event { case .bookmarkFolderDeleted(let folder): - await self?.access { state in + _ = await self?.access { state in state.folders.remove(byId: folder.id) } case .bookmarkFolderUpdated(let folder): - await self?.access { state in + _ = await self?.access { state in state.folders.replace(byId: folder) } default: diff --git a/Tests/StreamFeedsTests/StateLayer/Feed_Tests.swift b/Tests/StreamFeedsTests/StateLayer/Feed_Tests.swift index 1644631..05d5ec3 100644 --- a/Tests/StreamFeedsTests/StateLayer/Feed_Tests.swift +++ b/Tests/StreamFeedsTests/StateLayer/Feed_Tests.swift @@ -1394,7 +1394,7 @@ struct Feed_Tests { [ GetOrCreateFeedResponse.dummy( activities: [.dummy(id: "1")], - feed: .dummy(feed: feed), + feed: .dummy(feed: feed, ownCapabilities: [.readFeed, .addActivity]), followers: [ FollowResponse.dummy( sourceFeed: .dummy(feed: "user:bob"),