Skip to content

Commit 1b685f4

Browse files
committed
Update domain selector view conditional logic with a state enum in the view model.
1 parent 2d31f8a commit 1b685f4

File tree

3 files changed

+78
-53
lines changed

3 files changed

+78
-53
lines changed

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ private extension DomainSelectorHostingController {
4545

4646
/// Allows the user to search for a domain and then select one to continue.
4747
struct DomainSelectorView: View {
48+
/// The state of the main view below the fixed header.
49+
enum ViewState: Equatable {
50+
/// When loading domain suggestions.
51+
case loading
52+
/// Shown when the search query is empty.
53+
case placeholder
54+
/// When there is an error loading domain suggestions.
55+
case error(message: String)
56+
/// When domain suggestions are displayed.
57+
case results(domains: [String])
58+
}
59+
4860
/// Set in the hosting controller.
4961
var onDomainSelection: ((String) async -> Void) = { _ in }
5062

@@ -87,7 +99,8 @@ struct DomainSelectorView: View {
8799
iconSize: .init(width: 14, height: 14)))
88100
.focused($textFieldIsFocused)
89101

90-
if viewModel.searchTerm.isEmpty {
102+
switch viewModel.state {
103+
case .placeholder:
91104
// Placeholder image when search query is empty.
92105
Spacer()
93106
.frame(height: 30)
@@ -97,7 +110,7 @@ struct DomainSelectorView: View {
97110
Image(uiImage: .domainSearchPlaceholderImage)
98111
Spacer()
99112
}
100-
} else if viewModel.isLoadingDomainSuggestions {
113+
case .loading:
101114
// Progress indicator when loading domain suggestions.
102115
Spacer()
103116
.frame(height: 23)
@@ -107,7 +120,7 @@ struct DomainSelectorView: View {
107120
ProgressView()
108121
Spacer()
109122
}
110-
} else if let errorMessage = viewModel.errorMessage {
123+
case .error(let errorMessage):
111124
// Error message when there is an error loading domain suggestions.
112125
Spacer()
113126
.frame(height: 23)
@@ -118,7 +131,7 @@ struct DomainSelectorView: View {
118131
.frame(maxWidth: .infinity, alignment: .center)
119132
.multilineTextAlignment(.center)
120133
.padding(Layout.defaultPadding)
121-
} else {
134+
case .results(let domains):
122135
// Domain suggestions.
123136
Text(Localization.suggestionsHeader)
124137
.foregroundColor(Color(.secondaryLabel))
@@ -127,7 +140,7 @@ struct DomainSelectorView: View {
127140
.padding(.vertical, insets: .init(top: 14, leading: 0, bottom: 8, trailing: 0))
128141

129142
LazyVStack {
130-
ForEach(viewModel.domains, id: \.self) { domain in
143+
ForEach(domains, id: \.self) { domain in
131144
Button {
132145
textFieldIsFocused = false
133146
selectedDomainName = domain

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorViewModel.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ final class DomainSelectorViewModel: ObservableObject {
1010
@Published var searchTerm: String = ""
1111

1212
/// Domain names after domain suggestions are loaded remotely.
13-
@Published private(set) var domains: [String] = []
13+
@Published private var domains: [String] = []
1414

1515
/// Error message from loading domain suggestions.
16-
@Published private(set) var errorMessage: String?
16+
@Published private var errorMessage: String?
1717

1818
/// Whether domain suggestions are being loaded.
1919
@Published private(set) var isLoadingDomainSuggestions: Bool = false
2020

21+
/// The state of the main domain selector view based on the search query and loading state.
22+
@Published private(set) var state: DomainSelectorView.ViewState = .placeholder
23+
2124
/// Subscription for search query changes for domain search.
2225
private var searchQuerySubscription: AnyCancellable?
2326

@@ -36,6 +39,8 @@ final class DomainSelectorViewModel: ObservableObject {
3639
// and thus the initial value isn't emitted in `observeDomainQuery` until setting the value afterward.
3740
observeDomainQuery()
3841
self.searchTerm = initialSearchTerm
42+
43+
configureState()
3944
}
4045
}
4146

@@ -64,6 +69,22 @@ private extension DomainSelectorViewModel {
6469
}
6570
}
6671

72+
func configureState() {
73+
Publishers.CombineLatest4($searchTerm, $isLoadingDomainSuggestions, $errorMessage, $domains)
74+
.map { searchTerm, isLoadingDomainSuggestions, errorMessage, domains in
75+
if searchTerm.isEmpty {
76+
return .placeholder
77+
} else if isLoadingDomainSuggestions {
78+
return .loading
79+
} else if let errorMessage {
80+
return .error(message: errorMessage)
81+
} else {
82+
return .results(domains: domains)
83+
}
84+
}
85+
.assign(to: &$state)
86+
}
87+
6788
@MainActor
6889
func loadFreeDomainSuggestions(query: String) async -> Result<[FreeDomainSuggestion], Error> {
6990
await withCheckedContinuation { continuation in

WooCommerce/WooCommerceTests/ViewModels/Domains/DomainSelectorViewModelTests.swift

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -24,49 +24,61 @@ final class DomainSelectorViewModelTests: XCTestCase {
2424
func test_DomainAction_is_not_dispatched_when_searchTerm_is_empty() {
2525
// Given
2626
stores.whenReceivingAction(ofType: DomainAction.self) { action in
27+
// Then
2728
XCTFail("Unexpected action: \(action)")
2829
}
2930

3031
// When
3132
viewModel.searchTerm = ""
3233
viewModel.searchTerm = ""
33-
34-
// Then
35-
XCTAssertEqual(viewModel.domains, [])
36-
XCTAssertTrue(stores.receivedActions.isEmpty)
3734
}
3835

39-
func test_domain_suggestions_success_returns_domain_rows_for_free_domains() {
40-
// Given
41-
mockDomainSuggestionsSuccess(suggestions: [
42-
.init(name: "free.com", isFree: true),
43-
.init(name: "paid.com", isFree: false)
44-
])
36+
// MARK: - `isLoadingDomainSuggestions`
37+
38+
func test_isLoadingDomainSuggestions_is_toggled_when_loading_suggestions() {
39+
var loadingValues: [Bool] = []
40+
viewModel.$isLoadingDomainSuggestions.sink { value in
41+
loadingValues.append(value)
42+
}.store(in: &subscriptions)
43+
44+
mockDomainSuggestionsFailure(error: SampleError.first)
4545

4646
// When
47-
viewModel.searchTerm = "woo"
47+
viewModel.searchTerm = "Woo"
4848

4949
// Then
5050
waitUntil {
51-
self.viewModel.domains.isNotEmpty
51+
loadingValues == [false, true, false]
5252
}
53-
XCTAssertEqual(viewModel.domains, ["free.com"])
5453
}
5554

56-
func test_domain_suggestions_failure_does_not_update_domain_rows() {
55+
// MARK: - `state`
56+
57+
func test_state_is_placeholder_when_searchTerm_is_empty() {
58+
// When
59+
viewModel.searchTerm = ""
60+
61+
// Then
62+
XCTAssertEqual(viewModel.state, .placeholder)
63+
}
64+
65+
func test_state_is_results_with_free_domain_only_on_domain_suggestions_success() {
5766
// Given
58-
mockDomainSuggestionsFailure(error: SampleError.first)
67+
mockDomainSuggestionsSuccess(suggestions: [
68+
.init(name: "free.com", isFree: true),
69+
.init(name: "paid.com", isFree: false)
70+
])
5971

6072
// When
6173
viewModel.searchTerm = "woo"
6274

6375
// Then
64-
XCTAssertEqual(viewModel.domains, [])
76+
waitUntil {
77+
self.viewModel.state == .results(domains: ["free.com"])
78+
}
6579
}
6680

67-
// MARK: - `errorMessage`
68-
69-
func test_domain_suggestions_failure_with_non_DotcomError_sets_default_error_message() {
81+
func test_state_is_errorMessage_with_default_error_message_when_failure_is_not_DotcomError() {
7082
// Given
7183
mockDomainSuggestionsFailure(error: SampleError.first)
7284

@@ -75,12 +87,11 @@ final class DomainSelectorViewModelTests: XCTestCase {
7587

7688
// Then
7789
waitUntil {
78-
self.viewModel.errorMessage?.isNotEmpty == true
90+
self.viewModel.state == .error(message: DomainSelectorViewModel.Localization.defaultErrorMessage)
7991
}
80-
XCTAssertEqual(viewModel.errorMessage, DomainSelectorViewModel.Localization.defaultErrorMessage)
8192
}
8293

83-
func test_domain_suggestions_failure_with_DotcomError_unknown_error_sets_error_message() {
94+
func test_state_is_errorMessage_with_DotcomError_message_when_failure_is_DotcomError() {
8495
// Given
8596
mockDomainSuggestionsFailure(error: DotcomError.unknown(code: "", message: "error message"))
8697

@@ -89,46 +100,26 @@ final class DomainSelectorViewModelTests: XCTestCase {
89100

90101
// Then
91102
waitUntil {
92-
self.viewModel.errorMessage?.isNotEmpty == true
103+
self.viewModel.state == .error(message: "error message")
93104
}
94-
XCTAssertEqual(viewModel.errorMessage, "error message")
95105
}
96106

97-
func test_domain_suggestions_error_message_is_reset_when_loading_domain_suggestions() {
107+
func test_state_is_updated_from_errorMessage_to_results_when_changing_search_term_after_failure() {
98108
// Given
99109
mockDomainSuggestionsFailure(error: SampleError.first)
100110

101111
// When
102112
viewModel.searchTerm = "woo"
103113
waitUntil {
104-
self.viewModel.errorMessage?.isNotEmpty == true
114+
self.viewModel.state == .error(message: DomainSelectorViewModel.Localization.defaultErrorMessage)
105115
}
106116

107117
mockDomainSuggestionsSuccess(suggestions: [])
108118
viewModel.searchTerm = "wooo"
109119

110120
// Then
111121
waitUntil {
112-
self.viewModel.errorMessage == nil
113-
}
114-
}
115-
116-
// MARK: `isLoadingDomainSuggestions`
117-
118-
func test_isLoadingDomainSuggestions_is_toggled_when_loading_suggestions() {
119-
var loadingValues: [Bool] = []
120-
viewModel.$isLoadingDomainSuggestions.sink { value in
121-
loadingValues.append(value)
122-
}.store(in: &subscriptions)
123-
124-
mockDomainSuggestionsFailure(error: SampleError.first)
125-
126-
// When
127-
viewModel.searchTerm = "Woo"
128-
129-
// Then
130-
waitUntil {
131-
loadingValues == [false, true, false]
122+
self.viewModel.state == .results(domains: [])
132123
}
133124
}
134125
}

0 commit comments

Comments
 (0)