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:
parent
c6b222ff46
commit
4212e4781b
|
@ -1,4 +1,6 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
# Unreleased
|
||||||
|
- [#611] Disable Send ZEC button when sync in progress
|
||||||
# 0.0.1 build 44
|
# 0.0.1 build 44
|
||||||
This is the baseline build for iOS Re-Scoping epic.
|
This is the baseline build for iOS Re-Scoping epic.
|
||||||
- [#819] build and release from tag 0.0.1-44
|
- [#819] build and release from tag 0.0.1-44
|
||||||
|
|
|
@ -12,19 +12,28 @@ import ZcashLightClientKit
|
||||||
|
|
||||||
extension SDKSynchronizerDependency {
|
extension SDKSynchronizerDependency {
|
||||||
static let mock: SDKSynchronizerClient = MockSDKSynchronizerClient()
|
static let mock: SDKSynchronizerClient = MockSDKSynchronizerClient()
|
||||||
|
|
||||||
|
static func mockWithSnapshot(_ snapshot: SyncStatusSnapshot) -> MockSDKSynchronizerClient {
|
||||||
|
MockSDKSynchronizerClient(snapshot: snapshot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockSDKSynchronizerClient: SDKSynchronizerClient {
|
class MockSDKSynchronizerClient: SDKSynchronizerClient {
|
||||||
private var cancellables: [AnyCancellable] = []
|
private var cancellables: [AnyCancellable] = []
|
||||||
|
private var snapshot: SyncStatusSnapshot
|
||||||
private(set) var notificationCenter: NotificationCenterClient
|
private(set) var notificationCenter: NotificationCenterClient
|
||||||
private(set) var synchronizer: SDKSynchronizer?
|
private(set) var synchronizer: SDKSynchronizer?
|
||||||
private(set) var stateChanged: CurrentValueSubject<SDKSynchronizerState, Never>
|
private(set) var stateChanged: CurrentValueSubject<SDKSynchronizerState, Never>
|
||||||
private(set) var walletBirthday: BlockHeight?
|
private(set) var walletBirthday: BlockHeight?
|
||||||
private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState?
|
private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState?
|
||||||
|
|
||||||
init(notificationCenter: NotificationCenterClient = .noOp) {
|
init(
|
||||||
|
notificationCenter: NotificationCenterClient = .noOp,
|
||||||
|
snapshot: SyncStatusSnapshot = .default
|
||||||
|
) {
|
||||||
self.notificationCenter = notificationCenter
|
self.notificationCenter = notificationCenter
|
||||||
self.stateChanged = CurrentValueSubject<SDKSynchronizerState, Never>(.unknown)
|
self.stateChanged = CurrentValueSubject<SDKSynchronizerState, Never>(.unknown)
|
||||||
|
self.snapshot = snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareWith(initializer: Initializer, seedBytes: [UInt8]) throws { }
|
func prepareWith(initializer: Initializer, seedBytes: [UInt8]) throws { }
|
||||||
|
@ -39,7 +48,7 @@ class MockSDKSynchronizerClient: SDKSynchronizerClient {
|
||||||
|
|
||||||
func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) { }
|
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() }
|
func rewind(_ policy: RewindPolicy) -> AnyPublisher<Void, Error>? { Empty<Void, Error>().eraseToAnyPublisher() }
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,12 @@ struct HomeReducer: ReducerProtocol {
|
||||||
}
|
}
|
||||||
return false
|
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 {
|
enum Action: Equatable {
|
||||||
|
|
|
@ -82,6 +82,8 @@ extension HomeView {
|
||||||
})
|
})
|
||||||
.activeButtonStyle
|
.activeButtonStyle
|
||||||
.padding(.bottom, 30)
|
.padding(.bottom, 30)
|
||||||
|
.disabled(viewStore.isSendButtonDisabled)
|
||||||
|
.opacity(viewStore.isSendButtonDisabled ? 0.5 : 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func receiveButton(_ viewStore: HomeViewStore) -> some View {
|
func receiveButton(_ viewStore: HomeViewStore) -> some View {
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
@testable import secant_testnet
|
|
||||||
import ComposableArchitecture
|
import ComposableArchitecture
|
||||||
import ZcashLightClientKit
|
@testable import secant_testnet
|
||||||
|
@testable import ZcashLightClientKit
|
||||||
|
|
||||||
class HomeTests: XCTestCase {
|
class HomeTests: XCTestCase {
|
||||||
func testSynchronizerStateChanged_AnyButSynced() throws {
|
func testSynchronizerStateChanged_AnyButSynced() throws {
|
||||||
|
@ -32,7 +32,7 @@ class HomeTests: XCTestCase {
|
||||||
reducer: HomeReducer()
|
reducer: HomeReducer()
|
||||||
) { dependencies in
|
) { dependencies in
|
||||||
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
|
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
|
||||||
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
|
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
store.send(.synchronizerStateChanged(.synced))
|
store.send(.synchronizerStateChanged(.synced))
|
||||||
|
@ -42,6 +42,91 @@ class HomeTests: XCTestCase {
|
||||||
store.receive(.updateSynchronizerStatus)
|
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 .onAppear action is important to register for the synchronizer state updates.
|
||||||
/// The integration tests make sure registrations and side effects are properly implemented.
|
/// The integration tests make sure registrations and side effects are properly implemented.
|
||||||
func testOnAppear() throws {
|
func testOnAppear() throws {
|
||||||
|
@ -84,7 +169,7 @@ class HomeTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
// long-living (cancelable) effects need to be properly canceled.
|
// 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)
|
store.send(.onDisappear)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ class ProfileTests: XCTestCase {
|
||||||
reducer: ProfileReducer()
|
reducer: ProfileReducer()
|
||||||
) { dependencies in
|
) { dependencies in
|
||||||
dependencies.appVersion = .mock
|
dependencies.appVersion = .mock
|
||||||
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
|
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
let uAddress = try UnifiedAddress(
|
let uAddress = try UnifiedAddress(
|
||||||
|
|
|
@ -50,7 +50,7 @@ class SendTests: XCTestCase {
|
||||||
dependencies.derivationTool = .liveValue
|
dependencies.derivationTool = .liveValue
|
||||||
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
|
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
|
||||||
dependencies.mnemonic = .liveValue
|
dependencies.mnemonic = .liveValue
|
||||||
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
|
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
|
||||||
dependencies.walletStorage = .noOp
|
dependencies.walletStorage = .noOp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ class SendTests: XCTestCase {
|
||||||
dependencies.derivationTool = .liveValue
|
dependencies.derivationTool = .liveValue
|
||||||
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
|
dependencies.mainQueue = testScheduler.eraseToAnyScheduler()
|
||||||
dependencies.mnemonic = .liveValue
|
dependencies.mnemonic = .liveValue
|
||||||
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
|
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
|
||||||
dependencies.walletStorage = .noOp
|
dependencies.walletStorage = .noOp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ class WalletEventsTests: XCTestCase {
|
||||||
reducer: WalletEventsFlowReducer()
|
reducer: WalletEventsFlowReducer()
|
||||||
) { dependencies in
|
) { dependencies in
|
||||||
dependencies.mainQueue = Self.testScheduler.eraseToAnyScheduler()
|
dependencies.mainQueue = Self.testScheduler.eraseToAnyScheduler()
|
||||||
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mock
|
dependencies.sdkSynchronizer = SDKSynchronizerDependency.mockWithSnapshot(.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
store.send(.synchronizerStateChanged(.synced))
|
store.send(.synchronizerStateChanged(.synced))
|
||||||
|
|
Loading…
Reference in New Issue