[#611] Disable Send ZEC button when sync in progress (#623)

This disables the send button when the app is syncing. but if the user
is already there it won't change the underlying store to avoid unwanted
re-renders by SwiftUI engine.

Test reflect this situation. Also fixed a problem where the tests would
not reflect the correct state from the dependency injection.

Closes #611
This commit is contained in:
Francisco Gindre 2023-03-07 18:19:16 -03:00 committed by GitHub
parent c6b222ff46
commit 4212e4781b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 115 additions and 11 deletions

View File

@ -1,4 +1,6 @@
# Changelog
# Unreleased
- [#611] Disable Send ZEC button when sync in progress
# 0.0.1 build 44
This is the baseline build for iOS Re-Scoping epic.
- [#819] build and release from tag 0.0.1-44

View File

@ -12,19 +12,28 @@ import ZcashLightClientKit
extension SDKSynchronizerDependency {
static let mock: SDKSynchronizerClient = MockSDKSynchronizerClient()
static func mockWithSnapshot(_ snapshot: SyncStatusSnapshot) -> MockSDKSynchronizerClient {
MockSDKSynchronizerClient(snapshot: snapshot)
}
}
class MockSDKSynchronizerClient: SDKSynchronizerClient {
private var cancellables: [AnyCancellable] = []
private var snapshot: SyncStatusSnapshot
private(set) var notificationCenter: NotificationCenterClient
private(set) var synchronizer: SDKSynchronizer?
private(set) var stateChanged: CurrentValueSubject<SDKSynchronizerState, Never>
private(set) var walletBirthday: BlockHeight?
private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState?
init(notificationCenter: NotificationCenterClient = .noOp) {
init(
notificationCenter: NotificationCenterClient = .noOp,
snapshot: SyncStatusSnapshot = .default
) {
self.notificationCenter = notificationCenter
self.stateChanged = CurrentValueSubject<SDKSynchronizerState, Never>(.unknown)
self.snapshot = snapshot
}
func prepareWith(initializer: Initializer, seedBytes: [UInt8]) throws { }
@ -39,7 +48,7 @@ class MockSDKSynchronizerClient: SDKSynchronizerClient {
func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) { }
func statusSnapshot() -> SyncStatusSnapshot { .default }
func statusSnapshot() -> SyncStatusSnapshot { self.snapshot }
func rewind(_ policy: RewindPolicy) -> AnyPublisher<Void, Error>? { Empty<Void, Error>().eraseToAnyPublisher() }

View File

@ -53,6 +53,12 @@ struct HomeReducer: ReducerProtocol {
}
return false
}
var isSendButtonDisabled: Bool {
// If the destination is `.send` the button must be enabled
// to avoid involuntary navigation pop.
self.destination != .send && self.isSyncing
}
}
enum Action: Equatable {

View File

@ -82,6 +82,8 @@ extension HomeView {
})
.activeButtonStyle
.padding(.bottom, 30)
.disabled(viewStore.isSendButtonDisabled)
.opacity(viewStore.isSendButtonDisabled ? 0.5 : 1)
}
func receiveButton(_ viewStore: HomeViewStore) -> some View {

View File

@ -6,9 +6,9 @@
//
import XCTest
@testable import secant_testnet
import ComposableArchitecture
import ZcashLightClientKit
@testable import secant_testnet
@testable import ZcashLightClientKit
class HomeTests: XCTestCase {
func testSynchronizerStateChanged_AnyButSynced() throws {
@ -32,7 +32,7 @@ class HomeTests: XCTestCase {
reducer: HomeReducer()
) { dependencies in
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
}
store.send(.synchronizerStateChanged(.synced))
@ -41,7 +41,92 @@ class HomeTests: XCTestCase {
store.receive(.updateSynchronizerStatus)
}
func testSendButtonIsDisabledWhenSyncing() {
let testScheduler = DispatchQueue.test
let mockSnapshot = SyncStatusSnapshot.init(
.syncing(
.init(
startHeight: 1_700_000,
targetHeight: 1_800_000,
progressHeight: 1_770_000
)
)
)
let store = TestStore(
initialState: .init(
balanceBreakdownState: .placeholder,
profileState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
settingsState: .placeholder,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: mockSnapshot,
walletEventsState: .emptyPlaceHolder
),
reducer: HomeReducer()
)
store.dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
store.dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(mockSnapshot)
store.send(.synchronizerStateChanged(.progressUpdated))
testScheduler.advance(by: 0.01)
store.receive(.updateSynchronizerStatus)
XCTAssertTrue(store.state.isSyncing)
XCTAssertTrue(store.state.isSendButtonDisabled)
}
func testSendButtonIsNotDisabledWhenSyncingWhileOnSendScreen() {
let testScheduler = DispatchQueue.test
let mockSnapshot = SyncStatusSnapshot.init(
.syncing(
.init(
startHeight: 1_700_000,
targetHeight: 1_800_000,
progressHeight: 1_770_000
)
)
)
let store = TestStore(
initialState: .init(
balanceBreakdownState: .placeholder,
profileState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
settingsState: .placeholder,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: mockSnapshot,
walletEventsState: .emptyPlaceHolder
),
reducer: HomeReducer()
)
store.dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
store.dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(mockSnapshot)
store.send(.updateDestination(.send)) {
$0.destination = .send
}
testScheduler.advance(by: 0.01)
store.send(.synchronizerStateChanged(.progressUpdated))
testScheduler.advance(by: 0.01)
store.receive(.updateSynchronizerStatus)
XCTAssertTrue(store.state.isSyncing)
XCTAssertFalse(store.state.isSendButtonDisabled)
}
/// The .onAppear action is important to register for the synchronizer state updates.
/// The integration tests make sure registrations and side effects are properly implemented.
func testOnAppear() throws {
@ -84,7 +169,7 @@ class HomeTests: XCTestCase {
}
// long-living (cancelable) effects need to be properly canceled.
// the .onDisappear action cancles the observer of the synchronizer status change.
// the .onDisappear action cancels the observer of the synchronizer status change.
store.send(.onDisappear)
}
}

View File

@ -20,7 +20,7 @@ class ProfileTests: XCTestCase {
reducer: ProfileReducer()
) { dependencies in
dependencies.appVersion = .mock
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
}
let uAddress = try UnifiedAddress(

View File

@ -50,7 +50,7 @@ class SendTests: XCTestCase {
dependencies.derivationTool = .liveValue
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
dependencies.mnemonic = .liveValue
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
dependencies.walletStorage = .noOp
}
@ -122,7 +122,7 @@ class SendTests: XCTestCase {
dependencies.derivationTool = .liveValue
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
dependencies.mnemonic = .liveValue
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
dependencies.walletStorage = .noOp
}

View File

@ -78,7 +78,7 @@ class WalletEventsTests: XCTestCase {
reducer: WalletEventsFlowReducer()
) { dependencies in
dependencies.mainQueue = Self.testScheduler.eraseToAnyScheduler()
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
}
store.send(.synchronizerStateChanged(.synced))