Skip to content

Commit 0905082

Browse files
committed
Improve management of syncs to fix flaky test
1 parent c10fb0a commit 0905082

File tree

2 files changed

+51
-25
lines changed

2 files changed

+51
-25
lines changed

Modules/Tests/PointOfSaleTests/Controllers/PointOfSaleOrderControllerTests.swift

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,52 +53,57 @@ struct PointOfSaleOrderControllerTests {
5353
#expect(mockOrderService.syncOrderWasCalled == false)
5454
}
5555

56-
@Test func syncOrder_when_already_syncing_doesnt_call_orderService() async throws {
56+
@Test @MainActor func syncOrder_when_already_syncing_doesnt_call_orderService() async throws {
5757
// Given
5858
let sut = PointOfSaleOrderController(orderService: mockOrderService,
5959
receiptSender: mockReceiptSender,
6060
currencySettingsProvider: MockCurrencySettingsProvider(),
6161
analytics: MockPOSAnalytics())
62-
mockOrderService.simulateSyncing = true
6362

64-
// Use a continuation to wait until the first sync has definitely started
63+
// Block the sync so it doesn't complete until we manually resume it
64+
mockOrderService.blockNextSync()
65+
66+
// Start the first sync in a detached task so it runs concurrently
67+
let firstSyncTask = Task.detached {
68+
await sut.syncOrder(for: Cart(purchasableItems: [makeItem(quantity: 1)]), retryHandler: {})
69+
}
70+
71+
// Wait for the order state to actually become syncing
6572
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
66-
Task { @MainActor in
67-
// Start observing state changes
68-
@Sendable func observeOrderState() {
69-
withObservationTracking {
70-
_ = sut.orderState
71-
} onChange: {
72-
Task { @MainActor in
73-
if sut.orderState.isSyncing {
74-
// State is now syncing, resume the test
75-
continuation.resume()
76-
} else {
77-
// Keep observing
78-
observeOrderState()
79-
}
73+
@Sendable func observeOrderState() {
74+
withObservationTracking {
75+
_ = sut.orderState
76+
} onChange: {
77+
Task { @MainActor in
78+
if sut.orderState.isSyncing {
79+
continuation.resume()
80+
} else {
81+
observeOrderState()
8082
}
8183
}
8284
}
83-
observeOrderState()
84-
85-
// Start the first sync in a background task
86-
Task {
87-
await sut.syncOrder(for: Cart(purchasableItems: [makeItem(quantity: 1)]), retryHandler: {})
88-
}
8985
}
86+
observeOrderState()
9087
}
9188

89+
// Verify the state is actually syncing
90+
#expect(sut.orderState.isSyncing == true)
91+
#expect(mockOrderService.syncOrderWasCalled == true)
92+
9293
// Reset the flag after confirming the sync has started
9394
mockOrderService.syncOrderWasCalled = false
9495

95-
// When - try to sync while already syncing
96+
// When - try to sync while the first sync is still in progress
9697
await sut.syncOrder(for: Cart(purchasableItems: [makeItem(quantity: 2),
9798
makeItem(quantity: 5)]),
9899
retryHandler: {})
99100

100101
// Then - the second sync should have been skipped
101102
#expect(mockOrderService.syncOrderWasCalled == false)
103+
104+
// Cleanup - allow the first sync to complete
105+
mockOrderService.resumeBlockedSync()
106+
_ = await firstSyncTask.result
102107
}
103108

104109
@Test func syncOrder_with_no_previous_order_calls_orderService() async throws {

Modules/Tests/PointOfSaleTests/Mocks/MockPOSOrderService.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,33 @@ class MockPOSOrderService: POSOrderServiceProtocol {
1414
var spySyncOrderCurrency: CurrencyCode?
1515
var spyCashPaymentChangeDueAmount: String?
1616

17+
// For controlling sync timing in tests
18+
private var syncContinuation: CheckedContinuation<Void, Never>?
19+
private var shouldBlockSync = false
20+
21+
/// Blocks the next sync operation until `resumeBlockedSync()` is called
22+
func blockNextSync() {
23+
shouldBlockSync = true
24+
}
25+
26+
/// Resumes a blocked sync operation
27+
func resumeBlockedSync() {
28+
syncContinuation?.resume()
29+
syncContinuation = nil
30+
shouldBlockSync = false
31+
}
32+
1733
func syncOrder(cart: Yosemite.POSCart,
1834
currency: CurrencyCode) async throws -> Yosemite.Order {
1935
syncOrderWasCalled = true
2036
spySyncOrderCurrency = currency
2137

22-
if simulateSyncing {
38+
if shouldBlockSync {
39+
shouldBlockSync = false // Only block the first call
40+
await withCheckedContinuation { continuation in
41+
syncContinuation = continuation
42+
}
43+
} else if simulateSyncing {
2344
try await Task.sleep(nanoseconds: UInt64(1 * Double(NSEC_PER_SEC)))
2445
}
2546

0 commit comments

Comments
 (0)