From 8a2861b37f34e12249e7e0fba7850444703b4185 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sat, 30 Dec 2023 09:08:19 +0100 Subject: [PATCH] Add stream tests --- IceCubesApp.xcodeproj/project.pbxproj | 58 ------------------ .../Localization/Localizable.xcstrings | 3 +- Packages/Status/Package.swift | 4 ++ .../Sources/Timeline/TimelineDatasource.swift | 4 ++ .../Sources/Timeline/TimelineView.swift | 6 +- .../Sources/Timeline/TimelineViewModel.swift | 60 +++++++++---------- .../Tests/TimelineTests/TimelineTests.swift | 11 ---- .../TimelineViewModelTests.swift | 24 ++++++++ 8 files changed, 65 insertions(+), 105 deletions(-) delete mode 100644 Packages/Timeline/Tests/TimelineTests/TimelineTests.swift create mode 100644 Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 580ca4a6..eeba66f8 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -7,8 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 065FA1FE29866CD600012EA0 /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 065FA1FD29866CD600012EA0 /* LRUCache */; }; - 065FA20A298675BA00012EA0 /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 065FA209298675BA00012EA0 /* LRUCache */; }; 069709A5298C8545006E4CB5 /* Atkinson-Hyperlegible-Regular-102.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 069709A3298C8545006E4CB5 /* Atkinson-Hyperlegible-Regular-102.ttf */; }; 069709A8298C87B5006E4CB5 /* OpenDyslexic-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 069709A7298C87B5006E4CB5 /* OpenDyslexic-Regular.otf */; }; 069709AA298C9AD7006E4CB5 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069709A9298C9AD7006E4CB5 /* AboutView.swift */; }; @@ -93,13 +91,10 @@ 9FD542E72962D2FF0045321A /* Lists in Frameworks */ = {isa = PBXBuildFile; productRef = 9FD542E62962D2FF0045321A /* Lists */; }; 9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE151A5293C90F900E9683D /* IconSelectorView.swift */; }; 9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE3DB56296FEFCA00628CB0 /* AppAccount */; }; - 9FF614472B2EDCE500F7B0E6 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = 9FF614462B2EDCE500F7B0E6 /* GiphyUISDK */; }; - 9FF614492B2EDCEC00F7B0E6 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; productRef = 9FF614482B2EDCEC00F7B0E6 /* GiphyUISDK */; }; 9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; }; 9FFF677E299B7D2800FE700A /* Status in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677D299B7D2800FE700A /* Status */; }; 9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; }; 9FFF6782299B7D3A00FE700A /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6781299B7D3A00FE700A /* Account */; }; - 9FFF6784299B7D4400FE700A /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF6783299B7D4400FE700A /* LRUCache */; }; C9B22677297F6C2E001F9EFE /* ContentSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B22676297F6C2E001F9EFE /* ContentSettingsView.swift */; }; D08A9C3529956CFA00204A4A /* SwipeActionsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08A9C3429956CFA00204A4A /* SwipeActionsSettingsView.swift */; }; DA0B24FB2A6876D50045BDD7 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */; }; @@ -265,7 +260,6 @@ 9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */, 9F7D93942980063100EE6B7A /* AppAccount in Frameworks */, 9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */, - 9FFF6784299B7D4400FE700A /* LRUCache in Frameworks */, 9F2A5428296AB683009B2D7C /* Models in Frameworks */, 9FFF6782299B7D3A00FE700A /* Account in Frameworks */, 9F2A5426296AB67E009B2D7C /* KeychainSwift in Frameworks */, @@ -281,12 +275,10 @@ 9FAD85A2297456A400496AB1 /* Env in Frameworks */, 9FAD85A0297456A100496AB1 /* Models in Frameworks */, 9FAD85A4297456A800496AB1 /* DesignSystem in Frameworks */, - 9FF614492B2EDCEC00F7B0E6 /* GiphyUISDK in Frameworks */, 9FAD859E2974569B00496AB1 /* Account in Frameworks */, 9FAD859C2974422700496AB1 /* AppAccount in Frameworks */, 9FAD859A297440CB00496AB1 /* KeychainSwift in Frameworks */, 9FAD85982974405D00496AB1 /* Status in Frameworks */, - 065FA20A298675BA00012EA0 /* LRUCache in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -297,7 +289,6 @@ 9F7335EF29674F7100AFF0BA /* QuickLook.framework in Frameworks */, 9F7335ED2967463400AFF0BA /* AVKit.framework in Frameworks */, 9F2A540C29699705009B2D7C /* RevenueCat in Frameworks */, - 065FA1FE29866CD600012EA0 /* LRUCache in Frameworks */, 9F2A540E2969A0B0009B2D7C /* StoreKit.framework in Frameworks */, 9F55C6902955993C00F94077 /* Explore in Frameworks */, 9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */, @@ -309,7 +300,6 @@ 9F398AAB2935FFDB00A889F2 /* Models in Frameworks */, 9F5E581929545BE700A53960 /* Env in Frameworks */, 9F2A540A29699705009B2D7C /* ReceiptParser in Frameworks */, - 9FF614472B2EDCE500F7B0E6 /* GiphyUISDK in Frameworks */, 9F35DB44294F9A7D00B3281A /* Status in Frameworks */, DA0B24FB2A6876D50045BDD7 /* SFSafeSymbols in Frameworks */, 9F295540292B6C3400E0E81B /* Timeline in Frameworks */, @@ -578,7 +568,6 @@ 9FFF677D299B7D2800FE700A /* Status */, 9FFF677F299B7D2B00FE700A /* DesignSystem */, 9FFF6781299B7D3A00FE700A /* Account */, - 9FFF6783299B7D4400FE700A /* LRUCache */, ); productName = IceCubesNotifications; productReference = 9F2A5416296AB631009B2D7C /* IceCubesNotifications.appex */; @@ -605,8 +594,6 @@ 9FAD859F297456A100496AB1 /* Models */, 9FAD85A1297456A400496AB1 /* Env */, 9FAD85A3297456A800496AB1 /* DesignSystem */, - 065FA209298675BA00012EA0 /* LRUCache */, - 9FF614482B2EDCEC00F7B0E6 /* GiphyUISDK */, ); productName = IceCubesShareExtension; productReference = 9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */; @@ -644,9 +631,7 @@ 9F2A540929699705009B2D7C /* ReceiptParser */, 9F2A540B29699705009B2D7C /* RevenueCat */, 9FE3DB56296FEFCA00628CB0 /* AppAccount */, - 065FA1FD29866CD600012EA0 /* LRUCache */, DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */, - 9FF614462B2EDCE500F7B0E6 /* GiphyUISDK */, ); productName = IceCubesApp; productReference = 9FBFE639292A715500C250E9 /* Ice Cubes.app */; @@ -727,9 +712,7 @@ packageReferences = ( 9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */, 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */, - 065FA1FC29866CD600012EA0 /* XCRemoteSwiftPackageReference "LRUCache" */, DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, - 9FF614452B2EDCE500F7B0E6 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */, ); productRefGroup = 9FBFE63A292A715500C250E9 /* Products */; projectDirPath = ""; @@ -1410,14 +1393,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 065FA1FC29866CD600012EA0 /* XCRemoteSwiftPackageReference "LRUCache" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/nicklockwood/LRUCache"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/RevenueCat/purchases-ios.git"; @@ -1434,14 +1409,6 @@ kind = branch; }; }; - 9FF614452B2EDCE500F7B0E6 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Giphy/giphy-ios-sdk"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.2.7; - }; - }; DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols"; @@ -1453,16 +1420,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 065FA1FD29866CD600012EA0 /* LRUCache */ = { - isa = XCSwiftPackageProductDependency; - package = 065FA1FC29866CD600012EA0 /* XCRemoteSwiftPackageReference "LRUCache" */; - productName = LRUCache; - }; - 065FA209298675BA00012EA0 /* LRUCache */ = { - isa = XCSwiftPackageProductDependency; - package = 065FA1FC29866CD600012EA0 /* XCRemoteSwiftPackageReference "LRUCache" */; - productName = LRUCache; - }; 9F29553F292B6C3400E0E81B /* Timeline */ = { isa = XCSwiftPackageProductDependency; productName = Timeline; @@ -1568,16 +1525,6 @@ isa = XCSwiftPackageProductDependency; productName = AppAccount; }; - 9FF614462B2EDCE500F7B0E6 /* GiphyUISDK */ = { - isa = XCSwiftPackageProductDependency; - package = 9FF614452B2EDCE500F7B0E6 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */; - productName = GiphyUISDK; - }; - 9FF614482B2EDCEC00F7B0E6 /* GiphyUISDK */ = { - isa = XCSwiftPackageProductDependency; - package = 9FF614452B2EDCE500F7B0E6 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */; - productName = GiphyUISDK; - }; 9FFF677B299B7B2C00FE700A /* Notifications */ = { isa = XCSwiftPackageProductDependency; productName = Notifications; @@ -1594,11 +1541,6 @@ isa = XCSwiftPackageProductDependency; productName = Account; }; - 9FFF6783299B7D4400FE700A /* LRUCache */ = { - isa = XCSwiftPackageProductDependency; - package = 065FA1FC29866CD600012EA0 /* XCRemoteSwiftPackageReference "LRUCache" */; - productName = LRUCache; - }; DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */ = { isa = XCSwiftPackageProductDependency; package = DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index f019b58c..cddcd4d5 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -30558,6 +30558,7 @@ } }, "iPhone" : { + "extractionState" : "stale", "localizations" : { "be" : { "stringUnit" : { @@ -73626,4 +73627,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Packages/Status/Package.swift b/Packages/Status/Package.swift index 8d8fe915..3f09906c 100644 --- a/Packages/Status/Package.swift +++ b/Packages/Status/Package.swift @@ -23,6 +23,8 @@ let package = Package( .package(name: "Network", path: "../Network"), .package(name: "Env", path: "../Env"), .package(name: "DesignSystem", path: "../DesignSystem"), + .package(url: "https://github.com/Giphy/giphy-ios-sdk", from: "2.2.7"), + .package(url: "https://github.com/nicklockwood/LRUCache", from: "1.0.4"), ], targets: [ .target( @@ -34,6 +36,8 @@ let package = Package( .product(name: "Network", package: "Network"), .product(name: "Env", package: "Env"), .product(name: "DesignSystem", package: "DesignSystem"), + .product(name: "GiphyUISDK", package: "giphy-ios-sdk"), + .product(name: "LRUCache", package: "LRUCache"), ], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency"), diff --git a/Packages/Timeline/Sources/Timeline/TimelineDatasource.swift b/Packages/Timeline/Sources/Timeline/TimelineDatasource.swift index 5b37e8cc..8c28899c 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineDatasource.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineDatasource.swift @@ -11,6 +11,10 @@ actor TimelineDatasource { func get() -> [Status] { statuses.filter { $0.filtered?.first?.filter.filterAction != .hide } } + + func count() -> Int { + statuses.count + } func reset() { statuses = [] diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index 85f75aa5..3b8bf5c6 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -122,8 +122,10 @@ public struct TimelineView: View { SoundEffectManager.shared.playSound(.refresh) } .onChange(of: watcher.latestEvent?.id) { - if let latestEvent = watcher.latestEvent { - viewModel.handleEvent(event: latestEvent, currentAccount: account) + Task { + if let latestEvent = watcher.latestEvent { + await viewModel.handleEvent(event: latestEvent) + } } } .onChange(of: timeline) { _, newValue in diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index 8481a381..fc15e436 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -54,15 +54,11 @@ import SwiftUI var tag: Tag? // Internal source of truth for a timeline. - private var datasource = TimelineDatasource() + private(set) var datasource = TimelineDatasource() private let cache = TimelineCache() private var visibileStatusesIds = Set() private var canStreamEvents: Bool = true - private var accountId: String? { - CurrentAccount.shared.account?.id - } - var client: Client? { didSet { if oldValue != client { @@ -108,35 +104,33 @@ import SwiftUI await datasource.reset() } - func handleEvent(event: any StreamEvent, currentAccount _: CurrentAccount) { - Task { - if let event = event as? StreamEventUpdate, - timeline == .home, - canStreamEvents, - isTimelineVisible, - await !datasource.contains(statusId: event.status.id) - { - pendingStatusesObserver.pendingStatuses.insert(event.status.id, at: 0) - let newStatus = event.status - await datasource.insert(newStatus, at: 0) + func handleEvent(event: any StreamEvent) async { + if let event = event as? StreamEventUpdate, + timeline == .home, + canStreamEvents, + isTimelineVisible, + await !datasource.contains(statusId: event.status.id) + { + pendingStatusesObserver.pendingStatuses.insert(event.status.id, at: 0) + let newStatus = event.status + await datasource.insert(newStatus, at: 0) + await cacheHome() + let statuses = await datasource.get() + withAnimation { + statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) + } + } else if let event = event as? StreamEventDelete { + await datasource.remove(event.status) + await cacheHome() + let statuses = await datasource.get() + withAnimation { + statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) + } + } else if let event = event as? StreamEventStatusUpdate { + if let originalIndex = await datasource.indexOf(statusId: event.status.id) { + await datasource.replace(event.status, at: originalIndex) await cacheHome() - let statuses = await datasource.get() - withAnimation { - statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) - } - } else if let event = event as? StreamEventDelete { - await datasource.remove(event.status) - await cacheHome() - let statuses = await datasource.get() - withAnimation { - statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) - } - } else if let event = event as? StreamEventStatusUpdate { - if let originalIndex = await datasource.indexOf(statusId: event.status.id) { - await datasource.replace(event.status, at: originalIndex) - await cacheHome() - statusesState = await .display(statuses: datasource.get(), nextPageState: .hasNextPage) - } + statusesState = await .display(statuses: datasource.get(), nextPageState: .hasNextPage) } } } diff --git a/Packages/Timeline/Tests/TimelineTests/TimelineTests.swift b/Packages/Timeline/Tests/TimelineTests/TimelineTests.swift deleted file mode 100644 index cf9f504d..00000000 --- a/Packages/Timeline/Tests/TimelineTests/TimelineTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -@testable import Timeline -import XCTest - -final class TimelineTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(Timeline().text, "Hello, World!") - } -} diff --git a/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift b/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift new file mode 100644 index 00000000..7d869ab4 --- /dev/null +++ b/Packages/Timeline/Tests/TimelineTests/TimelineViewModelTests.swift @@ -0,0 +1,24 @@ +@testable import Timeline +import XCTest +import Network +import Models + +@MainActor +final class TimelineViewModelTests: XCTestCase { + func testStreamEventInsertNewStatus() async throws { + let subject = TimelineViewModel() + let client = Client(server: "localhost") + subject.client = client + subject.timeline = .home + subject.isTimelineVisible = true + + let isEmpty = await subject.datasource.isEmpty + XCTAssertTrue(isEmpty) + await subject.datasource.append(.placeholder()) + var count = await subject.datasource.count() + XCTAssertTrue(count == 1) + await subject.handleEvent(event: StreamEventUpdate(status: .placeholder())) + count = await subject.datasource.count() + XCTAssertTrue(count == 2) + } +}