diff --git a/account/build.gradle b/account/build.gradle
index 9499119..ec043e7 100644
--- a/account/build.gradle
+++ b/account/build.gradle
@@ -31,7 +31,11 @@ buildscript {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+
+ api 'io.reactivex.rxjava2:rxjava:2.1.9'
+
testCompile 'junit:junit:4.12'
+ testImplementation 'org.amshove.kluent:kluent:1.34'
}
sourceCompatibility = '1.7'
diff --git a/account/src/main/java/io/github/novacrypto/account/AccountModel.kt b/account/src/main/java/io/github/novacrypto/account/AccountModel.kt
new file mode 100644
index 0000000..5fccd3c
--- /dev/null
+++ b/account/src/main/java/io/github/novacrypto/account/AccountModel.kt
@@ -0,0 +1,45 @@
+/*
+ * NovaWallet, Cryptocurrency Wallet for Android
+ * Copyright (C) 2018 Alan Evans, NovaCrypto
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Original source: https://github.com/NovaCrypto/NovaWallet
+ * You can contact the authors via github issues.
+ */
+
+package io.github.novacrypto.account
+
+import io.reactivex.Observable
+import java.util.*
+
+data class AccountId(val uuid: UUID = UUID.randomUUID())
+
+data class AccountModel(
+ val id: AccountId = AccountId(),
+ val name: String = ""
+)
+
+fun Observable.toAccountModelStream(): Observable {
+ return this.scan(AccountModel(),
+ { model: AccountModel, intent: AccountModelIntent ->
+ accountModelReducer(intent, model)
+ })
+}
+
+fun accountModelReducer(intent: AccountModelIntent, model: AccountModel): AccountModel {
+ return when (intent) {
+ is AccountModelIntent.Rename -> model.copy(name = intent.name)
+ }
+}
diff --git a/account/src/main/java/io/github/novacrypto/account/AccountModelIntents.kt b/account/src/main/java/io/github/novacrypto/account/AccountModelIntents.kt
new file mode 100644
index 0000000..c6dfc88
--- /dev/null
+++ b/account/src/main/java/io/github/novacrypto/account/AccountModelIntents.kt
@@ -0,0 +1,44 @@
+/*
+ * NovaWallet, Cryptocurrency Wallet for Android
+ * Copyright (C) 2018 Alan Evans, NovaCrypto
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Original source: https://github.com/NovaCrypto/NovaWallet
+ * You can contact the authors via github issues.
+ */
+
+package io.github.novacrypto.account
+
+sealed class AccountModelIntent {
+ class Rename(val name: String) : AccountModelIntent()
+}
+
+fun AccountModel.toAddIntent() =
+ UserModelIntent.AddAccountIntent(this).forward()
+
+fun AccountModel.toInsertIntent(index: Int) =
+ UserModelIntent.AddAccountIntent(this, index).forward()
+
+fun AccountModel.toRemoveIntent() =
+ UserModelIntent.RemoveAccountIntent(this.id).forward()
+
+fun AccountModel.toRenameIntent(newName: String) =
+ renameAccount(newName).toUserModelIntent(this).forward()
+
+fun renameAccount(newName: String) =
+ AccountModelIntent.Rename(newName)
+
+fun AccountModelIntent.toUserModelIntent(account: AccountModel) =
+ UserModelIntent.ForwardAccountModelIntent(account.id, this)
diff --git a/account/src/main/java/io/github/novacrypto/account/ErrorModel.kt b/account/src/main/java/io/github/novacrypto/account/ErrorModel.kt
new file mode 100644
index 0000000..576e823
--- /dev/null
+++ b/account/src/main/java/io/github/novacrypto/account/ErrorModel.kt
@@ -0,0 +1,28 @@
+/*
+ * NovaWallet, Cryptocurrency Wallet for Android
+ * Copyright (C) 2018 Alan Evans, NovaCrypto
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Original source: https://github.com/NovaCrypto/NovaWallet
+ * You can contact the authors via github issues.
+ */
+
+package io.github.novacrypto.account
+
+data class ErrorModel(
+ val errorMessage: String
+)
+
+class UserReportableException(message: String) : Exception(message)
diff --git a/account/src/main/java/io/github/novacrypto/account/UserModel.kt b/account/src/main/java/io/github/novacrypto/account/UserModel.kt
new file mode 100644
index 0000000..b135bba
--- /dev/null
+++ b/account/src/main/java/io/github/novacrypto/account/UserModel.kt
@@ -0,0 +1,37 @@
+/*
+ * NovaWallet, Cryptocurrency Wallet for Android
+ * Copyright (C) 2018 Alan Evans, NovaCrypto
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Original source: https://github.com/NovaCrypto/NovaWallet
+ * You can contact the authors via github issues.
+ */
+
+package io.github.novacrypto.account
+
+data class UserModel(
+ val accounts: List = emptyList()
+)
+
+sealed class UserModelIntent {
+ class AddAccountIntent(val account: AccountModel, val index: Int? = null) : UserModelIntent()
+ class RemoveAccountIntent(val accountId: AccountId) : UserModelIntent()
+ class ForwardAccountModelIntent(
+ val accountId: AccountId,
+ val intent: AccountModelIntent
+ ) : UserModelIntent()
+}
+
+fun UserModelIntent.forward() = UserModelViewStateIntent.ForwardUserModelIntent(this)
diff --git a/account/src/main/java/io/github/novacrypto/account/UserModelViewState.kt b/account/src/main/java/io/github/novacrypto/account/UserModelViewState.kt
new file mode 100644
index 0000000..28ff510
--- /dev/null
+++ b/account/src/main/java/io/github/novacrypto/account/UserModelViewState.kt
@@ -0,0 +1,110 @@
+/*
+ * NovaWallet, Cryptocurrency Wallet for Android
+ * Copyright (C) 2018 Alan Evans, NovaCrypto
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Original source: https://github.com/NovaCrypto/NovaWallet
+ * You can contact the authors via github issues.
+ */
+
+package io.github.novacrypto.account
+
+import io.reactivex.Observable
+
+data class UserModelViewState(
+ val userModel: UserModel = UserModel(),
+ val error: ErrorModel? = null,
+ val undoModel: UserModelViewStateUndoModel? = null
+)
+
+data class UserModelViewStateUndoModel(
+ val message: String,
+ val undoIntent: UserModelViewStateIntent
+)
+
+fun Observable.toUserModelStream(): Observable {
+ return this.scan(UserModelViewState(),
+ { model: UserModelViewState, intent: UserModelViewStateIntent ->
+ model.transformWithIntent(intent)
+ })
+}
+
+private fun UserModelViewState.transformWithIntent(intent: UserModelViewStateIntent): UserModelViewState {
+ return when (intent) {
+ is UserModelViewStateIntent.DismissError -> copy(error = null)
+ is UserModelViewStateIntent.ForwardUserModelIntent ->
+ try {
+ val (newModel, undoModel) = userModel.transformWithIntent(intent.userModelIntent)
+ copy(userModel = newModel, undoModel = undoModel)
+ } catch (e: UserReportableException) {
+ copy(error = ErrorModel(e.message ?: ""))
+ }
+ is UserModelViewStateIntent.Undo ->
+ undoModel?.undoIntent?.let {
+ transformWithIntent(it
+ )
+ } ?: this
+ }
+}
+
+private fun UserModel.transformWithIntent(intent: UserModelIntent) =
+ when (intent) {
+ is UserModelIntent.AddAccountIntent -> {
+ copy(accounts = if (intent.index != null) {
+ accounts.insert(intent.account, intent.index)
+ } else {
+ accounts + intent.account
+ }).withNoUndo()
+ }
+ is UserModelIntent.RemoveAccountIntent -> {
+ withAccountAndIndexForId(intent.accountId) { account, index ->
+ Pair(copy(accounts = accounts - account),
+ UserModelViewStateUndoModel("Removed account '${account.name}'",
+ account.toInsertIntent(index)))
+ }
+ }
+ is UserModelIntent.ForwardAccountModelIntent -> {
+ withAccountAndIndexForId(intent.accountId) { account, index ->
+ val newAccounts = accounts.replace(index, accountModelReducer(intent.intent, account))
+ copy(accounts = newAccounts).withNoUndo()
+ }
+ }
+ }
+
+private fun UserModel.withAccountAndIndexForId(accountId: AccountId, transform: (account: AccountModel, index: Int) -> T): T {
+ val accountIdx = accounts.indexOfFirst { it.id == accountId }.also {
+ if (it == -1) throw cannotFind(accountId)
+ }
+ val existingAccount = accounts[accountIdx]
+ return transform(existingAccount, accountIdx)
+}
+
+private fun UserModel.withNoUndo() = Pair(this, null)
+
+private inline fun List.copyAndModify(modify: MutableList.() -> Unit): List =
+ toMutableList().apply(modify)
+
+private fun List.insert(newItem: E, index: Int) =
+ copyAndModify {
+ this.add(index, newItem)
+ }
+
+private fun List.replace(index: Int, replacement: E) =
+ copyAndModify {
+ this[index] = replacement
+ }
+
+private fun cannotFind(accountId: AccountId) =
+ UserReportableException("Cannot find account $accountId")
diff --git a/account/src/main/java/io/github/novacrypto/account/UserModelViewStateIntents.kt b/account/src/main/java/io/github/novacrypto/account/UserModelViewStateIntents.kt
new file mode 100644
index 0000000..1586e98
--- /dev/null
+++ b/account/src/main/java/io/github/novacrypto/account/UserModelViewStateIntents.kt
@@ -0,0 +1,32 @@
+/*
+ * NovaWallet, Cryptocurrency Wallet for Android
+ * Copyright (C) 2018 Alan Evans, NovaCrypto
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Original source: https://github.com/NovaCrypto/NovaWallet
+ * You can contact the authors via github issues.
+ */
+
+package io.github.novacrypto.account
+
+sealed class UserModelViewStateIntent {
+ class DismissError : UserModelViewStateIntent()
+ class Undo : UserModelViewStateIntent()
+ class ForwardUserModelIntent(
+ val userModelIntent: UserModelIntent
+ ) : UserModelViewStateIntent()
+}
+
+fun dismissError() = UserModelViewStateIntent.DismissError()
diff --git a/account/src/test/java/io/github/novacrypto/AccountModelTests.kt b/account/src/test/java/io/github/novacrypto/AccountModelTests.kt
new file mode 100644
index 0000000..ee0098e
--- /dev/null
+++ b/account/src/test/java/io/github/novacrypto/AccountModelTests.kt
@@ -0,0 +1,67 @@
+/*
+ * NovaWallet, Cryptocurrency Wallet for Android
+ * Copyright (C) 2018 Alan Evans, NovaCrypto
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Original source: https://github.com/NovaCrypto/NovaWallet
+ * You can contact the authors via github issues.
+ */
+
+package io.github.novacrypto
+
+import io.github.novacrypto.account.AccountModel
+import io.github.novacrypto.account.renameAccount
+import io.github.novacrypto.account.toAccountModelStream
+import io.reactivex.Observable
+import org.amshove.kluent.`should be`
+import org.amshove.kluent.`should equal`
+import org.amshove.kluent.`should not equal`
+import org.junit.Test
+
+class AccountModelTests {
+
+ @Test
+ fun `new account gets new id`() {
+ AccountModel().id `should not equal` AccountModel().id
+ }
+
+}
+
+class AccountModelRenameIntentTests {
+
+ @Test
+ fun `user can rename account`() {
+ Observable.just(
+ renameAccount("My Account")
+ ).toAccountModelStream()
+ .assertWithLastElement {
+ it.name `should equal` "My Account"
+ }
+ }
+
+ @Test
+ fun `user can rename account twice`() {
+ Observable.just(
+ renameAccount("My Account"),
+ renameAccount("My Account 2")
+ ).toAccountModelStream()
+ .assertWithLastElement {
+ it.name `should equal` "My Account 2"
+ }
+ .assertWithLastTwoElements { penultimate, ultimate ->
+ penultimate.id `should be` ultimate.id
+ }
+ }
+}
\ No newline at end of file
diff --git a/account/src/test/java/io/github/novacrypto/RxTestUtils.kt b/account/src/test/java/io/github/novacrypto/RxTestUtils.kt
new file mode 100644
index 0000000..ad6f0ea
--- /dev/null
+++ b/account/src/test/java/io/github/novacrypto/RxTestUtils.kt
@@ -0,0 +1,59 @@
+/*
+ * NovaWallet, Cryptocurrency Wallet for Android
+ * Copyright (C) 2018 Alan Evans, NovaCrypto
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Original source: https://github.com/NovaCrypto/NovaWallet
+ * You can contact the authors via github issues.
+ */
+
+package io.github.novacrypto
+
+import io.reactivex.Observable
+import org.amshove.kluent.`should be`
+
+fun Observable.assertWithLastElement(function: (T) -> Unit): Observable {
+ this.lastOrError()
+ .test()
+ .assertValue {
+ function(it)
+ true
+ }
+ return this
+}
+
+fun Observable.assertWithLastTwoElements(
+ function: (penultimate: T, ultimate: T) -> Unit
+) {
+ this.takeLast(2)
+ .test()
+ .values()
+ .also {
+ it.size `should be` 2
+ function(it[0], it[1])
+ }
+}
+
+fun Observable.assertWithLastAndThirdToLastElements(
+ function: (lastMinusTwo: T, last: T) -> Unit
+) {
+ this.takeLast(3)
+ .test()
+ .values()
+ .also {
+ it.size `should be` 3
+ function(it[0], it[2])
+ }
+}
diff --git a/account/src/test/java/io/github/novacrypto/UserModelIntentTests.kt b/account/src/test/java/io/github/novacrypto/UserModelIntentTests.kt
new file mode 100644
index 0000000..afcc137
--- /dev/null
+++ b/account/src/test/java/io/github/novacrypto/UserModelIntentTests.kt
@@ -0,0 +1,349 @@
+/*
+ * NovaWallet, Cryptocurrency Wallet for Android
+ * Copyright (C) 2018 Alan Evans, NovaCrypto
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Original source: https://github.com/NovaCrypto/NovaWallet
+ * You can contact the authors via github issues.
+ */
+
+package io.github.novacrypto
+
+import io.github.novacrypto.account.*
+import io.reactivex.Observable
+import org.amshove.kluent.`should be`
+import org.amshove.kluent.`should equal`
+import org.amshove.kluent.`should not be`
+import org.junit.Test
+
+class UserModelIntentTests {
+
+ @Test
+ fun `initial user state`() {
+ Observable.empty()
+ .toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts `should equal` emptyList()
+ }
+ }
+
+ @Test
+ fun `user can add an account`() {
+ val account = AccountModel()
+ Observable.just(account.toAddIntent())
+ .toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts `should equal` listOf(account)
+ }
+ }
+
+ @Test
+ fun `user can add two accounts`() {
+ val account1 = AccountModel()
+ val account2 = AccountModel()
+ Observable.just(account1.toAddIntent(), account2.toAddIntent())
+ .toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts `should equal` listOf(account1, account2)
+ }
+ }
+}
+
+class UserModelRemoveAccountIntentTests {
+
+ @Test
+ fun `user can remove only account`() {
+ val account = AccountModel()
+ Observable.just(
+ account.toAddIntent(),
+ account.toRemoveIntent()
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts `should equal` emptyList()
+ }
+ }
+
+ @Test
+ fun `user can remove only account after rename`() {
+ val account = AccountModel()
+ Observable.just(
+ account.toAddIntent(),
+ account.toRenameIntent("Account 1"),
+ account.toRemoveIntent()
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts `should equal` emptyList()
+ }
+ }
+
+ @Test
+ fun `user can't remove account not added`() {
+ val account = AccountModel()
+ Observable.just(
+ account.toRemoveIntent()
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.error!!.errorMessage `should equal` "Cannot find account " + account.id
+ }
+ }
+
+ @Test
+ fun `user can remove first of two accounts`() {
+ val account1 = AccountModel()
+ val account2 = AccountModel()
+ Observable.just(
+ account1.toAddIntent(),
+ account2.toAddIntent(),
+ account1.toRemoveIntent()
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts `should equal` listOf(account2)
+ }
+ }
+
+ @Test
+ fun `user can remove second of two accounts`() {
+ val account1 = AccountModel()
+ val account2 = AccountModel()
+ Observable.just(
+ account1.toAddIntent(),
+ account2.toAddIntent(),
+ account2.toRemoveIntent()
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts `should equal` listOf(account1)
+ }
+ }
+}
+
+class AccountModelRenameUserIntentTests {
+
+ @Test
+ fun `user can rename account in user model`() {
+ val account = AccountModel()
+ Observable.just(
+ account.toAddIntent(),
+ account.toRenameIntent("My Account")
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts.single().name `should equal` "My Account"
+ }
+ }
+
+ @Test
+ fun `user can rename twos account in the user model`() {
+ val account1 = AccountModel()
+ val account2 = AccountModel()
+ Observable.just(
+ account1.toAddIntent(),
+ account2.toAddIntent(),
+ account1.toRenameIntent("My Account 1"),
+ account2.toRenameIntent("My Account 2")
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts.size `should equal` 2
+ it.userModel.accounts[0].name `should equal` "My Account 1"
+ it.userModel.accounts[1].name `should equal` "My Account 2"
+ }
+ }
+
+ @Test
+ fun `user can rename twos account in the user model in reverse order`() {
+ val account1 = AccountModel()
+ val account2 = AccountModel()
+ Observable.just(
+ account1.toAddIntent(),
+ account2.toAddIntent(),
+ account2.toRenameIntent("My Account 2"),
+ account1.toRenameIntent("My Account 1")
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts.size `should equal` 2
+ it.userModel.accounts[0].name `should equal` "My Account 1"
+ it.userModel.accounts[1].name `should equal` "My Account 2"
+ }
+ }
+
+ @Test
+ fun `user can rename one account twice`() {
+ val account1 = AccountModel()
+ Observable.just(
+ account1.toAddIntent(),
+ account1.toRenameIntent("My Account A"),
+ account1.toRenameIntent("My Account B")
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts.single().name `should equal` "My Account B"
+ }
+ }
+
+ @Test
+ fun `user cant rename account not in list yet`() {
+ val account1 = AccountModel()
+ Observable.just(
+ account1.toRenameIntent("My Account A"),
+ account1.toAddIntent()
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.userModel.accounts.single().name `should equal` ""
+ }
+ }
+
+ @Test
+ fun `rename of missing account results in error`() {
+ val account = AccountModel()
+ Observable.just(
+ account.toRenameIntent("My Account A")
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.error!!.errorMessage `should equal` "Cannot find account " + account.id
+ }
+ }
+
+ @Test
+ fun `second error replaces first`() {
+ val account1 = AccountModel()
+ val account2 = AccountModel()
+ Observable.just(
+ account1.toRenameIntent("My Account A"),
+ account2.toRenameIntent("My Account A")
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.error!!.errorMessage `should equal` "Cannot find account " + account2.id
+ }
+ }
+
+ @Test
+ fun `error is not automatically dismissed by another action`() {
+ val account = AccountModel()
+ Observable.just(
+ account.toRenameIntent("My Account A"),
+ account.toAddIntent()
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.error `should not be` null
+ }
+ }
+
+ @Test
+ fun `error should keep the previous state`() {
+ val account = AccountModel()
+ val account2 = AccountModel()
+ Observable.just(
+ account.toAddIntent(),
+ account2.toRenameIntent("My Account A")
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.error `should not be` null
+ }
+ .assertWithLastTwoElements { penultimate, ultimate ->
+ penultimate.userModel `should be` ultimate.userModel
+ }
+ }
+
+ @Test
+ fun `user can dismiss error`() {
+ val account = AccountModel()
+ val account2 = AccountModel()
+ Observable.just(
+ account.toAddIntent(),
+ account2.toRenameIntent("My Account A"),
+ dismissError()
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.error `should be` null
+ }
+ .assertWithLastAndThirdToLastElements { lastMinusTwo, last ->
+ lastMinusTwo.userModel `should be` last.userModel
+ }
+ }
+
+}
+
+class UserModelUndo {
+
+ @Test
+ fun `user gets an undo message`() {
+ val account = AccountModel()
+ Observable.just(
+ account.toAddIntent(),
+ account.toRenameIntent("My Account A"),
+ account.toRemoveIntent()
+ ).toUserModelStream()
+ .assertWithLastElement {
+ it.undoModel!!.message `should equal` "Removed account 'My Account A'"
+ }
+ }
+
+ @Test
+ fun `user can undo a remove`() {
+ val account = AccountModel()
+ Observable.just(
+ account.toAddIntent(),
+ account.toRenameIntent("My Account A"),
+ account.toRemoveIntent(),
+ undo()
+ ).toUserModelStream()
+ .assertWithLastAndThirdToLastElements { lastMinusTwo, last ->
+ lastMinusTwo.userModel.accounts `should equal` last.userModel.accounts
+ }
+ }
+
+ @Test
+ fun `an undone remove is in the same position it was before (index 0)`() {
+ val account1 = AccountModel()
+ val account2 = AccountModel()
+ Observable.just(
+ account1.toAddIntent(),
+ account2.toAddIntent(),
+ account1.toRemoveIntent(),
+ undo()
+ ).toUserModelStream()
+ .assertWithLastAndThirdToLastElements { lastMinusTwo, last ->
+ lastMinusTwo.userModel.accounts `should equal` last.userModel.accounts
+ }
+ }
+
+ @Test
+ fun `an undone remove is in the same position it was before (index 1)`() {
+ val account1 = AccountModel()
+ val account2 = AccountModel()
+ val account3 = AccountModel()
+ Observable.just(
+ account1.toAddIntent(),
+ account2.toAddIntent(),
+ account3.toAddIntent(),
+ account2.toRemoveIntent(),
+ undo()
+ ).toUserModelStream()
+ .assertWithLastAndThirdToLastElements { lastMinusTwo, last ->
+ lastMinusTwo.userModel.accounts `should equal` last.userModel.accounts
+ }
+ }
+
+ @Test
+ fun `if there is nothing to undo, undo does nothing`() {
+ val account = AccountModel()
+ Observable.just(
+ account.toAddIntent(),
+ undo()
+ ).toUserModelStream()
+ .assertWithLastTwoElements { penultimate, ultimate ->
+ penultimate `should be` ultimate
+ }
+ }
+}
+
+private fun undo(): UserModelViewStateIntent = UserModelViewStateIntent.Undo()
\ No newline at end of file