-
Notifications
You must be signed in to change notification settings - Fork 118
[Woo POS] Ensure auto-draft order removal on exit POS #15975
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
[Woo POS] Ensure auto-draft order removal on exit POS #15975
Conversation
|
Version |
Version |
Version |
…al-on-exit-POS # Conflicts: # WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift
|
||
func waitForClearOrder() async { | ||
while !clearOrderWasCalled { | ||
try? await Task.sleep(nanoseconds: 1_000_000) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A question/feedback about this one: I've always though that using Task.sleep would be considered not the best practice, however we seem to use it scarcely in this test suite as well as other POS tests to handle testing async operations in mocks.
My first approach was just to add the waiting time to the tests itself, but updating the mock instead seemed cleaner, since the SUT is the aggregate model, and not the order controller.
I've also tried to use the confirmation API, but I got nowhere, so to avoid too many additional changes I decided that should be fine to add a small waiting time to account for the clearOrder func being now async.
Let me know if you have any thoughts about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, any sort of timeout in test can cause flakiness and (by design) makes tests slower. I would encourage us to find other ways to test without them if possible.
I will check if I have any ideas for this case!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it can be tested without timeouts, but I agree it can be tricky with Swift Testing when mixing synchronous and asynchronous code.
First, we want to know when clearOrder was called. Since clearOrder is async, we need some mechanism to let us know when something happens asynchronously. The easiest one to use in mocks is a callback.
Second, we need to wait for tests until our async condition is met. You're right, we cannot use the confirmation API. I jumped on it many times. It can only be used if the method we're testing is async. In our case, neither sut.startNewCart()
nor sut.pointOfSaleClosed()
are async. You were very close to the solution based on the code, in these cases we need to use withCheckedContinuation()
.
Therefore, in our mocks we should have:
var clearOrderWasCalled: Bool = false
var onClearOrderCalled: () -> Void = { }
func clearOrder() async {
clearOrderWasCalled = true
onClearOrderCalled()
}
in our tests we need to have:
sut.addToCart(makePurchasableItem())
await withCheckedContinuation { continuation in
orderController.onClearOrderCalled = {
continuation.resume()
}
// When
sut.startNewCart()
}
// Then
#expect(orderController.clearOrderWasCalled == true)
That's it! Here's the commit: https://github.com/woocommerce/woocommerce-ios/compare/task/WOOMOB-88-ensure-autodraft-order-removal-on-exit-POS...task/WOOMOB-88-ensure-autodraft-order-removal-on-exit-POS-with-async-tests?expand=1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's very helpful, appreciated 🙇 . It's definitely tricky to wrap my head around about the flow here: enters the continuation block -> set the callback -> start new cart (which spawns async task) -> suspend continuation -> execute clear order -> trigger callback -> resume the continuation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for working on it.
There's one more case to handle, a few places to clean up (I think adaptors can and should be avoided), timeouts to remove.
I was also wondering if we could leave this behavior within the payment code. E.g. cleaning the order if payment was canceled. Not sure if it's the option but if that would be possible, I think that would be the cleanest solution hiding any complexity from the client of the payment code (POS).
import struct Combine.AnyPublisher | ||
import struct NetworkingCore.JetpackSite | ||
|
||
public protocol POSOrderManagementServiceProtocol { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use POSOrderService
instead? I found it a bit counter-intuitive to encounter a different kind of order service or is there a good reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point. I don't have a good reason to keep it separate. It also makes more more sense to keep the order deletion action in the POSOrderService
along with the other order-related functions. Updated on 040be92
removeAllItemsFromCart() | ||
orderController.clearOrder() | ||
Task { | ||
await orderController.clearOrder() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The draft order is created in local storage <...> as a side effect of fetching the order remotely and then persisting it to storage temporarily in the background, when we're processing a card payment.
Since the order is put into storage as a site effect when processing a card payment, could it also be cleared at the same level? My concern is that we may be solving the issue at the wrong abstraction level. While some code outside the POS puts the order into storage, the POS itself clears the storage. If something changes in the outside code, we may continue to delete the order without actually needing to do it.
What are your thoughts on that, would that be doable?
} | ||
|
||
func clearOrder() { | ||
func clearOrder() async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also clear the order when coming back to the cart, because we create a new one every time we check out.
Simulator.Screen.Recording.-.iPad.Air.11-inch.M3.-.2025-10-01.at.18.36.00.mov
} | ||
} | ||
|
||
private struct POSOrderManagementServiceAdaptor: POSOrderManagementServiceProtocol { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code can be removed, no need to use the adaptors or store dispatching. This is only required for dependencies that cannot be injected in any other way, and we prefer to use them through the Environment
.
For our case:
POSOrderManagementService
relies onOrderStoreMethodsProtocol
POSOrderManagementService
can be created withinPOSTabCoordinator
(likePointOfSaleCouponService
), injecting methodsPOSOrderManagementService
can be passed to entry point view, which can then be passed toPointOfSaleOrderController
.
|
||
func waitForClearOrder() async { | ||
while !clearOrderWasCalled { | ||
try? await Task.sleep(nanoseconds: 1_000_000) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it can be tested without timeouts, but I agree it can be tricky with Swift Testing when mixing synchronous and asynchronous code.
First, we want to know when clearOrder was called. Since clearOrder is async, we need some mechanism to let us know when something happens asynchronously. The easiest one to use in mocks is a callback.
Second, we need to wait for tests until our async condition is met. You're right, we cannot use the confirmation API. I jumped on it many times. It can only be used if the method we're testing is async. In our case, neither sut.startNewCart()
nor sut.pointOfSaleClosed()
are async. You were very close to the solution based on the code, in these cases we need to use withCheckedContinuation()
.
Therefore, in our mocks we should have:
var clearOrderWasCalled: Bool = false
var onClearOrderCalled: () -> Void = { }
func clearOrder() async {
clearOrderWasCalled = true
onClearOrderCalled()
}
in our tests we need to have:
sut.addToCart(makePurchasableItem())
await withCheckedContinuation { continuation in
orderController.onClearOrderCalled = {
continuation.resume()
}
// When
sut.startNewCart()
}
// Then
#expect(orderController.clearOrderWasCalled == true)
That's it! Here's the commit: https://github.com/woocommerce/woocommerce-ios/compare/task/WOOMOB-88-ensure-autodraft-order-removal-on-exit-POS...task/WOOMOB-88-ensure-autodraft-order-removal-on-exit-POS-with-async-tests?expand=1
removeAllItemsFromCart() | ||
orderController.clearOrder() | ||
Task { | ||
await orderController.clearOrder() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check if we don't need to capture orderController weakly here. It likely doesn't cause memory leaks but we can check just to be safe. I remember it did for some other calls that didn't complete.
Version |
Closes WOOMOB-88
Description
This PR addresses
.autodraft
orders not being deleted from storage when we exit POS after checkout, but before completing a payment.The draft order is created in local storage when we perform
POSOrderService.syncOrder()
as a side effect of fetching the order remotely and then persisting it to storage temporarily in the background, when we're processing a card payment.This should be cleared after collecting a payment and starting a new order, however it will remain visible if we just exit POS without clearing it explicitly (we only clear the in-memory order, not the autodraft created in local storage), which will remain until we pull to refresh in the order list.
Changes
.autoDraft
statusOrderStoreMethods
andOrderStoreMethodsProtocol
for shared functionality around dispatching order actions.POSOrderManagementService
service to handle the order actions, abstracted from the app action dispatcherSteps to reproduce / Testing information
autodraft.orders.autoremoved.mp4
RELEASE-NOTES.txt
if necessary.