Skip to content
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

[Woo POS] Add test coverage for PointOfSaleDashboardViewModel #13310

Conversation

soundsokay
Copy link
Contributor

@soundsokay soundsokay commented Jul 12, 2024

Expands test coverage for PointOfSaleDashboardViewModel to verify the observePaymentStateForButtonDisabledProperties method correctly sets isAddMoreDisabled accounting for both payment state and order syncing state.

Closes: #13306

Description

The Add More button was disabled in #13270 but tests weren't updated. This PR adds coverage for that.

Steps to reproduce

Testing information

Invert the values returned and used in the assignment to isAddMoreDisabled, run the PointOfSaleDashboardViewModelTests, and observe failures in the newly added tests.

Screenshots

N/A


  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

Verify isAddMoreDisabled is set appropriately for payment state and order syncing state.
@dangermattic
Copy link
Collaborator

dangermattic commented Jul 12, 2024

2 Warnings
⚠️ View files have been modified, but no screenshot or video is included in the pull request. Consider adding some for clarity.
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

@soundsokay soundsokay added category: unit tests Related to unit testing. type: task An internally driven task. labels Jul 12, 2024
@soundsokay soundsokay added this to the 19.6 milestone Jul 12, 2024
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Jul 12, 2024

WooCommerce iOS📲 You can test the changes from this Pull Request in WooCommerce iOS by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS WooCommerce iOS
Build Numberpr13310-7f1cb1a
Version19.6
Bundle IDcom.automattic.alpha.woocommerce
Commit7f1cb1a
App Center BuildWooCommerce - Prototype Builds #10106
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@@ -36,13 +36,12 @@ final class PointOfSaleDashboardViewModel: ObservableObject {
init(itemProvider: POSItemProvider,
cardPresentPaymentService: CardPresentPaymentFacade,
orderService: POSOrderServiceProtocol,
currencyFormatter: CurrencyFormatter) {
currencyFormatter: CurrencyFormatter,
totalsViewModel: TotalsViewModel) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since with this change the totalsViewModel is injected and not created, what was the main reason for it? And will we need to change some other parts of the POS code with this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a first-cut attempt to improve testability of this class.

This injection was removed in commit 35370c7

Copy link
Contributor

@iamgabrielma iamgabrielma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this! I left a couple of comments regarding the decision of injecting the TotalsViewModel, as well as exposing private properties that were only gettable.

Perhaps we can improve the test coverage here by using Mocks/Fakes/Stubs/Spies instead? As an example we do this with the item provider

WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift Outdated Show resolved Hide resolved
Comment on lines 80 to 81
paymentState: PaymentState = .acceptingCard,
isSyncingOrder: Bool = false) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By setting these two properties and its initial values on initialization we're now setting some default initial state, when before that it was stateless. Note how the only dependencies being injected are abstractions like the payments Façade, utils like the currency formatter, or the POS interfaces.

Do we want it to always default to .acceptingCard and not syncing? Would it be preferable to leave the implementation details to each service? Eg: Payment state being set and updated from the CardPresentPaymentService rather than injected in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like these have initial values on trunk?

Copy link
Contributor

@iamgabrielma iamgabrielma Jul 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like these have initial values on trunk?

They are, but it's also internal state to the class. A concern here would be:

Imagine we implement a new XYZ that needs the TotalsViewModel to be in .acceptingCard state. Since the property is exposed, and this happens to be its default value then we just ignore setting the property specifically. Later on we update the implementation details of the class to be .notAcceptingCard instead, which will cause XYZ to fail because it relied on default state that is no longer the default case, and would trigger no errors (unless catch by unit tests!) because the compiler would find no issues with it.

To avoid these, we can either force providing a specific initial state by not providing a default value, so the compiler enforces this, or in our case just not expose the state to be modified from outside the class, and leave the responsibility of it to the TotalsViewModel itself, opening some getters so it can be checked (but not modified) for other objects that need to check it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a a good point.

Addressed in commit bb3d973.

Comment on lines 129 to 131
let totalsViewModel = TotalsViewModel(orderService: POSOrderPreviewService(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
currencyFormatter: .init(currencySettings: .init()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this happens on the #Preview, declaring a new instance of TotalsViewModel in order to inject it into the VM for the CartView seems to indicate that the design of the dependency graph is not quite right. I'd say the CartView should know nothing about the Totals VM and perhaps this shouldn't be injected here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 35370c7.

Comment on lines 127 to 129
let totalsViewModel = TotalsViewModel(orderService: POSOrderPreviewService(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
currencyFormatter: .init(currencySettings: .init()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, up until now the entry point seems to accept only abstractions either from the payment façade, or the POS interfaces, however now it would require to inject a specific POS VM.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 35370c7.

super.tearDown()
}

private func setupViewModelWithExpectationsExpectingAddMoreMatchesSyncingState(paymentState: TotalsViewModel.PaymentState,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being helper methods, these could go into an extension. I think there's also no need in the test target for these to be private. Maybe we can also simplify the function names? Since it would seem are specific only to the AddMoreIsDisabled property, however both payment state and syncing state are exposed. Eg:

setupViewModel(paymentState: TotalsViewModel.PaymentState, isSyncingOrder: Bool)

@soundsokay soundsokay changed the title [DRAFT Woo POS] test: Add test coverage for PointOfSaleDashboardViewModel [Woo POS] Add test coverage for PointOfSaleDashboardViewModel Jul 16, 2024
Fixes test: test_isAddMoreDisabled_is_true_when_order_is_syncing_and_payment_state_is_idle
Adds: test_isAddMoreDisabled_is_true_for_processingPayment.
@soundsokay soundsokay modified the milestones: 19.6, 19.7 Jul 18, 2024
Protocol no longer needed after refactoring to new testing approach using mocked dependencies.
Also restore read-only access control for external entities with lazy initialization for cartViewModel.
- Converted publishers to computed properties leveraging property wrappers for simplified exposure and automatic initialization.
- Introduced a `bindPublishers` method to centralize the assignment of publishers, ensuring all necessary bindings are in one place.
- Addressed feedback by eliminating manual publisher initialization in initializers, reducing duplication and potential errors.
- Enhanced code maintainability and readability by using property wrappers and centralizing publisher assignments.
…ta-to-go-back-to-product-selector-when-order-is-being-synced-add-test

Conflicts:
	WooCommerce/Classes/POS/Presentation/TotalsView.swift
	WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift
- Updated the TotalsViewModel to remove default values for `paymentState` and `isSyncingOrder`, making these parameters mandatory during initialization.
@soundsokay soundsokay requested a review from staskus July 21, 2024 23:23
Copy link
Contributor

@bozidarsevo bozidarsevo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we need AnyTotalsViewModel and can just go with TotalsViewModelProtocol and having TotalsViewModel and MockTotalsViewModel conform to it...

Copy link
Collaborator

@staskus staskus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made another suggestion.

I hate the fact that I'm holding this PR for so long but I want to make sure we don't make things harder for ourselves.

I suggest to use concrete implementation in Views, and protocols in View Models. This way we go around any compiler issues that we had and it also transparently exposes our dependency tree that we can refactor in the future.

@soundsokay
Copy link
Contributor Author

Thanks for your detailed feedback and WIP solution @staskus.

I too would like to see this moving faster but also want to get this right and not add undesirable patterns while staying pragmatic and mindful of speed of development in the short and long term.

I'll take a look and see if your WIP solution looks like a lower level of effort than the refactoring I had planned.

- Make tests compile.
- Required changing access modifiers on `PointOfSaleDashboardViewModel` from `private` to internal access.
- Added test_observeCartAddMoreAction_updates_orderStage_to_building.
- Added two tests that were formerly combined and now have been broken out into separate cases.
- Cleaned up other tests.
Remove trailing whitespace.
@soundsokay
Copy link
Contributor Author

Your rearchitecture looked good to me.

I merged it in and make adjustments to get the tests to compile in commit e8df637.

One downside is that changed access modifiers for cartViewModel and totalsViewModel on PointOfSaleDashboardViewModel from private to internal access. However the class already uses internal access for itemListViewModel.

I also expanded test coverage and cleaned up PointOfSaleDashboardViewModelTests. Which were my focus before pushing up the changes.

CartViewModelTests are failing. Maybe you can have a look?

Seems the issue is with CartViewModel initializing without any parameters.

@staskus
Copy link
Collaborator

staskus commented Jul 23, 2024

CartViewModelTests are failing. Maybe you can have a look? Seems the issue is with CartViewModel initializing without any parameters.

@soundsokay yes, it could be! CartViewModel depended on POSDashboardVM passing an observer through the initializer which prevented CartViewModel from being initialized separately. I will take a look 👍

Copy link
Collaborator

@staskus staskus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It builds, I think it's good time to merge this 👍

@bozidarsevo, would be good if you could take one more look yourself, since I also made the last commits.

…t-selector-when-order-is-being-synced-add-test
@soundsokay
Copy link
Contributor Author

Thank you for your contributions @staskus and @bozidarsevo.

I'm taking a look now.

@bozidarsevo
Copy link
Contributor

I will fix the build now, might have made a wrong conflict merge commit

bozidarsevo and others added 4 commits July 23, 2024 16:23
Adds verification that orderStage is set to .building when cart becomes empty.
Remove TODO for adding test coverage for PointOfSaleDashboardViewModel now that it has been added.
@soundsokay
Copy link
Contributor Author

I made recent commits that only change test code in PointOfSaleDashboardViewModelTests to expand test coverage and remove comments and whitespace.

With all checks passing and two 👍 I'm going to merge.

Thanks again for your reviews and contributions @staskus and @bozidarsevo.

@soundsokay soundsokay merged commit 4e1478a into trunk Jul 23, 2024
22 checks passed
@soundsokay soundsokay deleted the feat/13127-disable-cta-to-go-back-to-product-selector-when-order-is-being-synced-add-test branch July 23, 2024 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
category: unit tests Related to unit testing. type: task An internally driven task.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Woo POS] Expand Unit test coverage for PointOfSaleDashboardViewModel
6 participants