@@ -6,6 +6,7 @@ class FeaturesViewModelExtendedTests: XCTestCase {
66 // MARK: - Delegate capture
77
88 private class Capture : FeaturesFlowDelegate {
9+
910 var successCount = 0
1011 var failCount = 0
1112 var savedGroupsCount = 0
@@ -27,6 +28,16 @@ class FeaturesViewModelExtendedTests: XCTestCase {
2728 }
2829 func savedGroupsFetchFailed( error: SDKError , isRemote: Bool ) { failCount += 1 }
2930 func featuresAPIModelSuccessfully( model: FeaturesDataModel ) { }
31+
32+ // featuresUpdateIsComplete
33+
34+ var featuresUpdateIsCompleteCallCount = 0
35+ var featuresUpdateIsCompleteArguments : [ ( error: GrowthBook . SDKError ? , isRemote: Bool ) ] = [ ]
36+
37+ func featuresUpdateIsComplete( error: GrowthBook . SDKError ? , isRemote: Bool ) {
38+ featuresUpdateIsCompleteArguments += [ ( error, isRemote) ]
39+ featuresUpdateIsCompleteCallCount += 1
40+ }
3041 }
3142
3243 private func makeVM(
@@ -60,6 +71,7 @@ class FeaturesViewModelExtendedTests: XCTestCase {
6071 // With preloadedFeatures, the VM skips the cache → no success or fail from cache
6172 XCTAssertEqual ( capture. successCount, 0 )
6273 XCTAssertEqual ( capture. failCount, 0 )
74+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 0 )
6375 }
6476
6577 func testNoPreloadedFeaturesReadsCache( ) {
@@ -68,6 +80,7 @@ class FeaturesViewModelExtendedTests: XCTestCase {
6880 _ = makeVM ( delegate: capture)
6981 XCTAssertEqual ( capture. failCount, 1 )
7082 XCTAssertEqual ( capture. lastError, . failedToLoadData)
83+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 0 )
7184 }
7285
7386 // MARK: - fetchFeatures with nil apiUrl skips network
@@ -80,6 +93,9 @@ class FeaturesViewModelExtendedTests: XCTestCase {
8093 // No network call → no additional success
8194 XCTAssertEqual ( capture. successCount, 0 )
8295 XCTAssertEqual ( capture. failCount, beforeFail + 1 ) // one more fail from cache read in fetchFeatures
96+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 ) // fetchFeatures
97+ XCTAssertEqual ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error, . invalidAPIURL)
98+ XCTAssertEqual ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote, false )
8399 }
84100
85101 // MARK: - remoteEval path
@@ -90,13 +106,19 @@ class FeaturesViewModelExtendedTests: XCTestCase {
90106 vm. fetchFeatures ( apiUrl: " https://example.com " , remoteEval: true )
91107 // Remote eval success should trigger featuresFetchedSuccessfully
92108 XCTAssertGreaterThan ( capture. successCount, 0 )
109+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 )
110+ XCTAssertNil ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error)
111+ XCTAssertTrue ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote)
93112 }
94113
95114 func testFetchFeaturesRemoteEvalFailure( ) {
96115 let capture = Capture ( )
97116 let vm = makeVM ( error: SDKError . failedToFetchData, delegate: capture, ttlSeconds: 0 )
98117 vm. fetchFeatures ( apiUrl: " https://example.com " , remoteEval: true )
99118 XCTAssertGreaterThan ( capture. failCount, 0 )
119+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 )
120+ XCTAssertEqual ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error? . code, . failedToLoadData)
121+ XCTAssertTrue ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote)
100122 }
101123
102124 // MARK: - prepareFeaturesData: encryptedFeatures without key
@@ -111,6 +133,9 @@ class FeaturesViewModelExtendedTests: XCTestCase {
111133 vm. prepareFeaturesData ( data: payload)
112134 XCTAssertGreaterThan ( capture. failCount, 0 )
113135 XCTAssertEqual ( capture. lastError, . failedMissingKey)
136+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 )
137+ XCTAssertEqual ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error? . code, . failedMissingKey)
138+ XCTAssertTrue ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote)
114139 }
115140
116141 func testPrepareFeaturesDataEncryptedFeaturesWithWrongKeyFails( ) {
@@ -123,6 +148,9 @@ class FeaturesViewModelExtendedTests: XCTestCase {
123148 vm. prepareFeaturesData ( data: payload)
124149 XCTAssertGreaterThan ( capture. failCount, 0 )
125150 XCTAssertEqual ( capture. lastError, . failedEncryptedFeatures)
151+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 )
152+ XCTAssertEqual ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error? . code, . failedEncryptedFeatures)
153+ XCTAssertTrue ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote)
126154 }
127155
128156 // MARK: - prepareFeaturesData: no features and no encryptedFeatures
@@ -136,6 +164,9 @@ class FeaturesViewModelExtendedTests: XCTestCase {
136164 vm. prepareFeaturesData ( data: payload)
137165 XCTAssertGreaterThan ( capture. failCount, 0 )
138166 XCTAssertEqual ( capture. lastError, . failedMissingKey)
167+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 )
168+ XCTAssertEqual ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error? . code, . failedMissingKey)
169+ XCTAssertTrue ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote)
139170 }
140171
141172 // MARK: - prepareFeaturesData: plain features success
@@ -149,6 +180,9 @@ class FeaturesViewModelExtendedTests: XCTestCase {
149180 vm. prepareFeaturesData ( data: payload)
150181 XCTAssertEqual ( capture. successCount, 1 )
151182 XCTAssertNotNil ( capture. lastFeatures ? [ " my-flag " ] )
183+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 )
184+ XCTAssertNil ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error)
185+ XCTAssertTrue ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote)
152186 }
153187
154188 // MARK: - prepareFeaturesData: invalid JSON fails
@@ -159,6 +193,9 @@ class FeaturesViewModelExtendedTests: XCTestCase {
159193 vm. prepareFeaturesData ( data: " not json " . data ( using: . utf8) !)
160194 XCTAssertGreaterThan ( capture. failCount, 0 )
161195 XCTAssertEqual ( capture. lastError, . failedParsedData)
196+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 )
197+ XCTAssertEqual ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error? . code, . failedParsedData)
198+ XCTAssertTrue ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote)
162199 }
163200
164201 // MARK: - prepareFeaturesData: savedGroups in response
@@ -172,6 +209,9 @@ class FeaturesViewModelExtendedTests: XCTestCase {
172209 vm. prepareFeaturesData ( data: payload)
173210 XCTAssertEqual ( capture. savedGroupsCount, 1 )
174211 XCTAssertNotNil ( capture. lastSavedGroups)
212+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 )
213+ XCTAssertNil ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error)
214+ XCTAssertTrue ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote)
175215 }
176216
177217 // MARK: - fetchFeatures network failure falls back to cache
@@ -207,5 +247,32 @@ class FeaturesViewModelExtendedTests: XCTestCase {
207247 failVM. fetchFeatures ( apiUrl: " https://example.com " )
208248 // After network fail, falls back to cache → at least one success
209249 XCTAssertGreaterThan ( capture. successCount, 0 )
250+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 )
251+ XCTAssertEqual ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error? . code, . failedToFetchData)
252+ XCTAssertTrue ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote)
253+ }
254+
255+ // MARK: - fetchFeatures is not stale reports featuresAreUpToDate to the delegate
256+
257+ func testFetchFeaturesReportIfNotStale( ) {
258+ // GIVEN
259+ let capture = Capture ( )
260+ let vm = makeVM ( response: MockResponse ( ) . successResponse, delegate: capture, ttlSeconds: 100 )
261+ // first successful fetch
262+ vm. fetchFeatures ( apiUrl: " https://example.com " , remoteEval: true )
263+ XCTAssertGreaterThan ( capture. successCount, 0 )
264+ // no featuresAreUpToDate callse
265+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 1 )
266+ XCTAssertNil ( capture. featuresUpdateIsCompleteArguments [ 0 ] . error)
267+ XCTAssertTrue ( capture. featuresUpdateIsCompleteArguments [ 0 ] . isRemote)
268+
269+ // WHEN
270+ // trying to fetch while cache is not expired
271+ vm. fetchFeatures ( apiUrl: " https://example.com " , remoteEval: true )
272+
273+ // THEN
274+ XCTAssertEqual ( capture. featuresUpdateIsCompleteCallCount, 2 , " Expected to call featuresAreUpToDate delegate method " )
275+ XCTAssertEqual ( capture. featuresUpdateIsCompleteArguments [ 1 ] . error, nil )
276+ XCTAssertEqual ( capture. featuresUpdateIsCompleteArguments [ 1 ] . isRemote, true )
210277 }
211278}
0 commit comments