Skip to content

Commit 2dd0372

Browse files
committed
Remove test flakiness by mocking stores
1 parent 3be4a8b commit 2dd0372

File tree

2 files changed

+58
-34
lines changed

2 files changed

+58
-34
lines changed

WooCommerce/Classes/Tools/BackgroundTasks/ForegroundPOSCatalogSyncDispatcher.swift

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ final actor ForegroundPOSCatalogSyncDispatcher {
1818
private let notificationCenter: NotificationCenter
1919
private let timerProvider: DispatchTimerProviding
2020
private let featureFlagService: FeatureFlagService
21-
private let stores: StoresManager
21+
private let storeProvider: POSCatalogStoreProviding
2222
private let isAppActive: () async -> Bool
2323
private var observers: [NSObjectProtocol] = []
2424
private var timer: DispatchTimerProtocol?
@@ -32,13 +32,13 @@ final actor ForegroundPOSCatalogSyncDispatcher {
3232
notificationCenter: NotificationCenter = .default,
3333
timerProvider: DispatchTimerProviding = DefaultDispatchTimerProvider(),
3434
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
35-
stores: StoresManager = ServiceLocator.stores,
35+
storeProvider: POSCatalogStoreProviding = DefaultPOSCatalogStoreProvider(),
3636
isAppActive: @escaping () async -> Bool = UIApplication.isApplicationActive) {
3737
self.interval = interval
3838
self.notificationCenter = notificationCenter
3939
self.timerProvider = timerProvider
4040
self.featureFlagService = featureFlagService
41-
self.stores = stores
41+
self.storeProvider = storeProvider
4242
self.isAppActive = isAppActive
4343
}
4444

@@ -47,14 +47,14 @@ final actor ForegroundPOSCatalogSyncDispatcher {
4747
return
4848
}
4949

50-
if syncSiteID != stores.sessionManager.defaultStoreID {
50+
if syncSiteID != nil, syncSiteID != storeProvider.defaultStoreID {
5151
DDLogInfo("🔄 ForegroundPOSCatalogSyncDispatcher: Site has changed, resetting the sync")
5252
stop()
5353
}
5454

5555
guard !isRunning else { return }
5656

57-
DDLogInfo("🔄 ForegroundPOSCatalogSyncDispatcher: Starting foreground sync dispatcher for site \(stores.sessionManager.defaultStoreID ?? 0)")
57+
DDLogInfo("🔄 ForegroundPOSCatalogSyncDispatcher: Starting foreground sync dispatcher for site \(storeProvider.defaultStoreID ?? 0)")
5858

5959
let activeObserver = notificationCenter.addObserver(
6060
forName: UIApplication.didBecomeActiveNotification,
@@ -92,7 +92,7 @@ final actor ForegroundPOSCatalogSyncDispatcher {
9292
startTimer()
9393
}
9494

95-
syncSiteID = stores.sessionManager.defaultStoreID
95+
syncSiteID = storeProvider.defaultStoreID
9696
}
9797

9898
func stop() {
@@ -136,13 +136,13 @@ final actor ForegroundPOSCatalogSyncDispatcher {
136136
return
137137
}
138138

139-
guard let siteID = stores.sessionManager.defaultStoreID else {
139+
guard let siteID = storeProvider.defaultStoreID else {
140140
DDLogInfo("📋 ForegroundPOSCatalogSyncDispatcher: No default store, skipping sync")
141141
stop()
142142
return
143143
}
144144

145-
guard let coordinator = stores.posCatalogSyncCoordinator else {
145+
guard let coordinator = storeProvider.posCatalogSyncCoordinator else {
146146
DDLogInfo("📋 ForegroundPOSCatalogSyncDispatcher: Coordinator unavailable, skipping sync")
147147
stop()
148148
return
@@ -221,3 +221,26 @@ extension UIApplication {
221221
shared.applicationState == .active
222222
}
223223
}
224+
225+
/// Protocol for accessing store ID and POS catalog sync coordinator.
226+
protocol POSCatalogStoreProviding {
227+
var defaultStoreID: Int64? { get }
228+
var posCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol? { get }
229+
}
230+
231+
/// Default implementation using StoresManager.
232+
struct DefaultPOSCatalogStoreProvider: POSCatalogStoreProviding {
233+
private let stores: StoresManager
234+
235+
init(stores: StoresManager = ServiceLocator.stores) {
236+
self.stores = stores
237+
}
238+
239+
var defaultStoreID: Int64? {
240+
stores.sessionManager.defaultStoreID
241+
}
242+
243+
var posCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol? {
244+
stores.posCatalogSyncCoordinator
245+
}
246+
}

WooCommerce/WooCommerceTests/Tools/ForegroundPOSCatalogSyncDispatcherTests.swift

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,19 @@ struct ForegroundPOSCatalogSyncDispatcherTests {
99
private let timerProvider = MockDispatchTimerProvider()
1010
private let featureFlags = MockFeatureFlagService()
1111
private let notificationCenter = NotificationCenter()
12-
private let sessionManager: SessionManager
13-
private let stores: MockStoresManager
12+
private let storeProvider = MockPOSCatalogStoreProvider()
1413
private let sut: ForegroundPOSCatalogSyncDispatcher
14+
private let coordinator = MockPOSCatalogSyncCoordinator()
1515

1616
init() {
1717
featureFlags.isFeatureFlagEnabledReturnValue[.pointOfSaleLocalCatalogi1] = true
18-
sessionManager = SessionManager.testingInstance
19-
sessionManager.setStoreId(123)
20-
stores = MockStoresManager(sessionManager: sessionManager)
18+
storeProvider.defaultStoreID = 123
19+
storeProvider.posCatalogSyncCoordinator = coordinator
2120
sut = ForegroundPOSCatalogSyncDispatcher(
2221
notificationCenter: notificationCenter,
2322
timerProvider: timerProvider,
2423
featureFlagService: featureFlags,
25-
stores: stores,
24+
storeProvider: storeProvider,
2625
isAppActive: { true }
2726
)
2827
}
@@ -52,9 +51,6 @@ struct ForegroundPOSCatalogSyncDispatcherTests {
5251
@Test
5352
func timerFires_whenRequirementsMet_triggersSync() async throws {
5453
// Given
55-
let coordinator = MockPOSCatalogSyncCoordinator()
56-
stores.testPOSCatalogSyncCoordinator = coordinator
57-
5854
await sut.start()
5955
let timer = try #require(timerProvider.createdTimers.first)
6056

@@ -74,9 +70,7 @@ struct ForegroundPOSCatalogSyncDispatcherTests {
7470
@Test
7571
func timerFires_whenNoDefaultStore_skipsSync() async throws {
7672
// Given
77-
let coordinator = MockPOSCatalogSyncCoordinator()
78-
sessionManager.setStoreId(nil)
79-
stores.testPOSCatalogSyncCoordinator = coordinator
73+
storeProvider.defaultStoreID = nil
8074

8175
await sut.start()
8276
let timer = try #require(timerProvider.createdTimers.first)
@@ -91,9 +85,7 @@ struct ForegroundPOSCatalogSyncDispatcherTests {
9185
@Test
9286
func coordinatorError_whenSyncAlreadyInProgress_logsButDoesNotCrash() async throws {
9387
// Given
94-
let coordinator = MockPOSCatalogSyncCoordinator()
9588
coordinator.performSmartSyncResult = .failure(POSCatalogSyncError.syncAlreadyInProgress(siteID: 123))
96-
stores.testPOSCatalogSyncCoordinator = coordinator
9789

9890
await sut.start()
9991
let timer = try #require(timerProvider.createdTimers.first)
@@ -118,7 +110,7 @@ struct ForegroundPOSCatalogSyncDispatcherTests {
118110
#expect(timerProvider.createdTimers.count == 1)
119111

120112
// When - change site to 456 and start again
121-
sessionManager.setStoreId(456)
113+
storeProvider.defaultStoreID = 456
122114
await sut.start()
123115

124116
// Then - old timer cancelled, new timer created
@@ -127,18 +119,18 @@ struct ForegroundPOSCatalogSyncDispatcherTests {
127119
#expect(timerProvider.createdTimers.last?.isResumed == true)
128120

129121
// Verify sync uses new site ID
130-
let coordinator = MockPOSCatalogSyncCoordinator()
131-
stores.testPOSCatalogSyncCoordinator = coordinator
122+
let newCoordinator = MockPOSCatalogSyncCoordinator()
123+
storeProvider.posCatalogSyncCoordinator = newCoordinator
132124
let newTimer = try #require(timerProvider.createdTimers.last)
133125

134126
await withCheckedContinuation { continuation in
135-
coordinator.onPerformSmartSyncCalled = {
127+
newCoordinator.onPerformSmartSyncCalled = {
136128
continuation.resume()
137129
}
138130
newTimer.fire()
139131
}
140132

141-
#expect(coordinator.performSmartSyncSiteID == 456)
133+
#expect(newCoordinator.performSmartSyncSiteID == 456)
142134
}
143135

144136
@Test
@@ -168,17 +160,17 @@ struct ForegroundPOSCatalogSyncDispatcherTests {
168160
notificationCenter: notificationCenter,
169161
timerProvider: timerProvider,
170162
featureFlagService: featureFlags,
171-
stores: stores,
163+
storeProvider: storeProvider,
172164
isAppActive: { false }
173165
)
174166
await inactiveDispatcher.start()
175167

176168
// Then - no timer created yet
177169
#expect(timerProvider.createdTimers.isEmpty)
178170

179-
// When - app becomes active, wait for timer to be created
171+
// When - app becomes active, wait for timer to be created and resume
180172
await withCheckedContinuation { continuation in
181-
timerProvider.onTimerCreated = { continuation.resume() }
173+
timerProvider.onTimerCreated = { $0.onResume = { continuation.resume() } }
182174

183175
notificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil)
184176
}
@@ -206,9 +198,9 @@ struct ForegroundPOSCatalogSyncDispatcherTests {
206198
// Then - timer should be cancelled
207199
#expect(timer.isCancelled == true)
208200

209-
// When - app becomes active again, wait for new timer
201+
// When - app becomes active again, wait for new timer to be resumed
210202
await withCheckedContinuation { continuation in
211-
timerProvider.onTimerCreated = { continuation.resume() }
203+
timerProvider.onTimerCreated = { $0.onResume = { continuation.resume() } }
212204

213205
notificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil)
214206
}
@@ -225,6 +217,7 @@ private final class MockDispatchTimer: DispatchTimerProtocol {
225217
private(set) var isResumed = false
226218
private(set) var isCancelled = false
227219
var onCancelled: () -> Void = { }
220+
var onResume: () -> Void = { }
228221
private var eventHandler: (() -> Void)?
229222

230223
func schedule(deadline: DispatchTime, repeating: Double, leeway: DispatchTimeInterval) {
@@ -237,6 +230,7 @@ private final class MockDispatchTimer: DispatchTimerProtocol {
237230

238231
func resume() {
239232
isResumed = true
233+
onResume()
240234
}
241235

242236
func cancel() {
@@ -252,16 +246,23 @@ private final class MockDispatchTimer: DispatchTimerProtocol {
252246

253247
private final class MockDispatchTimerProvider: DispatchTimerProviding {
254248
private(set) var createdTimers: [MockDispatchTimer] = []
255-
var onTimerCreated: () -> Void = { }
249+
var onTimerCreated: (MockDispatchTimer) -> Void = { _ in }
256250

257251
func makeTimer(queue: DispatchQueue) -> DispatchTimerProtocol {
258252
let timer = MockDispatchTimer()
259253
createdTimers.append(timer)
260-
onTimerCreated()
254+
onTimerCreated(timer)
261255
return timer
262256
}
263257
}
264258

259+
// MARK: - Mock Store Provider
260+
261+
private final class MockPOSCatalogStoreProvider: POSCatalogStoreProviding {
262+
var defaultStoreID: Int64?
263+
var posCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol?
264+
}
265+
265266
// MARK: - Mock Coordinator
266267

267268
private final class MockPOSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {

0 commit comments

Comments
 (0)