diff --git a/WooCommerce/Classes/ViewModels/Authentication/AccountCreationFormViewModel.swift b/WooCommerce/Classes/ViewModels/Authentication/AccountCreationFormViewModel.swift index 2239d75c2b6..358f29142ad 100644 --- a/WooCommerce/Classes/ViewModels/Authentication/AccountCreationFormViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Authentication/AccountCreationFormViewModel.swift @@ -28,21 +28,22 @@ final class AccountCreationFormViewModel: ObservableObject { private let analytics: Analytics private var subscriptions: Set = [] - init(stores: StoresManager = ServiceLocator.stores, + init(debounceDuration: Double = Constants.fieldDebounceDuration, + stores: StoresManager = ServiceLocator.stores, analytics: Analytics = ServiceLocator.analytics) { self.stores = stores self.analytics = analytics $email .removeDuplicates() - .debounce(for: .seconds(Constants.fieldDebounceDuration), scheduler: DispatchQueue.main) + .debounce(for: .seconds(debounceDuration), scheduler: DispatchQueue.main) .sink { [weak self] email in self?.validateEmail(email) }.store(in: &subscriptions) $password .removeDuplicates() - .debounce(for: .seconds(Constants.fieldDebounceDuration), scheduler: DispatchQueue.main) + .debounce(for: .seconds(debounceDuration), scheduler: DispatchQueue.main) .sink { [weak self] password in self?.validatePassword(password) }.store(in: &subscriptions) diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 92fd9b90b9f..14def4dc5db 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -237,6 +237,7 @@ 026D4A24280461960090164F /* CollectOrderPaymentUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026D4A23280461960090164F /* CollectOrderPaymentUseCaseTests.swift */; }; 0270F47624D005B00005210A /* ProductFormViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0270F47524D005B00005210A /* ProductFormViewModelProtocol.swift */; }; 0270F47824D006F60005210A /* ProductFormPresentationStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0270F47724D006F60005210A /* ProductFormPresentationStyle.swift */; }; + 027111422913B9FC00F5269A /* AccountCreationFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027111412913B9FC00F5269A /* AccountCreationFormViewModelTests.swift */; }; 0271125D2887D4E900FCD13C /* LoggedOutAppSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0271125C2887D4E900FCD13C /* LoggedOutAppSettingsTests.swift */; }; 0271139A24DD15D800574A07 /* ProductsTabProductViewModel+VariationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0271139924DD15D800574A07 /* ProductsTabProductViewModel+VariationTests.swift */; }; 0271E1642509C66200633F7A /* DefaultProductFormTableViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0271E1632509C66200633F7A /* DefaultProductFormTableViewModelTests.swift */; }; @@ -2189,6 +2190,7 @@ 0270C0A827069BEF00FC799F /* Experiments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Experiments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0270F47524D005B00005210A /* ProductFormViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFormViewModelProtocol.swift; sourceTree = ""; }; 0270F47724D006F60005210A /* ProductFormPresentationStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFormPresentationStyle.swift; sourceTree = ""; }; + 027111412913B9FC00F5269A /* AccountCreationFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCreationFormViewModelTests.swift; sourceTree = ""; }; 0271125C2887D4E900FCD13C /* LoggedOutAppSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedOutAppSettingsTests.swift; sourceTree = ""; }; 0271139924DD15D800574A07 /* ProductsTabProductViewModel+VariationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductsTabProductViewModel+VariationTests.swift"; sourceTree = ""; }; 0271E1632509C66200633F7A /* DefaultProductFormTableViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultProductFormTableViewModelTests.swift; sourceTree = ""; }; @@ -4486,6 +4488,14 @@ path = Variations; sourceTree = ""; }; + 027111402913B9D400F5269A /* Authentication */ = { + isa = PBXGroup; + children = ( + 027111412913B9FC00F5269A /* AccountCreationFormViewModelTests.swift */, + ); + path = Authentication; + sourceTree = ""; + }; 02759B8F28FFA06F00918176 /* Store Creation */ = { isa = PBXGroup; children = ( @@ -6094,6 +6104,7 @@ 5791FB4024EC833200117FD6 /* ViewModels */ = { isa = PBXGroup; children = ( + 027111402913B9D400F5269A /* Authentication */, D41C9F2F26D9A41F00993558 /* WhatsNew */, D8025469265517F9001B2CC1 /* CardPresentPayments */, 0371C36F2876ED3A00277E2C /* Feature Announcement Cards */, @@ -11031,6 +11042,7 @@ 4524CDA1242D045C00B2F20A /* ProductStatusSettingListSelectorCommandTests.swift in Sources */, 02077F72253816FF005A78EF /* ProductFormActionsFactory+ReadonlyProductTests.swift in Sources */, D8C11A6222E24C4A00D4A88D /* LedgerTableViewCellTests.swift in Sources */, + 027111422913B9FC00F5269A /* AccountCreationFormViewModelTests.swift in Sources */, DE50295328BF4A8A00551736 /* JetpackConnectionWebViewModelTests.swift in Sources */, AEA622B727468790002A9B57 /* AddOrderCoordinatorTests.swift in Sources */, D802549126552FE1001B2CC1 /* CardPresentModalScanningForReaderTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/ViewModels/Authentication/AccountCreationFormViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewModels/Authentication/AccountCreationFormViewModelTests.swift new file mode 100644 index 00000000000..aecb3fd6358 --- /dev/null +++ b/WooCommerce/WooCommerceTests/ViewModels/Authentication/AccountCreationFormViewModelTests.swift @@ -0,0 +1,198 @@ +import XCTest +import Yosemite +@testable import WooCommerce + +final class AccountCreationFormViewModelTests: XCTestCase { + private var stores: MockStoresManager! + private var analyticsProvider: MockAnalyticsProvider! + private var analytics: WooAnalytics! + private var viewModel: AccountCreationFormViewModel! + + override func setUp() { + super.setUp() + stores = MockStoresManager(sessionManager: SessionManager.makeForTesting()) + analyticsProvider = MockAnalyticsProvider() + analytics = WooAnalytics(analyticsProvider: analyticsProvider) + viewModel = .init(debounceDuration: 0, stores: stores, analytics: analytics) + } + + override func tearDown() { + viewModel = nil + analytics = nil + analyticsProvider = nil + stores = nil + super.tearDown() + } + + // MARK: - `isEmailValid` + + func test_isEmailValid_is_false_after_entering_invalid_email() { + // When + viewModel.email = "notanemail@woocom" + + // Then + waitUntil { + self.viewModel.isEmailValid == false + } + } + + func test_isEmailValid_is_true_after_entering_valid_email() { + // When + viewModel.email = "notanemail@woo.com" + + // Then + waitUntil { + self.viewModel.isEmailValid == true + } + } + + // MARK: - `isPasswordValid` + + func test_isPasswordValid_is_false_after_entering_password_less_than_minimum_length() { + // When + viewModel.password = "minim" + + // Then + waitUntil { + self.viewModel.isPasswordValid == false + } + } + + func test_isPasswordValid_is_true_after_entering_password_of_minimum_length() { + // When + viewModel.password = "minimu" + + // Then + waitUntil { + self.viewModel.isPasswordValid == true + } + } + + // MARK: - `createAccount` + + func test_createAccount_success_sets_state_to_authenticated() async { + // Given + mockAccountCreationSuccess(result: .init(authToken: "token", username: "username")) + XCTAssertFalse(stores.isAuthenticated) + + // When + let result = await viewModel.createAccount() + + // Then + XCTAssertTrue(result.isSuccess) + XCTAssertTrue(stores.isAuthenticated) + } + + func test_createAccount_password_failure_sets_passwordErrorMessage() async { + // Given + mockAccountCreationFailure(error: .invalidPassword(message: "too complex to guess")) + XCTAssertFalse(stores.isAuthenticated) + XCTAssertNil(viewModel.passwordErrorMessage) + + // When + let result = await viewModel.createAccount() + + // Then + XCTAssertTrue(result.isFailure) + XCTAssertFalse(stores.isAuthenticated) + XCTAssertEqual(viewModel.passwordErrorMessage, "too complex to guess") + } + + func test_createAccount_invalidEmail_failure_sets_emailErrorMessage() async { + // Given + mockAccountCreationFailure(error: .invalidEmail) + XCTAssertNil(viewModel.emailErrorMessage) + + // When + let result = await viewModel.createAccount() + + // Then + XCTAssertTrue(result.isFailure) + XCTAssertNotNil(viewModel.emailErrorMessage) + } + + func test_passwordErrorMessage_is_cleared_after_changing_password_input() async { + // Given + mockAccountCreationFailure(error: .invalidPassword(message: "too complex to guess")) + + // When + let _ = await viewModel.createAccount() + viewModel.password = "simple password" + + // Then + waitUntil { + self.viewModel.passwordErrorMessage == nil + } + } + + func test_emailErrorMessage_is_cleared_after_changing_email_input() async { + // Given + mockAccountCreationFailure(error: .emailExists) + + // When + let _ = await viewModel.createAccount() + viewModel.email = "real@woo.com" + + // Then + waitUntil { + self.viewModel.emailErrorMessage == nil + } + } + + // MARK: - analytics + + func test_createAccount_success_tracks_expected_events() async { + // Given + mockAccountCreationSuccess(result: .init(authToken: "", username: "")) + + // When + let _ = await viewModel.createAccount() + + // Then + XCTAssertEqual(analyticsProvider.receivedEvents, ["signup_submitted", "signup_success"]) + } + + func test_createAccount_failure_tracks_expected_events() async { + // Given + mockAccountCreationFailure(error: .emailExists) + + // When + let _ = await viewModel.createAccount() + + // Then + XCTAssertEqual(analyticsProvider.receivedEvents, ["signup_submitted", "signup_failed"]) + } +} + +private extension AccountCreationFormViewModelTests { + func mockAccountCreationSuccess(result: CreateAccountResult) { + stores.whenReceivingAction(ofType: AccountCreationAction.self) { action in + switch action { + case let .createAccount(_, _, completion): + completion(.success(result)) + } + } + + stores.whenReceivingAction(ofType: AccountAction.self) { action in + switch action { + case let .synchronizeAccount(completion): + completion(.success(.fake())) + case let .synchronizeAccountSettings(_, completion): + completion(.success(.fake())) + case let .synchronizeSites(_, completion): + completion(.success(true)) + default: + break + } + } + } + + func mockAccountCreationFailure(error: CreateAccountError) { + stores.whenReceivingAction(ofType: AccountCreationAction.self) { action in + guard case let .createAccount(_, _, completion) = action else { + return + } + completion(.failure(error)) + } + } +}