Skip to content

Commit 826f8d7

Browse files
authored
Merge pull request #5834 from selanthiraiyan/issue/5829-ReviewsViewController-tests
Add tests to validate `ReviewsViewController` menu bar button item behaviour
2 parents 4051f07 + c0e8dfc commit 826f8d7

File tree

6 files changed

+184
-18
lines changed

6 files changed

+184
-18
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [**] Product SKU input scanner is now available as a beta feature. To try it, enable it from settings and you can scan a barcode to use as the product SKU in product inventory settings! [https://github.com/woocommerce/woocommerce-ios/pull/5695]
88
- [**] Now you chan share a payment link when creating a Simple Payments order [https://github.com/woocommerce/woocommerce-ios/pull/5819]
99
- [*] Reviews: "Mark all as read" checkmark bar button item button replaced with menu button which launches an action sheet. Menu button is displayed only if there are unread reviews available.[https://github.com/woocommerce/woocommerce-ios/pull/5833]
10+
- [internal] Refactored ReviewsViewController to add tests. [https://github.com/woocommerce/woocommerce-ios/pull/5834]
1011

1112
8.2
1213
-----

WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import SafariServices.SFSafariViewController
66
//
77
final class ReviewsViewController: UIViewController {
88

9-
private let siteID: Int64
9+
typealias ViewModel = ReviewsViewModelOutput & ReviewsViewModelActionsHandler
1010

1111
/// Main TableView.
1212
///
@@ -26,7 +26,7 @@ final class ReviewsViewController: UIViewController {
2626
return item
2727
}()
2828

29-
private let viewModel: ReviewsViewModel
29+
private let viewModel: ViewModel
3030

3131
/// Haptic Feedback!
3232
///
@@ -111,11 +111,15 @@ final class ReviewsViewController: UIViewController {
111111
})
112112
}()
113113

114-
// MARK: - View Lifecycle
114+
// MARK: - Initializers
115+
//
116+
convenience init(siteID: Int64) {
117+
self.init(viewModel: ReviewsViewModel(siteID: siteID,
118+
data: DefaultReviewsDataSource(siteID: siteID)))
119+
}
115120

116-
init(siteID: Int64) {
117-
self.siteID = siteID
118-
self.viewModel = ReviewsViewModel(siteID: siteID, data: DefaultReviewsDataSource(siteID: siteID))
121+
init(viewModel: ViewModel) {
122+
self.viewModel = viewModel
119123

120124
super.init(nibName: nil, bundle: nil)
121125

@@ -127,6 +131,8 @@ final class ReviewsViewController: UIViewController {
127131
fatalError("init(coder:) has not been implemented")
128132
}
129133

134+
// MARK: - View Lifecycle
135+
//
130136
override func viewDidLoad() {
131137
super.viewDidLoad()
132138
view.backgroundColor = .listBackground
@@ -151,7 +157,7 @@ final class ReviewsViewController: UIViewController {
151157
syncingCoordinator.resynchronize()
152158
}
153159

154-
if AppRatingManager.shared.shouldPromptForAppReview(section: Constants.section) {
160+
if viewModel.shouldPromptForAppReview {
155161
displayRatingPrompt()
156162
}
157163

@@ -604,10 +610,6 @@ private extension ReviewsViewController {
604610
case results
605611
case syncing(pageNumber: Int)
606612
}
607-
608-
struct Constants {
609-
static let section = "notifications"
610-
}
611613
}
612614

613615
// MARK: - Localization

WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewModel.swift

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,48 @@ import Yosemite
55

66
import class AutomatticTracks.CrashLogging
77

8+
/// Provides data for the Reviews screen
9+
///
10+
protocol ReviewsViewModelOutput {
11+
var isEmpty: Bool { get }
812

9-
final class ReviewsViewModel {
13+
var dataSource: UITableViewDataSource { get }
14+
15+
var delegate: ReviewsInteractionDelegate { get }
16+
17+
var hasUnreadNotifications: Bool { get }
18+
19+
var shouldPromptForAppReview: Bool { get }
20+
21+
var hasErrorLoadingData: Bool { get set }
22+
23+
func containsMorePages(_ highestVisibleReview: Int) -> Bool
24+
}
25+
26+
/// Handles actions related to Reviews screen
27+
///
28+
protocol ReviewsViewModelActionsHandler {
29+
func displayPlaceholderReviews(tableView: UITableView)
30+
31+
func removePlaceholderReviews(tableView: UITableView)
32+
33+
func configureResultsController(tableView: UITableView)
34+
35+
func refreshResults()
36+
37+
func configureTableViewCells(tableView: UITableView)
38+
39+
func markAllAsRead(onCompletion: @escaping (Error?) -> Void)
40+
41+
func synchronizeReviews(pageNumber: Int,
42+
pageSize: Int,
43+
onCompletion: (() -> Void)?)
44+
}
45+
46+
/// Provides data and handles actions of Reviews screen.
47+
/// Used as view model for `ReviewsViewController`
48+
///
49+
final class ReviewsViewModel: ReviewsViewModelOutput, ReviewsViewModelActionsHandler {
1050
private let siteID: Int64
1151

1252
private let data: ReviewsDataSource
@@ -31,6 +71,12 @@ final class ReviewsViewModel {
3171
return data.notifications.filter { $0.read == false }
3272
}
3373

74+
/// Used to check whether the user should be prompted for an app from `ReviewsViewController`
75+
///
76+
var shouldPromptForAppReview: Bool {
77+
AppRatingManager.shared.shouldPromptForAppReview(section: Constants.section)
78+
}
79+
3480
/// Set when sync fails, and used to display an error loading data banner
3581
///
3682
var hasErrorLoadingData: Bool = false
@@ -93,9 +139,9 @@ final class ReviewsViewModel {
93139
extension ReviewsViewModel {
94140
/// Prepares data necessary to render the reviews tab.
95141
///
96-
func synchronizeReviews(pageNumber: Int = Settings.firstPage,
97-
pageSize: Int = Settings.pageSize,
98-
onCompletion: (() -> Void)? = nil) {
142+
func synchronizeReviews(pageNumber: Int,
143+
pageSize: Int,
144+
onCompletion: (() -> Void)?) {
99145
hasErrorLoadingData = false
100146

101147
let group = DispatchGroup()
@@ -202,4 +248,8 @@ private extension ReviewsViewModel {
202248
static let firstPage = 1
203249
static let pageSize = 25
204250
}
251+
252+
struct Constants {
253+
static let section = "notifications"
254+
}
205255
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,7 @@
11251125
B6E851F5276330200041D1BA /* RefundFeesDetailsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E851F4276330200041D1BA /* RefundFeesDetailsTableViewCell.swift */; };
11261126
B6E851F7276331110041D1BA /* RefundFeesDetailsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B6E851F6276331110041D1BA /* RefundFeesDetailsTableViewCell.xib */; };
11271127
B873E8F8E103966D2182EE67 /* Pods_WooCommerceTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DC4526F9A7357761197EBF0 /* Pods_WooCommerceTests.framework */; };
1128+
BAA34C202787494300846F3C /* ReviewsViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA34C1F2787494300846F3C /* ReviewsViewControllerTests.swift */; };
11281129
BAE4F8432734325C00871344 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE4F8422734325C00871344 /* SettingsViewModel.swift */; };
11291130
BAFEF51E273C2151005F94CC /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFEF51D273C2151005F94CC /* SettingsViewModelTests.swift */; };
11301131
CC0324A3263AD9F40056C6B7 /* MockShippingLabelAccountSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0324A2263AD9F40056C6B7 /* MockShippingLabelAccountSettings.swift */; };
@@ -2704,6 +2705,7 @@
27042705
B6E851F2276320C70041D1BA /* RefundFeesDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundFeesDetailsViewModel.swift; sourceTree = "<group>"; };
27052706
B6E851F4276330200041D1BA /* RefundFeesDetailsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundFeesDetailsTableViewCell.swift; sourceTree = "<group>"; };
27062707
B6E851F6276331110041D1BA /* RefundFeesDetailsTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefundFeesDetailsTableViewCell.xib; sourceTree = "<group>"; };
2708+
BAA34C1F2787494300846F3C /* ReviewsViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReviewsViewControllerTests.swift; sourceTree = "<group>"; };
27072709
BABE5E07DD787ECA6D2A76DE /* Pods_WooCommerce.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WooCommerce.framework; sourceTree = BUILT_PRODUCTS_DIR; };
27082710
BAE4F8422734325C00871344 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
27092711
BAFEF51D273C2151005F94CC /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = "<group>"; };
@@ -6057,6 +6059,14 @@
60576059
path = Settings;
60586060
sourceTree = "<group>";
60596061
};
6062+
BAA34C1E2787494300846F3C /* Reviews */ = {
6063+
isa = PBXGroup;
6064+
children = (
6065+
BAA34C1F2787494300846F3C /* ReviewsViewControllerTests.swift */,
6066+
);
6067+
path = Reviews;
6068+
sourceTree = "<group>";
6069+
};
60606070
BAF1B3B32736595A00BA11DC /* Settings */ = {
60616071
isa = PBXGroup;
60626072
children = (
@@ -6930,6 +6940,7 @@
69306940
D816DDBA22265D8000903E59 /* ViewRelated */ = {
69316941
isa = PBXGroup;
69326942
children = (
6943+
BAA34C1E2787494300846F3C /* Reviews */,
69336944
03FBDAF7263EE49300ACE257 /* Coupons */,
69346945
BA143222273662DE00E4B3AB /* Settings */,
69356946
D449C52A26E02EFD00D75B02 /* WhatsNew */,
@@ -8894,6 +8905,7 @@
88948905
5761298B24589B84007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlusTests.swift in Sources */,
88958906
2667BFD7252E5DBF008099D4 /* RefundItemViewModelTests.swift in Sources */,
88968907
7435E59021C0162C00216F0F /* OrderNoteWooTests.swift in Sources */,
8908+
BAA34C202787494300846F3C /* ReviewsViewControllerTests.swift in Sources */,
88978909
02CE43092769953D0006EAEF /* MockCaptureDevicePermissionChecker.swift in Sources */,
88988910
7E6A01A32726C5D3001668D5 /* MockProductCategoryStoresManager.swift in Sources */,
88998911
45F5A3C323DF31D2007D40E5 /* ShippingInputFormatterTests.swift in Sources */,

WooCommerce/WooCommerceTests/Reviews/ReviewsViewModelTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ final class ReviewsViewModelTests: XCTestCase {
6666

6767
let expec = expectation(description: "Wait for synchronizeReviews to complete")
6868

69-
viewModel.synchronizeReviews {
69+
viewModel.synchronizeReviews(pageNumber: 1, pageSize: 25) {
7070
let allTargetsHit = storesManager.syncReviewsIsHit && storesManager.retrieveProductsIsHit
7171
XCTAssertTrue(allTargetsHit)
7272
if allTargetsHit {
@@ -86,7 +86,7 @@ final class ReviewsViewModelTests: XCTestCase {
8686
ServiceLocator.setStores(storesManager)
8787

8888
// When
89-
viewModel.synchronizeReviews()
89+
viewModel.synchronizeReviews(pageNumber: 1, pageSize: 25, onCompletion: nil)
9090

9191
// Then
9292
XCTAssertFalse(viewModel.hasErrorLoadingData)
@@ -107,7 +107,7 @@ final class ReviewsViewModelTests: XCTestCase {
107107
ServiceLocator.setStores(storesManager)
108108

109109
// When
110-
viewModel.synchronizeReviews()
110+
viewModel.synchronizeReviews(pageNumber: 1, pageSize: 25, onCompletion: nil)
111111

112112
// Then
113113
XCTAssertTrue(viewModel.hasErrorLoadingData)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import XCTest
2+
@testable import WooCommerce
3+
4+
/// Tests for `ReviewsViewController`.
5+
///
6+
final class ReviewsViewControllerTests: XCTestCase {
7+
private var mockViewModel: MockReviewsViewModel!
8+
private var sut: ReviewsViewController!
9+
10+
override func setUpWithError() throws {
11+
try super.setUpWithError()
12+
13+
mockViewModel = MockReviewsViewModel(siteID: 123)
14+
sut = ReviewsViewController(viewModel: mockViewModel)
15+
}
16+
17+
override func tearDownWithError() throws {
18+
mockViewModel = nil
19+
sut = nil
20+
21+
try super.tearDownWithError()
22+
}
23+
24+
func test_menu_bar_button_item_is_not_present_if_there_are_no_unread_notifications() {
25+
// When
26+
mockViewModel.hasUnreadNotifications = false
27+
sut.makeViewAppear()
28+
29+
// Then
30+
XCTAssertNil(sut.navigationItem.rightBarButtonItem)
31+
}
32+
33+
func test_menu_bar_button_item_is_visible_if_there_are_unread_notifications_available() throws {
34+
// When
35+
mockViewModel.hasUnreadNotifications = true
36+
sut.makeViewAppear()
37+
38+
// Then
39+
let markAllAsReadyButton = try XCTUnwrap(sut.navigationItem.rightBarButtonItem)
40+
XCTAssertEqual(markAllAsReadyButton.accessibilityIdentifier, "reviews-open-menu-button")
41+
}
42+
}
43+
44+
// MARK: ReviewsViewController helpers
45+
//
46+
private extension ReviewsViewController {
47+
func makeViewAppear() {
48+
loadViewIfNeeded()
49+
beginAppearanceTransition(true, animated: false)
50+
endAppearanceTransition()
51+
}
52+
}
53+
54+
// MARK: Mocks
55+
//
56+
private final class MockReviewsViewModel: ReviewsViewModelOutput, ReviewsViewModelActionsHandler {
57+
58+
private let data: ReviewsDataSource
59+
60+
init(siteID: Int64) {
61+
self.data = DefaultReviewsDataSource(siteID: siteID)
62+
}
63+
64+
// `ReviewsViewModelOutput` conformance
65+
//
66+
var isEmpty: Bool {
67+
data.isEmpty
68+
}
69+
70+
var dataSource: UITableViewDataSource {
71+
data
72+
}
73+
74+
var delegate: ReviewsInteractionDelegate {
75+
data
76+
}
77+
78+
var hasUnreadNotifications = true
79+
80+
var shouldPromptForAppReview = false
81+
82+
var hasErrorLoadingData = true
83+
84+
func containsMorePages(_ highestVisibleReview: Int) -> Bool { false }
85+
86+
// Empty methods for `ReviewsViewModelActionsHandler` conformance
87+
//
88+
func displayPlaceholderReviews(tableView: UITableView) {}
89+
90+
func removePlaceholderReviews(tableView: UITableView) {}
91+
92+
func configureResultsController(tableView: UITableView) {}
93+
94+
func refreshResults() {}
95+
96+
func configureTableViewCells(tableView: UITableView) {}
97+
98+
func markAllAsRead(onCompletion: @escaping (Error?) -> Void) {}
99+
100+
func synchronizeReviews(pageNumber: Int, pageSize: Int, onCompletion: (() -> Void)?) {}
101+
}

0 commit comments

Comments
 (0)