diff --git a/Modules/Server/Sources/PocketCastsServer/Public/Discover/DiscoverServerHandler.swift b/Modules/Server/Sources/PocketCastsServer/Public/Discover/DiscoverServerHandler.swift index 90d1ab98a1..082f0ec1d3 100644 --- a/Modules/Server/Sources/PocketCastsServer/Public/Discover/DiscoverServerHandler.swift +++ b/Modules/Server/Sources/PocketCastsServer/Public/Discover/DiscoverServerHandler.swift @@ -85,6 +85,14 @@ public class DiscoverServerHandler { } } + public func discoverPodcastCollection(source: String) async -> PodcastCollection? { + return await withCheckedContinuation { continuation in + discoverRequest(path: source, type: PodcastCollection.self) { podcastCollection, _ in + continuation.resume(returning: podcastCollection) + } + } + } + public func discoverItem(_ source: String?, type: T.Type) -> AnyPublisher where T: Decodable { guard let source = source else { return Fail(error: DiscoverServerError.badRequest).eraseToAnyPublisher() diff --git a/PocketCastsTests/Extensions/XCTestCase+waitForCondition.swift b/PocketCastsTests/Extensions/XCTestCase+waitForCondition.swift new file mode 100644 index 0000000000..88fb73bf85 --- /dev/null +++ b/PocketCastsTests/Extensions/XCTestCase+waitForCondition.swift @@ -0,0 +1,24 @@ +extension XCTestCase { + /// Waits for the provided condition to become true, executing the check block until the timeout is reached. + /// - Parameters: + /// - timeout: The maximum time to wait for the condition to become true. + /// - pollingInterval: The interval to wait between checks, in seconds. + /// - condition: A closure that returns a boolean value indicating whether the condition is met. + func waitForCondition( + timeout: TimeInterval, + pollingInterval: TimeInterval = 0.5, + condition: @escaping () -> Bool + ) async { + let deadline = Date().addingTimeInterval(timeout) + + while Date() < deadline { + if condition() { + return // Condition met, exit the function + } + try? await Task.sleep(nanoseconds: UInt64(pollingInterval * 1_000_000_000)) // Sleep for the polling interval + } + + // If we reach here, the condition was not met in time + XCTFail("Condition was not met in time.") + } +} diff --git a/PocketCastsTests/Tests/Discover/FeaturedSummaryViewControllerTests.swift b/PocketCastsTests/Tests/Discover/FeaturedSummaryViewControllerTests.swift new file mode 100644 index 0000000000..37dd812914 --- /dev/null +++ b/PocketCastsTests/Tests/Discover/FeaturedSummaryViewControllerTests.swift @@ -0,0 +1,101 @@ +import XCTest +@testable import podcasts +import PocketCastsServer + + +final class FeaturedSummaryViewControllerTests: XCTestCase { + @MainActor func testPopulateItem() async throws { + let vc = FeaturedSummaryViewController(nibName: nil, bundle: nil) + + let jsonData = """ + { + "id": "featured", + "title": "Featured", + "type": "podcast_list", + "summary_style": "carousel", + "expanded_style": "plain_list", + "source": "https://lists.pocketcasts.com/featured.json", + "category_id": null, + "sponsored_podcasts": [ + { + "position": 0, + "source": "https://lists.pocketcasts.com/972fda60-5a69-4713-b925-bf89f69c5fda.json" + }, + { + "position": 2, + "source": "https://lists.pocketcasts.com/478726f8-7b18-441b-bc0a-c49939697c38.json" + } + ], + "regions": [ + "au", + "at", + "be", + "br", + "ca", + "cn", + "dk", + "fi", + "fr", + "de", + "ie", + "in", + "it", + "jp", + "mx", + "nl", + "no", + "nz", + "pl", + "pt", + "ru", + "kr", + "es", + "se", + "ch", + "gb", + "us", + "za", + "sg", + "ph", + "hk", + "sa", + "tr", + "il", + "cz", + "tw", + "ua", + "global" + ] + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let featuredItem = try decoder.decode(DiscoverItem.self, from: jsonData) + + vc.populateFrom(item: featuredItem, region: "global", category: nil) + + await self.waitForCondition(timeout: 5) { + vc.podcasts.isEmpty == false + } + + for podcast in featuredItem.sponsoredPodcasts ?? [] { + let index = try XCTUnwrap(podcast.position) + let source = try XCTUnwrap(podcast.source) + let loadedPodcast = await DiscoverServerHandler.shared.discoverPodcastCollection(source: source) + XCTAssertEqual(vc.podcasts[index].uuid, loadedPodcast?.podcasts?.first?.uuid) + } + + vc.populateFrom(item: featuredItem, region: "global", category: nil) + + self.eventually(timeout: 5) { + vc.podcasts.isEmpty == false + } + + for podcast in featuredItem.sponsoredPodcasts ?? [] { + let index = try XCTUnwrap(podcast.position) + let source = try XCTUnwrap(podcast.source) + let loadedPodcast = await DiscoverServerHandler.shared.discoverPodcastCollection(source: source) + XCTAssertEqual(vc.podcasts[index].uuid, loadedPodcast?.podcasts?.first?.uuid) + } + } +} diff --git a/podcasts.xcodeproj/project.pbxproj b/podcasts.xcodeproj/project.pbxproj index f677790db0..7706699937 100644 --- a/podcasts.xcodeproj/project.pbxproj +++ b/podcasts.xcodeproj/project.pbxproj @@ -1663,6 +1663,7 @@ F52B4F8C2BB4A9CA00E87BE4 /* CategoriesSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52B4F8B2BB4A9CA00E87BE4 /* CategoriesSelectorViewController.swift */; }; F52B4F8E2BB4A9ED00E87BE4 /* CategoriesSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52B4F8D2BB4A9ED00E87BE4 /* CategoriesSelectorView.swift */; }; F533F19D2C24B8CB00EDE9AA /* ShareDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = F533F19C2C24B8CB00EDE9AA /* ShareDestination.swift */; }; + F5431A622CC0B5EF003C36A0 /* XCTestCase+waitForCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5431A612CC0B5E9003C36A0 /* XCTestCase+waitForCondition.swift */; }; F543F6A42C0804FA00FEC8B6 /* AnyPublisher+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = F543F6A32C0804FA00FEC8B6 /* AnyPublisher+Async.swift */; }; F54E72192CA722A000CD5C86 /* Array+DiscoverItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54E72182CA722A000CD5C86 /* Array+DiscoverItem.swift */; }; F54E721D2CA7359800CD5C86 /* DiscoverCellType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54E721C2CA7359800CD5C86 /* DiscoverCellType.swift */; }; @@ -1698,6 +1699,7 @@ F5BA5C9C2C80F38200BDA5B9 /* UIViewControllerContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BA5C9B2C80F38200BDA5B9 /* UIViewControllerContentConfiguration.swift */; }; F5BA5C9E2C81254300BDA5B9 /* DiscoverCollectionViewController+DiscoverDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BA5C9D2C81254300BDA5B9 /* DiscoverCollectionViewController+DiscoverDelegate.swift */; }; F5BFF9892C6B0A9100A84561 /* Optional+ThrowOnNil.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BFF9882C6B0A9100A84561 /* Optional+ThrowOnNil.swift */; }; + F5C9CB7C2CBF6DD7007F6830 /* FeaturedSummaryViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C9CB7B2CBF6DD0007F6830 /* FeaturedSummaryViewControllerTests.swift */; }; F5CFDB622C6DA2D600DE57B2 /* AudioClipExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5CFDB612C6DA2D600DE57B2 /* AudioClipExporter.swift */; }; F5D3A0D92B70950100EED067 /* MockURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D3A0D82B70950100EED067 /* MockURLHandler.swift */; }; F5DBA58A2B756A8700AED77F /* PodcastSettings+ImportUserDefaultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5DBA5892B756A8700AED77F /* PodcastSettings+ImportUserDefaultsTests.swift */; }; @@ -3585,6 +3587,7 @@ F52B4F8B2BB4A9CA00E87BE4 /* CategoriesSelectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoriesSelectorViewController.swift; sourceTree = ""; }; F52B4F8D2BB4A9ED00E87BE4 /* CategoriesSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoriesSelectorView.swift; sourceTree = ""; }; F533F19C2C24B8CB00EDE9AA /* ShareDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareDestination.swift; sourceTree = ""; }; + F5431A612CC0B5E9003C36A0 /* XCTestCase+waitForCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+waitForCondition.swift"; sourceTree = ""; }; F543F6A32C0804FA00FEC8B6 /* AnyPublisher+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPublisher+Async.swift"; sourceTree = ""; }; F54E72182CA722A000CD5C86 /* Array+DiscoverItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+DiscoverItem.swift"; sourceTree = ""; }; F54E721C2CA7359800CD5C86 /* DiscoverCellType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoverCellType.swift; sourceTree = ""; }; @@ -3617,6 +3620,7 @@ F5BA5C9B2C80F38200BDA5B9 /* UIViewControllerContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerContentConfiguration.swift; sourceTree = ""; }; F5BA5C9D2C81254300BDA5B9 /* DiscoverCollectionViewController+DiscoverDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoverCollectionViewController+DiscoverDelegate.swift"; sourceTree = ""; }; F5BFF9882C6B0A9100A84561 /* Optional+ThrowOnNil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+ThrowOnNil.swift"; sourceTree = ""; }; + F5C9CB7B2CBF6DD0007F6830 /* FeaturedSummaryViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedSummaryViewControllerTests.swift; sourceTree = ""; }; F5CFDB612C6DA2D600DE57B2 /* AudioClipExporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioClipExporter.swift; sourceTree = ""; }; F5D3A0D82B70950100EED067 /* MockURLHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLHandler.swift; sourceTree = ""; }; F5DBA5892B756A8700AED77F /* PodcastSettings+ImportUserDefaultsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PodcastSettings+ImportUserDefaultsTests.swift"; sourceTree = ""; }; @@ -4750,6 +4754,7 @@ 8B484EF828D256F5001AFA97 /* Date+Ext.swift */, 8B484EFA28D25719001AFA97 /* Double+Ext.swift */, 8BA55A1228CA7425002BECC5 /* XCTestCase+eventually.swift */, + F5431A612CC0B5E9003C36A0 /* XCTestCase+waitForCondition.swift */, 321AC0292B6AFC45008D6FF2 /* XCTestCase+WaitForNotification.swift */, F5952FC92C057C6400754BC3 /* FMDatabaseQueue+Test.swift */, F5F69D7F2C07C867000B3C87 /* FMDatabaseQueue+Test.swift */, @@ -7134,6 +7139,7 @@ C7BF5E4829083B3F00733C1E /* Discover */ = { isa = PBXGroup; children = ( + F5C9CB7B2CBF6DD0007F6830 /* FeaturedSummaryViewControllerTests.swift */, C7BF5E4929083B5300733C1E /* DiscoverCoordinatorTests.swift */, ); path = Discover; @@ -9212,6 +9218,7 @@ FFF024CE2B62AC9400457373 /* IAPHelperTests.swift in Sources */, 8B3295432A5336DA00BDFA11 /* WhatsNewTests.swift in Sources */, F586959A2C04320100E0754A /* PodcastManagerTests.swift in Sources */, + F5431A622CC0B5EF003C36A0 /* XCTestCase+waitForCondition.swift in Sources */, C781EA412A6B72AE001DBFCC /* SearchableListViewModelTests.swift in Sources */, FF6BBCEE2C53E9D000604A01 /* TranscriptManagerTests.swift in Sources */, F5952FCA2C057C6400754BC3 /* FMDatabaseQueue+Test.swift in Sources */, @@ -9219,6 +9226,7 @@ 8B484EF728D23F06001AFA97 /* PlaybackTimeHelperTests.swift in Sources */, 45ECEF8627E910FB00C65030 /* ShowNotesFormatterUtilsTests.swift in Sources */, 2F63CE9528809E5B00A34B51 /* ThemeTests.swift in Sources */, + F5C9CB7C2CBF6DD7007F6830 /* FeaturedSummaryViewControllerTests.swift in Sources */, 8BA6CAFE2C404C3600FB4704 /* ComposeFilterTests.swift in Sources */, 8B317BA728906CC200A26A13 /* TestingViewController.swift in Sources */, 8BA55A1028CA6843002BECC5 /* AnalyticsPlaybackHelperTests.swift in Sources */, diff --git a/podcasts/FeaturedSummaryViewController.swift b/podcasts/FeaturedSummaryViewController.swift index 287c7c8268..ab54b78ed0 100644 --- a/podcasts/FeaturedSummaryViewController.swift +++ b/podcasts/FeaturedSummaryViewController.swift @@ -9,8 +9,8 @@ class FeaturedSummaryViewController: SimpleNotificationsViewController, GridLayo } } - private var podcasts = [DiscoverPodcast]() - private var sponsoredPodcasts = [DiscoverPodcast]() + private(set) var podcasts = [DiscoverPodcast]() + private(set) var sponsoredPodcasts = [DiscoverPodcast]() private var lists: [PodcastCollection] = [] private static let cellId = "FeaturedCollectionViewCell" @@ -212,14 +212,16 @@ class FeaturedSummaryViewController: SimpleNotificationsViewController, GridLayo self.podcasts = Array(podcastsToShow.prefix(self.maxFeaturedItems)) // Add sponsored podcasts - for sponsoredPodcastToAdd in sponsoredPodcastsToAdd { + for sponsoredPodcastToAdd in sponsoredPodcastsToAdd.sorted(by: { $0.key < $1.key }) { self.podcasts.insert(sponsoredPodcastToAdd.value, safelyAt: sponsoredPodcastToAdd.key) } self.sponsoredPodcasts = sponsoredPodcastsToAdd.map { $0.value } // Update and reload - self.updatePageCount() - self.featuredCollectionView.reloadData() + if isViewLoaded { + self.updatePageCount() + self.featuredCollectionView.reloadData() + } } }