diff --git a/core/usecases/src/main/kotlin/theme/addOppositionToValueWeb/AddOppositionToValueWeb.kt b/core/usecases/src/main/kotlin/theme/addOppositionToValueWeb/AddOppositionToValueWeb.kt index 765ac1484..c6659f6e3 100644 --- a/core/usecases/src/main/kotlin/theme/addOppositionToValueWeb/AddOppositionToValueWeb.kt +++ b/core/usecases/src/main/kotlin/theme/addOppositionToValueWeb/AddOppositionToValueWeb.kt @@ -33,7 +33,7 @@ interface AddOppositionToValueWeb { val characterIncludedInTheme: CharacterIncludedInTheme? ) - interface OutputPort { + fun interface OutputPort { suspend fun addedOppositionToValueWeb(response: ResponseModel) } diff --git a/core/usecases/src/main/kotlin/theme/addValueWebToTheme/AddValueWebToTheme.kt b/core/usecases/src/main/kotlin/theme/addValueWebToTheme/AddValueWebToTheme.kt index 6de2339bf..237ce93bb 100644 --- a/core/usecases/src/main/kotlin/theme/addValueWebToTheme/AddValueWebToTheme.kt +++ b/core/usecases/src/main/kotlin/theme/addValueWebToTheme/AddValueWebToTheme.kt @@ -21,7 +21,7 @@ interface AddValueWebToTheme { val symbolicItemAdded: SymbolicRepresentationAddedToOpposition? ) - interface OutputPort { + fun interface OutputPort { suspend fun addedValueWebToTheme(response: ResponseModel) } } \ No newline at end of file diff --git a/core/usecases/src/main/kotlin/theme/listAvailableOppositionValuesForCharacterInTheme/ListAvailableOppositionValuesForCharacterInTheme.kt b/core/usecases/src/main/kotlin/theme/listAvailableOppositionValuesForCharacterInTheme/ListAvailableOppositionValuesForCharacterInTheme.kt index f0caad1e9..fc613ee6e 100644 --- a/core/usecases/src/main/kotlin/theme/listAvailableOppositionValuesForCharacterInTheme/ListAvailableOppositionValuesForCharacterInTheme.kt +++ b/core/usecases/src/main/kotlin/theme/listAvailableOppositionValuesForCharacterInTheme/ListAvailableOppositionValuesForCharacterInTheme.kt @@ -6,7 +6,7 @@ interface ListAvailableOppositionValuesForCharacterInTheme { suspend operator fun invoke(themeId: UUID, characterId: UUID, output: OutputPort) - interface OutputPort { + fun interface OutputPort { suspend fun availableOppositionValuesListedForCharacterInTheme(response: OppositionValuesAvailableForCharacterInTheme) } diff --git a/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addOppositionToValueWeb/AddOppositionToValueWebController.kt b/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addOppositionToValueWeb/AddOppositionToValueWebController.kt index 5a3026d5a..bbd7e5126 100644 --- a/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addOppositionToValueWeb/AddOppositionToValueWebController.kt +++ b/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addOppositionToValueWeb/AddOppositionToValueWebController.kt @@ -1,10 +1,14 @@ package com.soyle.stories.theme.addOppositionToValueWeb import com.soyle.stories.domain.validation.NonBlankString +import com.soyle.stories.usecase.theme.addOppositionToValueWeb.OppositionAddedToValueWeb +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job interface AddOppositionToValueWebController { - fun addOpposition(valueWebId: String) - fun addOppositionWithCharacter(valueWebId: String, name: NonBlankString, characterId: String) + fun addOpposition(valueWebId: String): Deferred + fun addOpposition(valueWebId: String, name: NonBlankString): Deferred + fun addOppositionWithCharacter(valueWebId: String, name: NonBlankString, characterId: String): Deferred } \ No newline at end of file diff --git a/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addOppositionToValueWeb/AddOppositionToValueWebControllerImpl.kt b/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addOppositionToValueWeb/AddOppositionToValueWebControllerImpl.kt index dc886848c..c390466b9 100644 --- a/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addOppositionToValueWeb/AddOppositionToValueWebControllerImpl.kt +++ b/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addOppositionToValueWeb/AddOppositionToValueWebControllerImpl.kt @@ -4,7 +4,11 @@ import com.soyle.stories.common.ThreadTransformer import com.soyle.stories.domain.validation.NonBlankString import com.soyle.stories.usecase.theme.addOppositionToValueWeb.AddOppositionToValueWeb import com.soyle.stories.usecase.theme.addOppositionToValueWeb.AddOppositionToValueWeb.RequestModel +import com.soyle.stories.usecase.theme.addOppositionToValueWeb.OppositionAddedToValueWeb import com.soyle.stories.usecase.theme.addSymbolicItemToOpposition.CharacterId +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job import java.util.* class AddOppositionToValueWebControllerImpl( @@ -13,29 +17,41 @@ class AddOppositionToValueWebControllerImpl( private val addOppositionToValueWebOutputPort: AddOppositionToValueWeb.OutputPort ) : AddOppositionToValueWebController { - override fun addOpposition(valueWebId: String) { + override fun addOpposition(valueWebId: String): Deferred { val request = RequestModel( UUID.fromString(valueWebId) ) - addOppositionToValueWeb(request) + return addOppositionToValueWeb(request) } - override fun addOppositionWithCharacter(valueWebId: String, name: NonBlankString, characterId: String) { + override fun addOpposition(valueWebId: String, name: NonBlankString): Deferred { + val request = RequestModel( + UUID.fromString(valueWebId), + name, + null + ) + return addOppositionToValueWeb(request) + } + + override fun addOppositionWithCharacter(valueWebId: String, name: NonBlankString, characterId: String): Deferred { val request = RequestModel( UUID.fromString(valueWebId), name, CharacterId(UUID.fromString(characterId)) ) - addOppositionToValueWeb(request) + return addOppositionToValueWeb(request) } - private fun addOppositionToValueWeb(requestModel: RequestModel) + private fun addOppositionToValueWeb(requestModel: RequestModel): Deferred { + val deferred = CompletableDeferred() threadTransformer.async { - addOppositionToValueWeb.invoke( - requestModel, addOppositionToValueWebOutputPort - ) + addOppositionToValueWeb.invoke(requestModel) { + deferred.complete(it.oppositionAddedToValueWeb) + addOppositionToValueWebOutputPort.addedOppositionToValueWeb(it) + } } + return deferred } } \ No newline at end of file diff --git a/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addValueWebToTheme/AddValueWebToThemeController.kt b/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addValueWebToTheme/AddValueWebToThemeController.kt index 42e35d3b8..f7aaebc6e 100644 --- a/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addValueWebToTheme/AddValueWebToThemeController.kt +++ b/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addValueWebToTheme/AddValueWebToThemeController.kt @@ -1,10 +1,12 @@ package com.soyle.stories.theme.addValueWebToTheme import com.soyle.stories.domain.validation.NonBlankString +import com.soyle.stories.usecase.theme.addValueWebToTheme.ValueWebAddedToTheme +import kotlinx.coroutines.Deferred interface AddValueWebToThemeController { - fun addValueWebToTheme(themeId: String, name: NonBlankString, onError: (Throwable) -> Unit) - fun addValueWebToThemeWithCharacter(themeId: String, name: NonBlankString, characterId: String, onError: (Throwable) -> Unit) + fun addValueWebToTheme(themeId: String, name: NonBlankString, onError: (Throwable) -> Unit): Deferred + fun addValueWebToThemeWithCharacter(themeId: String, name: NonBlankString, characterId: String, onError: (Throwable) -> Unit): Deferred } \ No newline at end of file diff --git a/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addValueWebToTheme/AddValueWebToThemeControllerImpl.kt b/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addValueWebToTheme/AddValueWebToThemeControllerImpl.kt index 546c1cd4f..b534a88a4 100644 --- a/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addValueWebToTheme/AddValueWebToThemeControllerImpl.kt +++ b/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/addValueWebToTheme/AddValueWebToThemeControllerImpl.kt @@ -5,6 +5,9 @@ import com.soyle.stories.domain.validation.NonBlankString import com.soyle.stories.usecase.theme.addSymbolicItemToOpposition.CharacterId import com.soyle.stories.usecase.theme.addValueWebToTheme.AddValueWebToTheme import com.soyle.stories.usecase.theme.addValueWebToTheme.AddValueWebToTheme.RequestModel +import com.soyle.stories.usecase.theme.addValueWebToTheme.ValueWebAddedToTheme +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import java.util.* class AddValueWebToThemeControllerImpl( @@ -13,18 +16,18 @@ class AddValueWebToThemeControllerImpl( private val addValueWebToThemeOutputPort: AddValueWebToTheme.OutputPort ) : AddValueWebToThemeController { - override fun addValueWebToTheme(themeId: String, name: NonBlankString, onError: (Throwable) -> Unit) { + override fun addValueWebToTheme( + themeId: String, + name: NonBlankString, + onError: (Throwable) -> Unit + ): Deferred { val preparedThemeId = UUID.fromString(themeId) - threadTransformer.async { - try { - addValueWebToTheme.invoke( - RequestModel( - preparedThemeId, - name - ), addValueWebToThemeOutputPort - ) - } catch (t: Throwable) { onError(t) } - } + return addValueWebToTheme( + RequestModel( + preparedThemeId, + name + ) + ) } override fun addValueWebToThemeWithCharacter( @@ -32,20 +35,27 @@ class AddValueWebToThemeControllerImpl( name: NonBlankString, characterId: String, onError: (Throwable) -> Unit - ) { + ): Deferred { val preparedThemeId = UUID.fromString(themeId) val preparedCharacterId = UUID.fromString(characterId) + return addValueWebToTheme( + RequestModel( + preparedThemeId, + name, + CharacterId(preparedCharacterId) + ) + ) + } + + private fun addValueWebToTheme(request: RequestModel): Deferred { + val deferred = CompletableDeferred() threadTransformer.async { - try { - addValueWebToTheme.invoke( - RequestModel( - preparedThemeId, - name, - CharacterId(preparedCharacterId) - ), addValueWebToThemeOutputPort - ) - } catch (t: Throwable) { onError(t) } + addValueWebToTheme(request) { + deferred.complete(it.addedValueWeb) + addValueWebToThemeOutputPort.addedValueWebToTheme(it) + } } + return deferred } } \ No newline at end of file diff --git a/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/valueWeb/opposition/list/ListAvailableOppositionValuesForCharacterInThemeController.kt b/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/valueWeb/opposition/list/ListAvailableOppositionValuesForCharacterInThemeController.kt new file mode 100644 index 000000000..3e962880d --- /dev/null +++ b/desktop/adapters/src/main/kotlin/com/soyle/stories/theme/valueWeb/opposition/list/ListAvailableOppositionValuesForCharacterInThemeController.kt @@ -0,0 +1,38 @@ +package com.soyle.stories.theme.valueWeb.opposition.list + +import com.soyle.stories.common.ThreadTransformer +import com.soyle.stories.domain.character.Character +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.ListAvailableOppositionValuesForCharacterInTheme +import kotlinx.coroutines.Job + +interface ListAvailableOppositionValuesForCharacterInThemeController { + companion object { + + operator fun invoke( + threadTransformer: ThreadTransformer, + getAvailableOppositionValuesForCharacterInTheme: ListAvailableOppositionValuesForCharacterInTheme + ): ListAvailableOppositionValuesForCharacterInThemeController = + object : ListAvailableOppositionValuesForCharacterInThemeController { + override fun listAvailableOppositionValuesForCharacter( + themeId: Theme.Id, + characterId: Character.Id, + output: ListAvailableOppositionValuesForCharacterInTheme.OutputPort + ): Job { + return threadTransformer.async { + getAvailableOppositionValuesForCharacterInTheme( + themeId.uuid, + characterId.uuid, + output + ) + } + } + } + } + + fun listAvailableOppositionValuesForCharacter( + themeId: Theme.Id, + characterId: Character.Id, + output: ListAvailableOppositionValuesForCharacterInTheme.OutputPort + ): Job +} \ No newline at end of file diff --git a/desktop/locale/src/main/kotlin/SoyleMessageBundle.kt b/desktop/locale/src/main/kotlin/SoyleMessageBundle.kt index aefea9f89..8ab7d200d 100644 --- a/desktop/locale/src/main/kotlin/SoyleMessageBundle.kt +++ b/desktop/locale/src/main/kotlin/SoyleMessageBundle.kt @@ -33,6 +33,11 @@ interface SoyleMessageBundle { val pleaseProvideALocationName: String val replaceWith: String val removeFromScene: String + val addValue: String + val createNewValueWeb: String + val createOppositionValue: String + val themeHasNoValueWebs: String + val nameCannotBeBlank: String sealed class MessageSegment { abstract val message: String diff --git a/desktop/locale/src/main/kotlin/en/EnglishMessages.kt b/desktop/locale/src/main/kotlin/en/EnglishMessages.kt index badad01bd..23a41296e 100644 --- a/desktop/locale/src/main/kotlin/en/EnglishMessages.kt +++ b/desktop/locale/src/main/kotlin/en/EnglishMessages.kt @@ -47,4 +47,9 @@ object EnglishMessages : SoyleMessageBundle { override val pleaseProvideALocationName: String = "Please provide a location name." override val removeFromScene: String = "Remove From Scene" override val replaceWith: String = "Replace With..." + override val addValue: String = "Add Value" + override val createNewValueWeb: String = "Create New Value Web" + override val createOppositionValue: String = "Create Opposition Value" + override val themeHasNoValueWebs: String = "Theme Has No Value Webs" + override val nameCannotBeBlank: String = "Name Cannot Be Blank" } \ No newline at end of file diff --git a/desktop/src/main/kotlin/locale/LocaleHolder.kt b/desktop/src/main/kotlin/locale/LocaleHolder.kt index 6993ba2fb..663c925c3 100644 --- a/desktop/src/main/kotlin/locale/LocaleHolder.kt +++ b/desktop/src/main/kotlin/locale/LocaleHolder.kt @@ -9,12 +9,16 @@ import com.soyle.stories.scene.setting.SceneSettingToolLocale import com.soyle.stories.scene.setting.list.SceneSettingItemListLocale import com.soyle.stories.scene.setting.list.item.SceneSettingItemLocale import com.soyle.stories.scene.setting.list.useLocationButton.UseLocationButtonLocale +import com.soyle.stories.theme.characterValueComparison.components.addValueButton.AddValueButtonLocale +import com.soyle.stories.theme.valueWeb.create.CreateValueWebFormLocale +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueFormLocale import javafx.beans.value.ObservableValue import javafx.scene.Parent import tornadofx.* class LocaleHolder(bundle: SoyleMessageBundle) : LocationDetailsLocale, SceneSettingToolLocale, - SceneSettingItemListLocale, SceneSettingItemLocale, UseLocationButtonLocale, CreateLocationDialogLocale { + SceneSettingItemListLocale, SceneSettingItemLocale, UseLocationButtonLocale, CreateLocationDialogLocale, AddValueButtonLocale, + CreateOppositionValueFormLocale, CreateValueWebFormLocale { private val currentLocale = objectProperty(bundle) @@ -81,6 +85,11 @@ class LocaleHolder(bundle: SoyleMessageBundle) : LocationDetailsLocale, SceneSet override val pleaseProvideALocationName: ObservableValue = currentLocale.stringBinding { it!!.pleaseProvideALocationName } override val removeFromScene: ObservableValue = currentLocale.stringBinding { it!!.removeFromScene } override val replaceWith: ObservableValue = currentLocale.stringBinding { it!!.replaceWith } + override val addValue: ObservableValue = currentLocale.stringBinding { it!!.addValue } + override val createNewValueWeb: ObservableValue = currentLocale.stringBinding { it!!.createNewValueWeb } + override val createOppositionValue: ObservableValue = currentLocale.stringBinding { it!!.createOppositionValue } + override val themeHasNoValueWebs: ObservableValue = currentLocale.stringBinding { it!!.themeHasNoValueWebs } + override val nameCannotBeBlank: ObservableValue = currentLocale.stringBinding { it!!.nameCannotBeBlank } private fun createDynamicMessage(parent: Parent, segment: SoyleMessageBundle.MessageSegment) { when (segment) { diff --git a/desktop/src/main/kotlin/theme/Presentation.kt b/desktop/src/main/kotlin/theme/Presentation.kt index 957acd59c..fd7c06cba 100644 --- a/desktop/src/main/kotlin/theme/Presentation.kt +++ b/desktop/src/main/kotlin/theme/Presentation.kt @@ -3,11 +3,22 @@ package com.soyle.stories.desktop.config.theme import com.soyle.stories.characterarc.moveCharacterArcSectionInMoralArgument.CharacterArcSectionMovedInMoralArgumentNotifier import com.soyle.stories.characterarc.removeCharacterArcSectionFromMoralArgument.CharacterArcSectionRemovedNotifier import com.soyle.stories.common.listensTo +import com.soyle.stories.desktop.config.locale.LocaleHolder import com.soyle.stories.di.get import com.soyle.stories.di.scoped +import com.soyle.stories.domain.character.Character +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.domain.theme.valueWeb.ValueWeb import com.soyle.stories.layout.config.dynamic.MoralArgument +import com.soyle.stories.project.ProjectScope import com.soyle.stories.theme.addCharacterArcSectionToMoralArgument.ArcSectionAddedToCharacterArcNotifier +import com.soyle.stories.theme.characterValueComparison.CharacterValueComparisonScope +import com.soyle.stories.theme.characterValueComparison.components.addValueButton.AddValueButton import com.soyle.stories.theme.moralArgument.* +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueForm +import com.soyle.stories.usecase.theme.addOppositionToValueWeb.OppositionAddedToValueWeb +import com.soyle.stories.usecase.theme.addValueWebToTheme.ValueWebAddedToTheme object Presentation { @@ -44,6 +55,55 @@ object Presentation { ) } } + scoped { + provide { + object : CreateValueWebForm.Factory { + override fun invoke( + themeId: Theme.Id, + onCreateValueWeb: suspend (ValueWebAddedToTheme) -> Unit + ): CreateValueWebForm { + return CreateValueWebForm( + themeId, + onCreateValueWeb, + applicationScope.get(), + get() + ) + } + } + } + provide { + object : CreateOppositionValueForm.Factory { + override fun invoke( + valueWebId: ValueWeb.Id, + onCreateOppositionValue: suspend (OppositionAddedToValueWeb) -> Unit + ): CreateOppositionValueForm { + return CreateOppositionValueForm( + valueWebId, + onCreateOppositionValue, + applicationScope.get(), + get() + ) + } + } + } + } + scoped { + provide { + object : AddValueButton.Factory { + override fun invoke(themeId: Theme.Id, characterId: Character.Id): AddValueButton { + return AddValueButton( + themeId, + characterId, + projectScope.applicationScope.get(), + projectScope.get(), + projectScope.get(), + projectScope.get(), + projectScope.get() + ) + } + } + } + } } } \ No newline at end of file diff --git a/desktop/src/main/kotlin/theme/UseCases.kt b/desktop/src/main/kotlin/theme/UseCases.kt index 4387e7004..bdbcd1de7 100644 --- a/desktop/src/main/kotlin/theme/UseCases.kt +++ b/desktop/src/main/kotlin/theme/UseCases.kt @@ -12,7 +12,10 @@ import com.soyle.stories.theme.changeThemeDetails.changeThematicRevelation.Chang import com.soyle.stories.theme.changeThemeDetails.changeThemeLine.ChangeThemeLineController import com.soyle.stories.theme.changeThemeDetails.changeThemeLine.ChangeThemeLineControllerImpl import com.soyle.stories.theme.removeSymbolFromTheme.RemoveSymbolFromThemeOutput +import com.soyle.stories.theme.valueWeb.opposition.list.ListAvailableOppositionValuesForCharacterInThemeController import com.soyle.stories.usecase.theme.changeThemeDetails.* +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.ListAvailableOppositionValuesForCharacterInTheme +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.ListAvailableOppositionValuesForCharacterInThemeUseCase import com.soyle.stories.usecase.theme.outlineMoralArgument.GetMoralArgumentFrame import com.soyle.stories.usecase.theme.outlineMoralArgument.OutlineMoralArgument import com.soyle.stories.usecase.theme.outlineMoralArgument.OutlineMoralArgumentForCharacterInTheme @@ -68,11 +71,23 @@ object UseCases { applicationScope.get(), get(), get() ) } - + listAvailableOppositionValuesForCharacterInTheme() removeSymbolFromTheme() } } + private fun InProjectScope.listAvailableOppositionValuesForCharacterInTheme() + { + provide { + ListAvailableOppositionValuesForCharacterInThemeUseCase( + get() + ) + } + provide { + ListAvailableOppositionValuesForCharacterInThemeController(applicationScope.get(), get()) + } + } + private fun InProjectScope.removeSymbolFromTheme() { provide { RemoveSymbolFromThemeUseCase(get(), get()) } diff --git a/desktop/src/test/kotlin/drivers/theme/Character Value Comparison Robot.kt b/desktop/src/test/kotlin/drivers/theme/Character Value Comparison Robot.kt index 82429b1a5..dcf1ad85d 100644 --- a/desktop/src/test/kotlin/drivers/theme/Character Value Comparison Robot.kt +++ b/desktop/src/test/kotlin/drivers/theme/Character Value Comparison Robot.kt @@ -2,9 +2,12 @@ package com.soyle.stories.desktop.config.drivers.theme import com.soyle.stories.characterarc.createCharacterDialog.CreateCharacterForm import com.soyle.stories.desktop.config.drivers.character.getCreateCharacterDialogOrError +import com.soyle.stories.desktop.config.drivers.robot import com.soyle.stories.desktop.view.theme.characterComparison.`Character Card View Access`.Companion.access +import com.soyle.stories.desktop.view.theme.characterComparison.addValueButton.`Add Value Button Access`.Companion.access import com.soyle.stories.desktop.view.theme.characterComparison.`Character Comparison View Access`.Companion.access import com.soyle.stories.desktop.view.theme.characterComparison.`Character Comparison View Access`.Companion.drive +import com.soyle.stories.desktop.view.theme.valueWeb.create.getOpenCreateValueWebDialog import com.soyle.stories.domain.character.Character import com.soyle.stories.domain.theme.Theme import com.soyle.stories.domain.theme.oppositionValue.OppositionValue @@ -16,6 +19,8 @@ import com.soyle.stories.theme.characterValueComparison.CharacterValueComparison import com.soyle.stories.theme.createOppositionValueDialog.CreateOppositionValueDialog import com.soyle.stories.theme.createValueWebDialog.CreateValueWebDialog import com.soyle.stories.theme.themeList.ThemeList +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueForm import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.fail @@ -77,43 +82,41 @@ fun CharacterValueComparison.givenCreateCharacterDialogHasBeenOpened(): CreateCh return getCreateCharacterDialogOrError() } -fun CharacterValueComparison.givenCreateOppositionValueDialogHasBeenOpenedFor(valueWebId: ValueWeb.Id): CreateOppositionValueDialog +fun CharacterValueComparison.givenCreateOppositionValueDialogHasBeenOpenedFor(valueWebId: ValueWeb.Id): CreateOppositionValueForm { drive { characterCards.single { it.access().addValueButton.isShowing } - .access { - addValueButton.getCreateOppositionValueItem(valueWebId)!!.fire() + .access().addValueButton.access { + getValueWebItem(valueWebId)!!.createOppositionValueItem.fire() } } return getCreateOppositionValueDialogOrError() } -fun CharacterValueComparison.givenCreateValueWebDialogHasBeenOpened(): CreateValueWebDialog +fun CharacterValueComparison.givenCreateValueWebDialogHasBeenOpened(): CreateValueWebForm { drive { characterCards.single { it.access().addValueButton.isShowing } - .access { - addValueButton.getCreateValueWebItem()!!.fire() - } + .access().addValueButton.access().createValueWebItem!!.fire() } - return getCreateValueWebDialog() ?: fail("Create Value Web Dialog was not opened") + return robot.getOpenCreateValueWebDialog() ?: fail("Create Value Web Dialog was not opened") } -fun CharacterValueComparison.givenOppositionValueUsedForCharacter(characterId: Character.Id, oppositionValue: OppositionValue): CharacterValueComparison +fun CharacterValueComparison.givenOppositionValueUsedForCharacter(characterId: Character.Id, valueWeb: ValueWeb, oppositionValue: OppositionValue): CharacterValueComparison { if (access().getCharacterCard(characterId)!!.access().getValue(oppositionValue.id) == null) { loadAvailableValuesFor(characterId) - selectOppositionValue(oppositionValue) + selectOppositionValue(valueWeb, oppositionValue) } return this } -fun CharacterValueComparison.selectOppositionValue(oppositionValue: OppositionValue) +fun CharacterValueComparison.selectOppositionValue(valueWeb: ValueWeb, oppositionValue: OppositionValue) { drive { characterCards.single { it.access().addValueButton.isShowing } - .access { - addValueButton.getOppositionValueItem(oppositionValue.id)!!.fire() + .access().addValueButton.access { + getValueWebItem(valueWeb.id)!!.getOppositionValueItem(oppositionValue.id)!!.fire() } } } \ No newline at end of file diff --git a/desktop/src/test/kotlin/drivers/theme/Create Opposition Value Dialog Robot.kt b/desktop/src/test/kotlin/drivers/theme/Create Opposition Value Dialog Robot.kt index 4b6ce632a..839f6fd5a 100644 --- a/desktop/src/test/kotlin/drivers/theme/Create Opposition Value Dialog Robot.kt +++ b/desktop/src/test/kotlin/drivers/theme/Create Opposition Value Dialog Robot.kt @@ -3,16 +3,16 @@ package com.soyle.stories.desktop.config.drivers.theme import com.soyle.stories.desktop.config.drivers.robot import com.soyle.stories.desktop.view.project.workbench.getOpenDialog import com.soyle.stories.theme.createOppositionValueDialog.CreateOppositionValueDialog -import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.`Create Opposition Value Dialog Access`.Companion.drive +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.`Create Opposition Value Form Access`.Companion.drive +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.getOpenCreateOppositionValueDialog +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueForm import javafx.event.ActionEvent import org.junit.jupiter.api.fail -fun getCreateOppositionValueDialogOrError(): CreateOppositionValueDialog = - getCreateOppositionValueDialog() ?: fail("Create Opposition Value Dialog is not open") +fun getCreateOppositionValueDialogOrError(): CreateOppositionValueForm = + robot.getOpenCreateOppositionValueDialog() ?: fail("Create Opposition Value Dialog is not open") -fun getCreateOppositionValueDialog(): CreateOppositionValueDialog? = robot.getOpenDialog() - -fun CreateOppositionValueDialog.createOppositionValueNamed(name: String) +fun CreateOppositionValueForm.createOppositionValueNamed(name: String) { drive { nameInput.requestFocus() diff --git a/desktop/src/test/kotlin/drivers/theme/Create Value Web Dialog Driver.kt b/desktop/src/test/kotlin/drivers/theme/Create Value Web Dialog Driver.kt index 10aafc6fc..bece57bcc 100644 --- a/desktop/src/test/kotlin/drivers/theme/Create Value Web Dialog Driver.kt +++ b/desktop/src/test/kotlin/drivers/theme/Create Value Web Dialog Driver.kt @@ -5,15 +5,16 @@ import com.soyle.stories.desktop.view.theme.createValueWebDialog.CreateValueWebD import com.soyle.stories.desktop.view.theme.oppositionWebTool.ValueOppositionWebDriver import com.soyle.stories.theme.createValueWebDialog.CreateValueWebDialog import com.soyle.stories.theme.themeOppositionWebs.ValueOppositionWebs +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import com.soyle.stories.desktop.view.theme.valueWeb.create.`Create Value Web Form Access`.Companion.drive +import com.soyle.stories.desktop.view.theme.valueWeb.create.getOpenCreateValueWebDialog import javafx.event.ActionEvent import tornadofx.uiComponent -fun getCreateValueWebDialog(): CreateValueWebDialog? = - robot.listWindows().asSequence() - .mapNotNull { it.scene.root.uiComponent() } - .firstOrNull { it.currentStage?.isShowing == true } +fun getCreateValueWebDialog(): CreateValueWebForm? = + robot.getOpenCreateValueWebDialog() -fun ValueOppositionWebs.openCreateValueWebDialog(): CreateValueWebDialog +fun ValueOppositionWebs.openCreateValueWebDialog(): CreateValueWebForm { val driver = ValueOppositionWebDriver(this) val createButton = driver.getCreateValueWebButton() @@ -21,11 +22,9 @@ fun ValueOppositionWebs.openCreateValueWebDialog(): CreateValueWebDialog return getCreateValueWebDialog() ?: error("Value Opposition Web tool did not properly open Create Value Web Dialog") } -fun CreateValueWebDialog.createValueWebNamed(valueWebName: String) +fun CreateValueWebForm.createValueWebNamed(valueWebName: String) { - val driver = CreateValueWebDialogDriver(this) - val nameInput = driver.getNameInput() - driver.interact { + drive { nameInput.text = valueWebName nameInput.fireEvent(ActionEvent()) } diff --git a/desktop/src/test/kotlin/features/theme/Character in Theme Steps.kt b/desktop/src/test/kotlin/features/theme/Character in Theme Steps.kt index 534f9116d..4abc066f3 100644 --- a/desktop/src/test/kotlin/features/theme/Character in Theme Steps.kt +++ b/desktop/src/test/kotlin/features/theme/Character in Theme Steps.kt @@ -84,7 +84,7 @@ class `Character in Theme Steps` : En { soyleStories.getAnyOpenWorkbenchOrError() .givenThemeListToolHasBeenOpened() .givenCharacterComparisonToolHasBeenOpenedFor(theme.id) - .givenOppositionValueUsedForCharacter(character.id, oppositionValue) + .givenOppositionValueUsedForCharacter(character.id, valueWeb, oppositionValue) } } @@ -157,7 +157,7 @@ class `Character in Theme Steps` : En { .givenThemeListToolHasBeenOpened() .givenCharacterComparisonToolHasBeenOpenedFor(theme.id) .givenAvailableValuesHaveBeenLoadedFor(character.id) - .selectOppositionValue(oppositionValue) + .selectOppositionValue(valueWeb, oppositionValue) } When( "I create an opposition value named {string} in the {theme}'s {string} value web to add to the {character}" diff --git a/desktop/views/src/design/kotlin/theme/characterValueComparison/Add Value Button Design.kt b/desktop/views/src/design/kotlin/theme/characterValueComparison/Add Value Button Design.kt new file mode 100644 index 000000000..2556e4f0d --- /dev/null +++ b/desktop/views/src/design/kotlin/theme/characterValueComparison/Add Value Button Design.kt @@ -0,0 +1,106 @@ +package com.soyle.stories.desktop.view.theme.characterValueComparison + +import com.soyle.stories.desktop.view.testframework.DesignTest +import com.soyle.stories.desktop.view.testframework.State +import com.soyle.stories.desktop.view.theme.characterComparison.addValueButton.AddValueButtonFactory +import com.soyle.stories.desktop.view.theme.characterComparison.addValueButton.`Add Value Button Access`.Companion.access +import com.soyle.stories.desktop.view.theme.characterComparison.doubles.ListAvailableOppositionValuesForCharacterInThemeControllerDouble +import com.soyle.stories.domain.character.Character +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.theme.characterValueComparison.components.addValueButton.AddValueButton +import com.soyle.stories.theme.valueWeb.opposition.list.ListAvailableOppositionValuesForCharacterInThemeController +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.AvailableOppositionValueForCharacterInTheme +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.AvailableValueWebForCharacterInTheme +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.ListAvailableOppositionValuesForCharacterInTheme +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.OppositionValuesAvailableForCharacterInTheme +import com.soyle.stories.usecase.theme.listOppositionsInValueWeb.OppositionValueItem +import javafx.scene.Node +import kotlinx.coroutines.runBlocking +import java.util.* + +class `Add Value Button Design` : DesignTest() { + + private var loadAvailableOppositionsOutput: ListAvailableOppositionValuesForCharacterInTheme.OutputPort? = null + + private val factory = AddValueButtonFactory() + override val node: AddValueButton + get() = factory.invoke(Theme.Id(), Character.Id()) + + @State + fun `default loading`() = verifyDesign() + + @State + fun `no available value webs`() { + factory.listAvailableOppositionValuesForCharacterInThemeController = + ListAvailableOppositionValuesForCharacterInThemeControllerDouble( + onInvoke = { _, _, output -> + runBlocking { + output.availableOppositionValuesListedForCharacterInTheme( + OppositionValuesAvailableForCharacterInTheme( + UUID.randomUUID(), + UUID.randomUUID(), + emptyList() + ) + ) + } + } + ) + verifyDesign() + } + + @State + fun `available value webs`() { + factory.listAvailableOppositionValuesForCharacterInThemeController = + ListAvailableOppositionValuesForCharacterInThemeControllerDouble( + onInvoke = { _, _, output -> + runBlocking { + output.availableOppositionValuesListedForCharacterInTheme( + OppositionValuesAvailableForCharacterInTheme(UUID.randomUUID(), UUID.randomUUID(), List(5) { + AvailableValueWebForCharacterInTheme( + UUID.randomUUID(), + "Value Web ${it + 1}", + null, + List(5) { + AvailableOppositionValueForCharacterInTheme( + UUID.randomUUID(), + "Opposition Value ${it + 1}" + ) + }) + }) + ) + } + } + ) + verifyDesign() + } + + @State + fun `available value webs and opposition value used from value web`() { + factory.listAvailableOppositionValuesForCharacterInThemeController = + ListAvailableOppositionValuesForCharacterInThemeControllerDouble( + onInvoke = { _, _, output -> + runBlocking { + output.availableOppositionValuesListedForCharacterInTheme( + OppositionValuesAvailableForCharacterInTheme(UUID.randomUUID(), UUID.randomUUID(), List(5) { + val oppositionValues = List(5) { + AvailableOppositionValueForCharacterInTheme( + UUID.randomUUID(), + "Opposition Value ${it + 1}" + ) + } + val oppositionValueUsed = oppositionValues.random().oppositionValueId + AvailableValueWebForCharacterInTheme( + UUID.randomUUID(), + "Value Web ${it + 1}", + OppositionValueItem(oppositionValueUsed, ""), + oppositionValues + ) + }) + ) + } + } + ) + verifyDesign() + } + +} \ No newline at end of file diff --git a/desktop/views/src/design/kotlin/theme/valueWeb/Create Value Web Form Design.kt b/desktop/views/src/design/kotlin/theme/valueWeb/Create Value Web Form Design.kt new file mode 100644 index 000000000..67a0ec8f7 --- /dev/null +++ b/desktop/views/src/design/kotlin/theme/valueWeb/Create Value Web Form Design.kt @@ -0,0 +1,44 @@ +package com.soyle.stories.desktop.view.theme.valueWeb + +import com.soyle.stories.desktop.view.testframework.DesignTest +import com.soyle.stories.desktop.view.testframework.State +import com.soyle.stories.desktop.view.theme.valueWeb.create.CreateValueWebFormLocaleMock +import com.soyle.stories.desktop.view.theme.valueWeb.create.`Create Value Web Form Access`.Companion.access +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import javafx.scene.Node + +class `Create Value Web Form Design` : DesignTest() { + + private var onCreateNode: CreateValueWebForm.() -> Unit = {} + + override val node: CreateValueWebForm + get() = CreateValueWebForm( + Theme.Id(), + {}, + CreateValueWebFormLocaleMock(), + AddValueWebToThemeControllerDouble() + ).apply(onCreateNode) + + @State + fun `default`() = verifyDesign() + + @State + fun `executing`() { + onCreateNode = { + access().nameInput.text = "Banana" + tryToCreateValueWeb() + } + verifyDesign() + } + + @State + fun `failure`() { + onCreateNode = { + tryToCreateValueWeb() + } + verifyDesign() + } + + +} \ No newline at end of file diff --git a/desktop/views/src/design/kotlin/theme/valueWeb/opposition/Create Opposition Value Form Design.kt b/desktop/views/src/design/kotlin/theme/valueWeb/opposition/Create Opposition Value Form Design.kt new file mode 100644 index 000000000..9a013afea --- /dev/null +++ b/desktop/views/src/design/kotlin/theme/valueWeb/opposition/Create Opposition Value Form Design.kt @@ -0,0 +1,39 @@ +package com.soyle.stories.desktop.view.theme.valueWeb.opposition + +import com.soyle.stories.desktop.view.testframework.DesignTest +import com.soyle.stories.desktop.view.testframework.State +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.AddOppositionToValueWebControllerDouble +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.CreateOppositionValueFormLocaleMock +import com.soyle.stories.domain.theme.valueWeb.ValueWeb +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueForm +import javafx.scene.Node +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async + +class `Create Opposition Value Form Design` : DesignTest() { + + private val addOppositionToValueWeb = AddOppositionToValueWebControllerDouble() + + override val node: Node + get() = CreateOppositionValueForm( + ValueWeb.Id(), + {}, + CreateOppositionValueFormLocaleMock(), + addOppositionToValueWeb + ) + + @State + fun `default`() = verifyDesign() + + @State + fun `error`() { + addOppositionToValueWeb.onAddOpposition = { a, b, c -> + CoroutineScope(Dispatchers.Main).async { + throw Error("Failed to do the thing") + } + } + + verifyDesign() + } +} \ No newline at end of file diff --git a/desktop/views/src/main/kotlin/com/soyle/stories/di/theme/ThemeModule.kt b/desktop/views/src/main/kotlin/com/soyle/stories/di/theme/ThemeModule.kt index bb2b940ac..a139a9cd0 100644 --- a/desktop/views/src/main/kotlin/com/soyle/stories/di/theme/ThemeModule.kt +++ b/desktop/views/src/main/kotlin/com/soyle/stories/di/theme/ThemeModule.kt @@ -224,11 +224,6 @@ object ThemeModule { get() ) } - provide { - ListAvailableOppositionValuesForCharacterInThemeUseCase( - get() - ) - } provide { ExamineCentralConflictOfThemeUseCase(get(), get()) } provide { ListAvailablePerspectiveCharactersUseCase(get()) } provide( diff --git a/desktop/views/src/main/kotlin/com/soyle/stories/theme/characterValueComparison/components/CharacterCard.kt b/desktop/views/src/main/kotlin/com/soyle/stories/theme/characterValueComparison/components/CharacterCard.kt index e11f82b47..a0a258b4d 100644 --- a/desktop/views/src/main/kotlin/com/soyle/stories/theme/characterValueComparison/components/CharacterCard.kt +++ b/desktop/views/src/main/kotlin/com/soyle/stories/theme/characterValueComparison/components/CharacterCard.kt @@ -10,11 +10,15 @@ import com.soyle.stories.common.components.ComponentsStyles.Companion.discourage import com.soyle.stories.common.components.ComponentsStyles.Companion.noDisableStyle import com.soyle.stories.common.components.ComponentsStyles.Companion.noSelectionMenuItem import com.soyle.stories.di.get +import com.soyle.stories.di.resolve import com.soyle.stories.di.resolveLater +import com.soyle.stories.domain.character.Character +import com.soyle.stories.domain.theme.Theme import com.soyle.stories.theme.characterValueComparison.CharacterComparedWithValuesViewModel import com.soyle.stories.theme.characterValueComparison.CharacterValueComparisonModel import com.soyle.stories.theme.characterValueComparison.CharacterValueComparisonViewListener import com.soyle.stories.theme.characterValueComparison.CharacterValueViewModel +import com.soyle.stories.theme.characterValueComparison.components.addValueButton.AddValueButton import com.soyle.stories.theme.createOppositionValueDialog.CreateOppositionValueDialog import com.soyle.stories.theme.createValueWebDialog.CreateValueWebDialog import de.jensd.fx.glyphs.materialicons.MaterialIcon @@ -26,11 +30,13 @@ import javafx.scene.control.Tooltip import javafx.scene.layout.Region import javafx.util.Duration import tornadofx.* +import java.util.* class CharacterCard : ItemFragment() { private val viewListener by resolveLater() private val model by resolveLater() + private val makeAddValueButton = resolve() private val viewModel = ItemViewModel() @@ -107,99 +113,20 @@ class CharacterCard : ItemFragment() { style { fontSize = 1.2.em } } spacer() - menubutton { - id = "add-value-button" - this.textProperty().bind(addValueButtonLabelProperty) - val loadingItem = item("Loading...") { - isDisable = true - } - val createValueWebItem = MenuItem("[Create New Value Web]").apply { - id = "create-value-web" - action { - val characterId = itemProperty.get()?.characterId ?: return@action - model.scope.projectScope.get().showToAutoLinkCharacter( - model.scope.type.themeId.toString(), - characterId, - currentWindow - ) - } - } - contextmenu { - item("Yeah, fuck you") - } - model.availableOppositionValues.onChange { - items.clear() - when { - it == null -> items.add(loadingItem) - it.isEmpty() -> { - items.add(createValueWebItem) - item("No available values") { - isDisable = true - } - } - else -> { - items.add(createValueWebItem) - it.forEach { availableValueWeb -> - item(availableValueWeb.label) { - addClass(noDisableStyle, noSelectionMenuItem, contextMenuSectionHeaderItem) - isDisable = true - } - val discourageTooltip = availableValueWeb.preSelectedOppositionValue?.let { - Tooltip(""" - ${item.characterName} already represents the ${it.label} value for - the ${availableValueWeb.label} value web. Selecting this value - will replace ${it.label}. - """.trimIndent() - ).apply { - style { fontSize = 1.em } - showDelay = Duration.seconds(0.0) - hideDelay = Duration.seconds(0.0) - } - } - customitem { - id = "create-opposition-value-${availableValueWeb.valueWebId}" - addClass(contextMenuSectionedItem) - availableValueWeb.preSelectedOppositionValue?.let { - addClass(discouragedSelection) - } - label("[Create New Opposition Value]") { - tooltip = discourageTooltip - } - action { - val characterId = itemProperty.get()?.characterId ?: return@action - model.scope.projectScope.get().showToAutoLinkCharacter( - availableValueWeb.valueWebId, - characterId, - currentWindow - ) - } - } - availableValueWeb.availableOppositions.forEach { - customitem { - id = it.oppositionId - addClass(contextMenuSectionedItem) - availableValueWeb.preSelectedOppositionValue?.let { - addClass(discouragedSelection) - } - label(it.label) { - tooltip = discourageTooltip - } - action { - val characterId = itemProperty.get()?.characterId ?: return@action - viewListener.selectOppositionValueForCharacter(characterId, it.oppositionId) - } - } - } - } - } - } - } - setOnShowing { _ -> - val characterId = itemProperty.get()?.characterId ?: return@setOnShowing - viewListener.getAvailableOppositionValues(characterId) - } - setOnHidden { - model.availableOppositionValues.value = null + val characterIdProperty = itemProperty.stringBinding { it?.characterId } + val addValueButtonProperty = characterIdProperty.objectBinding { + if (it != null) { + makeAddValueButton( + Theme.Id(model.scope.type.themeId), + Character.Id(UUID.fromString(it)) + ) + } else null + } + addValueButtonProperty.addListener { observable, oldValue, newValue -> + if (oldValue != null && newValue != null) oldValue.replaceWith(newValue) + else { + oldValue?.removeFromParent() + newValue?.let { add(it) } } } } diff --git a/desktop/views/src/main/kotlin/com/soyle/stories/theme/characterValueComparison/components/addValueButton/AddValueButton.kt b/desktop/views/src/main/kotlin/com/soyle/stories/theme/characterValueComparison/components/addValueButton/AddValueButton.kt new file mode 100644 index 000000000..c7e62d369 --- /dev/null +++ b/desktop/views/src/main/kotlin/com/soyle/stories/theme/characterValueComparison/components/addValueButton/AddValueButton.kt @@ -0,0 +1,201 @@ +package com.soyle.stories.theme.characterValueComparison.components.addValueButton + +import com.soyle.stories.common.components.ComponentsStyles +import com.soyle.stories.common.components.buttons.ButtonStyles +import com.soyle.stories.common.guiUpdate +import com.soyle.stories.domain.character.Character +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.domain.theme.oppositionValue.OppositionValue +import com.soyle.stories.domain.theme.valueWeb.ValueWeb +import com.soyle.stories.theme.addSymbolicItemToOpposition.AddSymbolicItemToOppositionController +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueForm +import com.soyle.stories.theme.valueWeb.opposition.list.ListAvailableOppositionValuesForCharacterInThemeController +import com.soyle.stories.usecase.theme.addSymbolicItemToOpposition.AddSymbolicItemToOpposition +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.AvailableOppositionValueForCharacterInTheme +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.AvailableValueWebForCharacterInTheme +import javafx.application.Platform +import javafx.scene.Scene +import javafx.scene.control.Menu +import javafx.scene.control.MenuButton +import javafx.scene.control.MenuItem +import javafx.scene.control.RadioButton +import javafx.stage.Stage +import tornadofx.* +import java.util.* + +class AddValueButton( + // input props + private val themeId: Theme.Id, + private val characterId: Character.Id, + // locale + private val locale: AddValueButtonLocale, + // controllers + private val getAvailableOppositionValues: ListAvailableOppositionValuesForCharacterInThemeController, + private val addSymbolicItemToOpposition: AddSymbolicItemToOppositionController, + // other components + private val makeCreateValueWebForm: CreateValueWebForm.Factory, + private val makeCreateOppositionValueWebForm: CreateOppositionValueForm.Factory +) : MenuButton() { + + interface Factory { + + operator fun invoke( + themeId: Theme.Id, + characterId: Character.Id, + ): AddValueButton + } + + /* * * * * * * * * * * */ + // region Initialization + /* * * * * * * * * * * */ + + // initialize style + init { + addClass(Styles.addValueButton) + addClass(ButtonStyles.noArrow) + addClass(ComponentsStyles.outlined) + addClass(ComponentsStyles.primary) + addClass(ComponentsStyles.loading) + } + + // initialize properties + init { + textProperty().bind(locale.addValue) + } + + // initialize behavior + init { + setOnShowing { reloadOppositionValues() } + } + + //endregion + + /* * * * * * * */ + // region Items + /* * * * * * * */ + + private fun loadingItem(): MenuItem = MenuItem().apply { + id = "loading" + + isDisable = true + textProperty().bind(locale.loading) + } + + private fun createValueWebItem(): MenuItem = MenuItem().apply { + id = "create-value-web" + + textProperty().bind(locale.createNewValueWeb) + action { openCreateValueWebDialog() } + } + + private fun noAvailableValueWebsItem(): MenuItem = MenuItem().apply { + id = "no-available-value-webs" + + isDisable = true + textProperty().bind(locale.themeHasNoValueWebs) + } + + private fun availableValueWebItem(availableValueWeb: AvailableValueWebForCharacterInTheme): MenuItem = + Menu(availableValueWeb.valueWebName).apply { + id = ValueWeb.Id(availableValueWeb.valueWebId).toString() + addClass(Styles.availableValueWebItem) + + items.add(createOppositionValueItem(ValueWeb.Id(availableValueWeb.valueWebId))) + items.addAll(availableValueWeb.map { + availableOppositionValueItem(it, availableValueWeb.oppositionCharacterRepresents?.oppositionValueId) + }) + } + + private fun availableOppositionValueItem( + availableOppositionValue: AvailableOppositionValueForCharacterInTheme, + selectedOppositionValueId: UUID? + ): MenuItem { + return MenuItem(availableOppositionValue.oppositionValueName).apply { + id = OppositionValue.Id(availableOppositionValue.oppositionValueId).toString() + addClass(Styles.availableOppositionItem) + + graphic = RadioButton().apply { + isSelected = availableOppositionValue.oppositionValueId == selectedOppositionValueId + } + + action { applyOppositionValueToCharacter(availableOppositionValue.oppositionValueId) } + } + } + + private fun createOppositionValueItem(valueWebId: ValueWeb.Id): MenuItem = MenuItem("", RadioButton()).apply { + id = "create-opposition-value" + + textProperty().bind(locale.createOppositionValue) + + action { openCreateOppositionValueDialog(valueWebId) } + } + + // endregion + + /* * * * * * * * * */ + // region Behaviors + /* * * * * * * * * */ + + private fun reloadOppositionValues() { + toggleClass(ComponentsStyles.loading, true) + items.setAll(loadingItem()) + getAvailableOppositionValues.listAvailableOppositionValuesForCharacter(themeId, characterId) { + guiUpdate { + toggleClass(ComponentsStyles.loading, false) + items.setAll(createValueWebItem()) + if (it.isEmpty()) items.add(noAvailableValueWebsItem()) + items.addAll(it.map(::availableValueWebItem)) + } + } + } + + private fun openCreateValueWebDialog() { + Stage().apply { + scene = Scene(makeCreateValueWebForm(themeId) { + guiUpdate { close() } + applyOppositionValueToCharacter(it.oppositionAddedToValueWeb.oppositionValueId) + }) + show() + } + } + + private fun openCreateOppositionValueDialog(valueWebId: ValueWeb.Id) { + Stage().apply { + scene = Scene(makeCreateOppositionValueWebForm(valueWebId) { + guiUpdate { close() } + applyOppositionValueToCharacter(it.oppositionValueId) + }) + show() + } + } + + private fun applyOppositionValueToCharacter(oppositionValueId: UUID) { + addSymbolicItemToOpposition.addCharacterToOpposition(oppositionValueId.toString(), characterId.uuid.toString()) + } + + // endregion + + /* * * * * * * * * * * * * */ + // region Style Definitions + /* * * * * * * * * * * * * */ + + override fun getUserAgentStylesheet(): String = Styles().externalForm + + class Styles : Stylesheet() { + companion object { + + val addValueButton by cssclass() + val availableValueWebItem by cssclass() + val availableOppositionItem by cssclass() + + init { + if (Platform.isFxApplicationThread()) importStylesheet() + else runLater { importStylesheet() } + } + } + } + + // endregion + +} \ No newline at end of file diff --git a/desktop/views/src/main/kotlin/com/soyle/stories/theme/characterValueComparison/components/addValueButton/AddValueButtonLocale.kt b/desktop/views/src/main/kotlin/com/soyle/stories/theme/characterValueComparison/components/addValueButton/AddValueButtonLocale.kt new file mode 100644 index 000000000..0bcf473ec --- /dev/null +++ b/desktop/views/src/main/kotlin/com/soyle/stories/theme/characterValueComparison/components/addValueButton/AddValueButtonLocale.kt @@ -0,0 +1,11 @@ +package com.soyle.stories.theme.characterValueComparison.components.addValueButton + +import javafx.beans.value.ObservableValue + +interface AddValueButtonLocale { + val addValue: ObservableValue + val loading: ObservableValue + val createNewValueWeb: ObservableValue + val themeHasNoValueWebs: ObservableValue + val createOppositionValue: ObservableValue +} \ No newline at end of file diff --git a/desktop/views/src/main/kotlin/com/soyle/stories/theme/themeOppositionWebs/ValueOppositionWebs.kt b/desktop/views/src/main/kotlin/com/soyle/stories/theme/themeOppositionWebs/ValueOppositionWebs.kt index be4e4a6b5..a143fa497 100644 --- a/desktop/views/src/main/kotlin/com/soyle/stories/theme/themeOppositionWebs/ValueOppositionWebs.kt +++ b/desktop/views/src/main/kotlin/com/soyle/stories/theme/themeOppositionWebs/ValueOppositionWebs.kt @@ -5,6 +5,7 @@ import com.soyle.stories.common.components.emptyListDisplay import com.soyle.stories.common.onChangeUntil import com.soyle.stories.di.get import com.soyle.stories.di.resolve +import com.soyle.stories.domain.theme.Theme import com.soyle.stories.domain.validation.NonBlankString import com.soyle.stories.theme.createValueWebDialog.CreateValueWebDialog import com.soyle.stories.theme.deleteValueWebDialog.DeleteValueWebDialog @@ -12,6 +13,7 @@ import com.soyle.stories.theme.themeOppositionWebs.Styles.Companion.selectedItem import com.soyle.stories.theme.themeOppositionWebs.Styles.Companion.valueWebList import com.soyle.stories.theme.themeOppositionWebs.components.oppositionValueCard import com.soyle.stories.theme.valueOppositionWebs.ValueOppositionWebsViewListener +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm import de.jensd.fx.glyphs.materialicons.MaterialIcon import de.jensd.fx.glyphs.materialicons.MaterialIconView import javafx.animation.Timeline @@ -19,10 +21,13 @@ import javafx.beans.property.SimpleDoubleProperty import javafx.geometry.Insets import javafx.geometry.Pos import javafx.scene.Parent +import javafx.scene.Scene import javafx.scene.layout.ColumnConstraints import javafx.scene.layout.Priority import javafx.scene.layout.Region import javafx.scene.paint.Color +import javafx.stage.Stage +import javafx.stage.StageStyle import javafx.util.Duration import tornadofx.* import kotlin.math.max @@ -52,7 +57,7 @@ class ValueOppositionWebs : View() { "".toProperty(), "Create First Value Web".toProperty() ) { - scope.projectScope.get().show(scope.themeId.toString(), currentWindow) + openCreateValueWebDialog() } anchorpane { addClass("populated") @@ -196,7 +201,7 @@ class ValueOppositionWebs : View() { graphic = MaterialIconView(MaterialIcon.ADD, "16px") addClass("create-value-web-button") action { - scope.projectScope.get().show(scope.themeId.toString(), currentWindow) + openCreateValueWebDialog() } } } @@ -249,6 +254,16 @@ class ValueOppositionWebs : View() { } } + private fun openCreateValueWebDialog() { + val stage = Stage() + val createValueWebForm = scope.projectScope.get() + .invoke(Theme.Id(scope.themeId)) { stage.close() } + stage.initOwner(currentWindow) + stage.scene = Scene(createValueWebForm) + stage.initStyle(StageStyle.UTILITY) + stage.show() + } + companion object { const val smallBoundary = 581 const val largeBoundary = 900 diff --git a/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/create/CreateValueWebForm.kt b/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/create/CreateValueWebForm.kt new file mode 100644 index 000000000..002dafa25 --- /dev/null +++ b/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/create/CreateValueWebForm.kt @@ -0,0 +1,171 @@ +package com.soyle.stories.theme.valueWeb.create + +import com.soyle.stories.common.ViewBuilder +import com.soyle.stories.common.components.text.Caption.Companion.caption +import com.soyle.stories.common.components.text.FieldLabel.Companion.fieldLabel +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.domain.validation.NonBlankString +import com.soyle.stories.theme.addValueWebToTheme.AddValueWebToThemeController +import com.soyle.stories.usecase.theme.addValueWebToTheme.AddValueWebToTheme +import com.soyle.stories.usecase.theme.addValueWebToTheme.ValueWebAddedToTheme +import javafx.application.Platform +import javafx.beans.property.ReadOnlyBooleanProperty +import javafx.beans.property.ReadOnlyStringProperty +import javafx.scene.Parent +import javafx.scene.layout.VBox +import javafx.scene.paint.Color +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.javafx.JavaFx +import kotlinx.coroutines.launch +import tornadofx.* +import javax.swing.text.Style + +class CreateValueWebForm( + // props + private val themeId: Theme.Id, + private val onCreateValueWeb: suspend (ValueWebAddedToTheme) -> Unit, + // locale + private val locale: CreateValueWebFormLocale, + // actions + private val addValueWebToTheme: AddValueWebToThemeController +) : VBox() { + + interface Factory { + operator fun invoke( + themeId: Theme.Id, + onCreateValueWeb: suspend (ValueWebAddedToTheme) -> Unit = {} + ): CreateValueWebForm + } + + /* * * * * * * */ + // region State + /* * * * * * * */ + + private val nameProperty = stringProperty("") + private val errorMessage = stringProperty(null) + private val creatingValueWeb = booleanProperty(false) + + fun nameProperty() = nameProperty + fun errorMessageProperty(): ReadOnlyStringProperty = errorMessage + fun creatingValueWebProperty(): ReadOnlyBooleanProperty = creatingValueWeb + + // endregion + + /* * * * * * * * * * * */ + // region Initialization + /* * * * * * * * * * * */ + + // initialize styles + init { + addClass(Styles.createValueWebForm) + } + + // initialize children + init { + fieldLabel(locale.name) + vbox { + addClass(Stylesheet.field) + + nameInput() + errorCaption() + } + } + + // endregion + + /* * * * * * * * * * * */ + // region Sub Components + /* * * * * * * * * * * */ + + @ViewBuilder + private fun Parent.nameInput() = textfield { + textProperty().bindBidirectional(nameProperty) + disableWhen(creatingValueWeb) + + action { tryToCreateValueWeb(text) } + requestFocus() + } + + @ViewBuilder + private fun Parent.errorCaption() = caption() { + addClass(Stylesheet.error) + + visibleWhen(errorMessage.isNotEmpty) + textProperty().bind(errorMessage) + } + + // endregion + + /* * * * * * * * * */ + // region Behaviors + /* * * * * * * * * */ + + /** + * will attempt to create a value web with the current value of the [nameProperty]. + */ + fun tryToCreateValueWeb() { + tryToCreateValueWeb(nameProperty.value) + } + + private fun tryToCreateValueWeb(name: String) { + errorMessage.unbind() + val nonBlankName = NonBlankString.create(name) ?: return errorMessage.bind(locale.nameCannotBeBlank) + tryToCreateValueWeb(nonBlankName) + } + + private fun tryToCreateValueWeb(name: NonBlankString) { + creatingValueWeb.value = true + errorMessage.set(null) + CoroutineScope(Dispatchers.JavaFx).launch { + awaitValueWebAddedToTheme(name) + } + } + + private suspend fun awaitValueWebAddedToTheme(name: NonBlankString) { + val addedValueWebToTheme = try { + addValueWebToTheme.addValueWebToTheme(themeId.uuid.toString(), name) {}.await() + } catch (t: Throwable) { + errorMessage.set(t.localizedMessage) + return + } finally { + creatingValueWeb.value = false + } + onCreateValueWeb(addedValueWebToTheme) + nameProperty.set("") + } + + // endregion + + override fun getUserAgentStylesheet(): String = Styles().externalForm + + class Styles : Stylesheet() { + + companion object { + + val createValueWebForm by cssclass() + + init { + if (Platform.isFxApplicationThread()) importStylesheet() + else runLater { importStylesheet() } + } + } + + init { + createValueWebForm { + spacing = 8.px + + field { + spacing = 4.px + + error { + textFill = Color.RED + padding = box(0.em, 0.766666.em) + } + } + } + } + + } + +} \ No newline at end of file diff --git a/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/create/CreateValueWebFormLocale.kt b/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/create/CreateValueWebFormLocale.kt new file mode 100644 index 000000000..3716c0293 --- /dev/null +++ b/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/create/CreateValueWebFormLocale.kt @@ -0,0 +1,8 @@ +package com.soyle.stories.theme.valueWeb.create + +import javafx.beans.value.ObservableValue + +interface CreateValueWebFormLocale { + val name: ObservableValue + val nameCannotBeBlank: ObservableValue +} \ No newline at end of file diff --git a/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/opposition/create/CreateOppositionValueForm.kt b/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/opposition/create/CreateOppositionValueForm.kt new file mode 100644 index 000000000..8bacaa442 --- /dev/null +++ b/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/opposition/create/CreateOppositionValueForm.kt @@ -0,0 +1,151 @@ +package com.soyle.stories.theme.valueWeb.opposition.create + +import com.soyle.stories.common.components.text.Caption +import com.soyle.stories.common.existsWhen +import com.soyle.stories.domain.theme.valueWeb.ValueWeb +import com.soyle.stories.domain.validation.NonBlankString +import com.soyle.stories.theme.addOppositionToValueWeb.AddOppositionToValueWebController +import com.soyle.stories.usecase.theme.addOppositionToValueWeb.OppositionAddedToValueWeb +import javafx.application.Platform +import javafx.scene.Parent +import javafx.scene.control.TextField +import javafx.scene.layout.VBox +import javafx.scene.paint.Color +import kotlinx.coroutines.* +import kotlinx.coroutines.javafx.JavaFx +import tornadofx.* + +class CreateOppositionValueForm( + // props + private val valueWebId: ValueWeb.Id, + private val onCreateOppositionValue: suspend (OppositionAddedToValueWeb) -> Unit, + // locale + private val locale: CreateOppositionValueFormLocale, + // actions + private val createOppositionValue: AddOppositionToValueWebController +) : VBox() { + + interface Factory { + + operator fun invoke( + valueWebId: ValueWeb.Id, + onCreateOppositionValue: suspend (OppositionAddedToValueWeb) -> Unit = {} + ): CreateOppositionValueForm + } + + /* * * * * * * */ + // region State + /* * * * * * * */ + + private val errorMessage = stringProperty(null) + private val creatingOpposition = booleanProperty(false) + + // endregion + + /* * * * * * * * * * * */ + // region Initialization + /* * * * * * * * * * * */ + + // initialize styles + init { + addClass(Styles.createOppositionValueForm) + } + + // initialize children + init { + label(locale.name) + vbox { + addClass(Stylesheet.field) + + add(nameInput()) + add(errorLabel()) + } + } + + //endregion + + /* * * * * * * * * * * */ + // region SubComponents + /* * * * * * * * * * * */ + + private fun nameInput() = TextField().apply { + disableWhen(creatingOpposition) + action { tryToCreateOpposition(text) } + requestFocus() + } + + private fun errorLabel() = Caption().apply { + addClass(Stylesheet.error) + + visibleWhen(errorMessage.isNotEmpty) + textProperty().bind(errorMessage) + } + + //endregion + + /* * * * * * * * * */ + // region Behaviors + /* * * * * * * * * */ + + private fun tryToCreateOpposition(name: String) { + errorMessage.unbind() + val nonBlankName = NonBlankString.create(name) ?: return errorMessage.bind(locale.nameCannotBeBlank) + tryToCreateOpposition(nonBlankName) + } + + private val handleCreationFailure = CoroutineExceptionHandler { context, failure -> } + + private fun tryToCreateOpposition(nonBlankName: NonBlankString) { + creatingOpposition.set(true) + CoroutineScope(Dispatchers.JavaFx) + .launch(handleCreationFailure) { awaitOppositionCreation(nonBlankName) } + } + + private suspend fun awaitOppositionCreation(nonBlankName: NonBlankString) { + val oppositionAddedToValueWeb = try { + createOppositionValue.addOpposition(valueWebId.uuid.toString(), nonBlankName).await() + } catch (t: Throwable) { + errorMessage.set(t.localizedMessage) + return + } finally { + creatingOpposition.set(false) + } + errorMessage.set(null) + onCreateOppositionValue(oppositionAddedToValueWeb) + } + + //endregion + + override fun getUserAgentStylesheet(): String = Styles().externalForm + + class Styles : Stylesheet() { + + companion object { + + val createOppositionValueForm by cssclass() + + init { + if (Platform.isFxApplicationThread()) importStylesheet() + else runLater { importStylesheet() } + } + } + + init { + createOppositionValueForm { + spacing = 8.px + + field { + spacing = 4.px + + error { + textFill = Color.RED + padding = box(0.em, 0.766666.em) + backgroundInsets += box(0.px, 1.px) + } + } + } + } + + } + +} \ No newline at end of file diff --git a/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/opposition/create/CreateOppositionValueFormLocale.kt b/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/opposition/create/CreateOppositionValueFormLocale.kt new file mode 100644 index 000000000..f2fe2bc4e --- /dev/null +++ b/desktop/views/src/main/kotlin/com/soyle/stories/theme/valueWeb/opposition/create/CreateOppositionValueFormLocale.kt @@ -0,0 +1,8 @@ +package com.soyle.stories.theme.valueWeb.opposition.create + +import javafx.beans.value.ObservableValue + +interface CreateOppositionValueFormLocale { + val name: ObservableValue + val nameCannotBeBlank: ObservableValue +} \ No newline at end of file diff --git a/desktop/views/src/test/kotlin/theme/characterValueComparison/Add Value Button Unit Test.kt b/desktop/views/src/test/kotlin/theme/characterValueComparison/Add Value Button Unit Test.kt new file mode 100644 index 000000000..e87d8c18c --- /dev/null +++ b/desktop/views/src/test/kotlin/theme/characterValueComparison/Add Value Button Unit Test.kt @@ -0,0 +1,431 @@ +package com.soyle.stories.desktop.view.theme.characterValueComparison + +import com.soyle.stories.common.components.ComponentsStyles.Companion.loading +import com.soyle.stories.desktop.view.common.NodeTest +import com.soyle.stories.desktop.view.theme.characterComparison.addValueButton.AddValueButtonLocaleMock +import com.soyle.stories.desktop.view.theme.characterComparison.doubles.ListAvailableOppositionValuesForCharacterInThemeControllerDouble +import com.soyle.stories.domain.character.Character +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.theme.characterValueComparison.components.addValueButton.AddValueButton +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.ListAvailableOppositionValuesForCharacterInTheme +import com.soyle.stories.desktop.view.theme.characterComparison.addValueButton.`Add Value Button Access`.Companion.access +import com.soyle.stories.desktop.view.theme.characterComparison.addValueButton.`Add Value Button Access`.Companion.drive +import com.soyle.stories.desktop.view.theme.characterComparison.doubles.AddSymbolicItemToOppositionControllerDouble +import com.soyle.stories.desktop.view.theme.valueWeb.create.CreateValueWebFormFactory +import com.soyle.stories.desktop.view.theme.valueWeb.create.getOpenCreateValueWebDialog +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.CreateOppositionValueFormFactory +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.getOpenCreateOppositionValueDialog +import com.soyle.stories.domain.theme.oppositionValue.OppositionValue +import com.soyle.stories.domain.theme.valueWeb.ValueWeb +import com.soyle.stories.domain.validation.NonBlankString +import com.soyle.stories.theme.addSymbolicItemToOpposition.AddSymbolicItemToOppositionController +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueForm +import com.soyle.stories.usecase.theme.addOppositionToValueWeb.OppositionAddedToValueWeb +import com.soyle.stories.usecase.theme.addValueWebToTheme.ValueWebAddedToTheme +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.AvailableOppositionValueForCharacterInTheme +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.AvailableValueWebForCharacterInTheme +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.OppositionValuesAvailableForCharacterInTheme +import com.soyle.stories.usecase.theme.listOppositionsInValueWeb.OppositionValueItem +import javafx.scene.control.CheckBox +import javafx.scene.control.RadioButton +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import tornadofx.hasClass +import java.util.* + +class `Add Value Button Unit Test` : NodeTest() { + + private val themeId = Theme.Id() + private val characterId = Character.Id() + + private var loadAvailableOppositionsOutput: ListAvailableOppositionValuesForCharacterInTheme.OutputPort? = null + private var onCreateValueWeb: (suspend (ValueWebAddedToTheme) -> Unit)? = null + private var applyOppositionValueRequest: OppositionValue.Id? = null + private var createOppositionValueForValueWebRequestId: ValueWeb.Id? = null + private var onCreateOppositionValue: (suspend (OppositionAddedToValueWeb) -> Unit)? = null + + private val locale = AddValueButtonLocaleMock() + override val view: AddValueButton = AddValueButton( + themeId, + characterId, + locale, + ListAvailableOppositionValuesForCharacterInThemeControllerDouble( + onInvoke = { themeId, characterId, output -> + assertEquals(this.themeId, themeId) + assertEquals(this.characterId, characterId) + loadAvailableOppositionsOutput = output + } + ), + AddSymbolicItemToOppositionControllerDouble( + onAddCharacterToOpposition = { oppositionId: String, characterId: String -> + assertEquals(this.characterId.uuid.toString(), characterId) + applyOppositionValueRequest = OppositionValue.Id(UUID.fromString(oppositionId)) + }, + onAddLocationToOpposition = { _, _ -> fail("Should not have added location to opposition") }, + onAddSymbolToOpposition = { _, _ -> fail("Should not have added symbol to opposition") } + ), + CreateValueWebFormFactory( + onInvoke = { themeId, callback -> + assertEquals(this.themeId, themeId) + onCreateValueWeb = callback + } + ), + CreateOppositionValueFormFactory( + onInvoke = { valueWebId, it -> + createOppositionValueForValueWebRequestId = valueWebId + onCreateOppositionValue = it + } + ) + ) + + init { + showView() + interact { + listWindows().asSequence() + .filter { it.scene.window?.isShowing == true } + .filter { it.scene.root is CreateOppositionValueForm || it.scene.root is CreateValueWebForm } + .forEach { it.hide() } + } + } + + @Test + fun `should be loading given not yet shown`() { + assertTrue(view.hasClass(loading)) + } + + @Test + fun `should display text from locale`() { + interact { locale.addValue.set("Put a new value in here") } + assertEquals("Put a new value in here", view.text) + } + + @Test + fun `should load items when shown`() { + interact { view.fire() } + assertNotNull(loadAvailableOppositionsOutput) + + interact { locale.loading.set("Be with you in a moment") } + view.access().loadingItem!!.run { + assertTrue(isDisable) + assertEquals("Be with you in a moment", text) + } + } + + @Nested + inner class `Given was Opened` { + + init { + if (!view.isShowing) interact { view.show() } + } + + @Test + fun `should show create value web option when loaded`() { + loadAvailableValueWebs(emptyList()) + + // expected wrong thread exception, but nothing was thrown. If this test fails later, wrap in interact { } + locale.createNewValueWeb.set("Let's make another value web") + view.access().createValueWebItem!!.run { + assertEquals("Let's make another value web", text) + } + } + + @Test + fun `should not be loading when loaded`() { + loadAvailableValueWebs(emptyList()) + + assertFalse(view.hasClass(loading)) + assertNull(view.access().loadingItem) + } + + @Test + fun `should show no available value web item when loaded value webs is empty`() { + loadAvailableValueWebs(emptyList()) + + // expected wrong thread exception, but nothing was thrown. If this test fails later, wrap in interact { } + locale.themeHasNoValueWebs.set("This theme be empty, yo") + view.access().noAvailableValueWebsItem!!.run { + assertTrue(isDisable) + assertEquals("This theme be empty, yo", text) + } + } + + @Test + fun `should open create value web dialog when create value web item is selected`() { + loadAvailableValueWebs(emptyList()) + interact { view.access().createValueWebItem!!.fire() } + + assertNotNull(getOpenCreateValueWebDialog()) + assertNotNull(onCreateValueWeb) + } + + @Nested + inner class `Given Create Value Web Dialog has been Opened` { + + init { + loadAvailableValueWebs(emptyList()) + interact { view.access().createValueWebItem!!.fire() } + } + + @Test + fun `should apply first opposition value from created value web to character`() { + val expectedOppositionId = OppositionValue.Id() + valueWebAdded(createdOppositionId = expectedOppositionId.uuid) + + assertEquals(expectedOppositionId, applyOppositionValueRequest) + } + + @Test + fun `should close the create value web dialog when value web is created`() { + valueWebAdded() + + assertNull(getOpenCreateValueWebDialog()) + } + + private fun valueWebAdded(createdOppositionId: UUID = UUID.randomUUID()) { + val valueWebId = UUID.randomUUID() + runBlocking { + onCreateValueWeb!!.invoke( + ValueWebAddedToTheme( + themeId.uuid, + valueWebId, + "", + OppositionAddedToValueWeb(themeId.uuid, valueWebId, createdOppositionId, "", false) + ) + ) + } + } + } + + @Nested + inner class `Given Value Webs are Available` { + + private val valueWebs = List(5) { ValueWeb(themeId, NonBlankString.create("value web $it")!!) } + private val valueWebIdSet = valueWebs.map { it.id.toString() }.toSet() + + init { + loadAvailableValueWebs(valueWebs.map { + AvailableValueWebForCharacterInTheme( + it.id.uuid, + it.name.value, + null, + emptyList() + ) + }) + } + + @Test + fun `should show all available value webs`() { + assertNull(view.access().noAvailableValueWebsItem) + + assertEquals(5, view.access().valueWebItems.size) + assertEquals(valueWebIdSet, view.access().valueWebItems.map { it.id }.toSet()) + view.access().valueWebItems.forEach { menuItem -> + val backingValueWeb = valueWebs.single { it.id.toString() == menuItem.id } + assertEquals(backingValueWeb.name.value, menuItem.text) + } + } + + @Test + fun `should provide create opposition value option for each value web`() { + view.access { + valueWebItems.forEach { + it.createOppositionValueItem + } + } + + // expected wrong thread exception, but nothing was thrown. If this test fails later, wrap in interact { } + locale.createOppositionValue.set("Make another opposition") + view.access { + valueWebItems.forEach { + assertEquals("Make another opposition", it.createOppositionValueItem.text) + } + } + } + + @Test + fun `should open create opposition value dialog when create opposition item is selected`() { + val valueWeb = valueWebs.random() + view.drive { + valueWebItems.single { it.id == valueWeb.id.toString() }.createOppositionValueItem.fire() + } + + assertNotNull(getOpenCreateOppositionValueDialog()) + assertEquals(valueWeb.id, createOppositionValueForValueWebRequestId) + assertNotNull(onCreateOppositionValue) + } + + @Nested + inner class `Given Create Opposition Value Dialog has been Opened` { + + private val valueWeb = valueWebs.random() + + init { + view.drive { + valueWebItems.single { it.id == valueWeb.id.toString() }.createOppositionValueItem.fire() + } + } + + @Test + fun `should apply created opposition value to character`() { + val expectedOppositionId = OppositionValue.Id() + oppositionValueAdded(createdOppositionId = expectedOppositionId.uuid) + + assertEquals(expectedOppositionId, applyOppositionValueRequest) + } + + @Test + fun `should close create opposition value dialog`() { + + oppositionValueAdded(createdOppositionId = UUID.randomUUID()) + + interact { } + + assertNull(getOpenCreateOppositionValueDialog()) + } + + private fun oppositionValueAdded(createdOppositionId: UUID) { + runBlocking { + onCreateOppositionValue!!.invoke( + OppositionAddedToValueWeb( + themeId.uuid, + valueWeb.id.uuid, + createdOppositionId, + "", + false + ) + ) + } + } + } + + @Nested + inner class `Given Value Webs have Available Oppositions` { + + private val oppositionValues = valueWebs.associate { web -> + web.id to List((1..6).random()) { + OppositionValue(NonBlankString.create("Opposition Value ${web.id} $it")!!) + } + } + + init { + loadAvailableValueWebs(valueWebs.map { + AvailableValueWebForCharacterInTheme( + it.id.uuid, + it.name.value, + null, + oppositionValues.getValue(it.id).map { + AvailableOppositionValueForCharacterInTheme( + it.id.uuid, + it.name.value + ) + } + ) + }) + } + + @Test + fun `should display all available opposition values for each value web`() { + view.access { + valueWebItems.forEach { menu -> + val backingValueWeb = valueWebs.single { it.id.toString() == menu.id } + val backingOppositions = oppositionValues.getValue(backingValueWeb.id) + + assertEquals(backingOppositions.size, menu.oppositionValueItems.size) + assertEquals( + backingOppositions.map { it.id.toString() }.toSet(), + menu.oppositionValueItems.map { it.id }.toSet() + ) + menu.oppositionValueItems.forEach { menuItem -> + val backingOppositionValue = + backingOppositions.single { it.id.toString() == menuItem.id } + assertEquals(backingOppositionValue.name.value, menuItem.text) + } + } + } + } + + @Test + fun `should apply opposition value to character when available opposition value is selected`() { + val valueWeb = valueWebs.random() + val oppositionValue = oppositionValues.getValue(valueWeb.id).random() + view.drive { + valueWebItems.single { it.id == valueWeb.id.toString() } + .oppositionValueItems.single { it.id == oppositionValue.id.toString() } + .fire() + } + + assertEquals(oppositionValue.id, applyOppositionValueRequest) + } + + @Nested + inner class `Given Character Already has Opposition Value for Value Web` { + + val selectedOppositionValues = valueWebs.associate { + it.id to oppositionValues.getValue(it.id).random() + } + val selectedOppositionValueIds = selectedOppositionValues.map { it.value.id.toString() }.toSet() + + init { + loadAvailableValueWebs(valueWebs.map { + AvailableValueWebForCharacterInTheme( + it.id.uuid, + it.name.value, + selectedOppositionValues.getValue(it.id).let { + OppositionValueItem(it.id.uuid, it.name.value) + }, + oppositionValues.getValue(it.id).map { + AvailableOppositionValueForCharacterInTheme( + it.id.uuid, + it.name.value + ) + } + ) + }) + } + + @Test + fun `should show all other opposition values as unselected`() { + view.access { + valueWebItems.forEach { menu -> + menu.oppositionValueItems.filterNot { it.id in selectedOppositionValueIds } + .forEach { + assertFalse((it.graphic as RadioButton).isSelected) + } + } + } + } + + @Test + fun `should opposition value as selected`() { + view.access { + valueWebItems.forEach { menu -> + val item = menu.oppositionValueItems.single { it.id in selectedOppositionValueIds } + assertTrue((item.graphic as RadioButton).isSelected) + } + } + } + + } + + } + + } + + private fun loadAvailableValueWebs(valueWebs: List) { + runBlocking { + loadAvailableOppositionsOutput!!.availableOppositionValuesListedForCharacterInTheme( + OppositionValuesAvailableForCharacterInTheme( + themeId.uuid, + characterId.uuid, + valueWebs + ) + ) + } + } + + } + +} \ No newline at end of file diff --git a/desktop/views/src/test/kotlin/theme/valueWeb/Create Value Web Form Unit Test.kt b/desktop/views/src/test/kotlin/theme/valueWeb/Create Value Web Form Unit Test.kt new file mode 100644 index 000000000..031f21db7 --- /dev/null +++ b/desktop/views/src/test/kotlin/theme/valueWeb/Create Value Web Form Unit Test.kt @@ -0,0 +1,218 @@ +package com.soyle.stories.desktop.view.theme.valueWeb + +import com.soyle.stories.desktop.view.common.NodeTest +import com.soyle.stories.desktop.view.theme.valueWeb.create.CreateValueWebFormLocaleMock +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import com.soyle.stories.usecase.theme.addValueWebToTheme.ValueWebAddedToTheme +import com.soyle.stories.desktop.view.theme.valueWeb.create.`Create Value Web Form Access`.Companion.access +import com.soyle.stories.desktop.view.theme.valueWeb.create.`Create Value Web Form Access`.Companion.drive +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.domain.theme.valueWeb.ValueWeb +import com.soyle.stories.domain.validation.NonBlankString +import com.soyle.stories.theme.addValueWebToTheme.AddValueWebToThemeController +import com.soyle.stories.usecase.theme.addOppositionToValueWeb.OppositionAddedToValueWeb +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import javafx.event.ActionEvent +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import java.util.* + +class `Create Value Web Form Unit Test` : NodeTest() { + + private val themeId = Theme.Id() + + private val locale = CreateValueWebFormLocaleMock() + + private var valueWebAddedToTheme: ValueWebAddedToTheme? = null + private var createValueWebRequest: NonBlankString? = null + private val createValueWeb = AddValueWebToThemeControllerDouble( + onAddValueWebToTheme = { themeId, name -> + assertEquals(this.themeId.uuid.toString(), themeId) + createValueWebRequest = name + CompletableDeferred() + } + ) + + override val view: CreateValueWebForm = CreateValueWebForm( + themeId, + ::valueWebAddedToTheme::set, + locale, + createValueWeb + ) + + init { + showView() + } + + @Test + fun `should show name label when first created`() { + interact { locale.name.set("The name of the thing") } + assertEquals("The name of the thing", view.access().nameLabel.text) + } + + @Test + fun `name input should not be disabled when first created`() { + assertFalse(view.access().nameInput.isDisable) + } + + @Test + fun `name input should be focused when first created`() { + assertTrue(view.access().nameInput.isFocused) + } + + @Test + fun `should not show error message when first created`() { + assertNull(view.access().errorMessage) + } + + @Nested + inner class `When Enter Key is Pressed in the Name Input Field` { + + @Test + fun `should not disable name input`() { + view.drive { nameInput.fireEvent(ActionEvent()) } + + assertFalse(view.access().nameInput.isDisable) + } + + @Test + fun `should display name cannot be blank error`() { + view.drive { nameInput.fireEvent(ActionEvent()) } + + interact { locale.nameCannotBeBlank.set("The name can't be blank") } + assertEquals("The name can't be blank", view.access().errorMessage!!.text) + } + + @Test + fun `should not display error message when another attempt is made`() { + view.drive { nameInput.fireEvent(ActionEvent()) } + view.drive { + nameInput.text = "Banana" + nameInput.fireEvent(ActionEvent()) + } + + assertNull(view.access().errorMessage) + } + + @Nested + inner class `Given Name is not Blank` { + + init { + view.drive { nameInput.text = "Banana" } + } + + @Test + fun `should disable name input`() { + view.drive { nameInput.fireEvent(ActionEvent()) } + + assertTrue(view.access().nameInput.isDisable) + } + + @Test + fun `should generate create value web request`() { + view.drive { nameInput.fireEvent(ActionEvent()) } + + assertEquals("Banana", createValueWebRequest!!.value) + } + + @Test + fun `should not display error message yet`() { + view.drive { nameInput.fireEvent(ActionEvent()) } + + assertNull(view.access().errorMessage) + } + + @Nested + inner class `Given Value Web Successfully Created` { + + private val createdValueWebId = ValueWeb.Id() + + init { + ensureRequestSucceeds() + view.drive { nameInput.fireEvent(ActionEvent()) } + } + + @Test + fun `should send event to callback`() { + assertEquals(themeId.uuid, valueWebAddedToTheme!!.themeId) + assertEquals(createdValueWebId.uuid, valueWebAddedToTheme!!.valueWebId) + } + + @Test + fun `should clear name input and enable it`() { + assertTrue(view.access().nameInput.text.isEmpty()) + assertFalse(view.access().nameInput.isDisable) + } + + private fun ensureRequestSucceeds() { + val currentHandler = createValueWeb.onAddValueWebToTheme + createValueWeb.onAddValueWebToTheme = { themeId, name -> + currentHandler(themeId, name) + CompletableDeferred( + ValueWebAddedToTheme( + this@`Create Value Web Form Unit Test`.themeId.uuid, + createdValueWebId.uuid, + name.value, + OppositionAddedToValueWeb( + this@`Create Value Web Form Unit Test`.themeId.uuid, + createdValueWebId.uuid, + UUID.randomUUID(), + name.value, + false + ) + ) + ) + } + } + + } + + @Nested + inner class `Given Value Web Fails to be Created` { + + private val expectedErrorText = "I failed asynchronously!" + + init { + ensureRequestFails() + view.drive { nameInput.fireEvent(ActionEvent()) } + } + + @Test + fun `error message should display error text`() { + assertEquals(expectedErrorText, view.access().errorMessage!!.text) + } + + @Test + fun `name input should be enabled to resolve issue and try again`() { + assertFalse(view.access().nameInput.isDisable) + } + + @Test + fun `should not display error message when another attempt is made`() { + createValueWeb.onAddValueWebToTheme = { themeId, name -> CompletableDeferred() } + view.drive { nameInput.fireEvent(ActionEvent()) } + + assertNull(view.access().errorMessage) + } + + private fun ensureRequestFails() { + val currentHandler = createValueWeb.onAddValueWebToTheme + createValueWeb.onAddValueWebToTheme = { themeId, name -> + currentHandler(themeId, name) + CoroutineScope(Dispatchers.Main).async { + throw Error(expectedErrorText) + } + } + } + + } + + } + + } + +} \ No newline at end of file diff --git a/desktop/views/src/test/kotlin/theme/valueWeb/opposition/Create Opposition Value Form Unit Test.kt b/desktop/views/src/test/kotlin/theme/valueWeb/opposition/Create Opposition Value Form Unit Test.kt new file mode 100644 index 000000000..0ed8c4881 --- /dev/null +++ b/desktop/views/src/test/kotlin/theme/valueWeb/opposition/Create Opposition Value Form Unit Test.kt @@ -0,0 +1,179 @@ +package com.soyle.stories.desktop.view.theme.valueWeb.opposition + +import com.soyle.stories.desktop.view.common.NodeTest +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.AddOppositionToValueWebControllerDouble +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.CreateOppositionValueFormLocaleMock +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueForm +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.`Create Opposition Value Form Access`.Companion.access +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.`Create Opposition Value Form Access`.Companion.drive +import com.soyle.stories.domain.theme.valueWeb.ValueWeb +import com.soyle.stories.domain.validation.NonBlankString +import com.soyle.stories.theme.addOppositionToValueWeb.AddOppositionToValueWebController +import com.soyle.stories.usecase.theme.addOppositionToValueWeb.OppositionAddedToValueWeb +import javafx.application.Platform +import javafx.event.ActionEvent +import kotlinx.coroutines.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import java.util.* + +class `Create Opposition Value Form Unit Test` : NodeTest() { + + private val valueWebId = ValueWeb.Id() + + private val locale = CreateOppositionValueFormLocaleMock() + + private var createOppositionValueRequest: Pair? = null + private val addOppositionToValueWeb = AddOppositionToValueWebControllerDouble( + onAddOpposition = { valueWebId, name, characterId -> + assertNull(characterId) + assertEquals(this.valueWebId.uuid.toString(), valueWebId) + createOppositionValueRequest = ValueWeb.Id(UUID.fromString(valueWebId)) to name!! + CompletableDeferred() + } + ) + + private var oppositionAddedToValueWeb: OppositionAddedToValueWeb? = null + override val view: CreateOppositionValueForm = CreateOppositionValueForm( + valueWebId, + ::oppositionAddedToValueWeb::set, + locale, + addOppositionToValueWeb + ) + + init { + showView() + } + + @Test + fun `should show text from locale for name input label`() { + interact { locale.name.set("The name of the thing") } + assertEquals("The name of the thing", view.access().nameLabel.text) + } + + @Test + fun `error message should not be visible`() { + assertNull(view.access().errorMessage) + } + + @Test + fun `name input should be enabled and focused`() { + assertFalse(view.access().nameInput.isDisable) + assertTrue(view.access().nameInput.isFocused) + } + + @Test + fun `should should name cannot be blank error when enter is pressed given text is blank`() { + view.drive { + nameInput.fireEvent(ActionEvent()) + } + + interact { locale.nameCannotBeBlank.set("you can't NOT provide a name, dude") } + assertEquals("you can't NOT provide a name, dude", view.access().errorMessage!!.text) + } + + @Test + fun `should create opposition value when enter is pressed given text is not blank`() { + view.drive { + nameInput.text = "Banana" + nameInput.fireEvent(ActionEvent()) + } + + assertEquals("Banana", createOppositionValueRequest!!.second.value) + } + + @Test + fun `should show error if create opposition value request fails`() { + val currentHandler = addOppositionToValueWeb.onAddOpposition + addOppositionToValueWeb.onAddOpposition = { a, b, c -> + currentHandler(a, b, c) + CoroutineScope(Dispatchers.Main).async { + throw Error("Some error with a locale message") + } + } + view.drive { + nameInput.text = "Banana" + nameInput.fireEvent(ActionEvent()) + } + + assertEquals("Some error with a locale message", view.access().errorMessage!!.text) + assertFalse(view.access().nameInput.isDisable) + } + + @Test + fun `should disable name input while creating opposition`() { + view.drive { + nameInput.text = "Banana" + nameInput.fireEvent(ActionEvent()) + } + + assertTrue(view.access().nameInput.isDisable) + } + + @Test + fun `should notify creator when opposition is created`() { + val currentHandler = addOppositionToValueWeb.onAddOpposition + addOppositionToValueWeb.onAddOpposition = { a, b, c -> + currentHandler(a, b, c) + CoroutineScope(Dispatchers.Main).async { + OppositionAddedToValueWeb(UUID.randomUUID(), valueWebId.uuid, UUID.randomUUID(), b!!.value, false) + } + } + view.drive { + nameInput.text = "Banana" + nameInput.fireEvent(ActionEvent()) + } + + assertNotNull(oppositionAddedToValueWeb) + assertFalse(view.access().nameInput.isDisable) + assertNull(view.access().errorMessage) + } + + @Nested + inner class `Given Previously Tried to Use Blank Name` { + + init { + view.drive { + nameInput.fireEvent(ActionEvent()) + } + } + + @Test + fun `should still display new error message if next attempt fails`() { + val currentHandler = addOppositionToValueWeb.onAddOpposition + addOppositionToValueWeb.onAddOpposition = { a, b, c -> + currentHandler(a, b, c) + CoroutineScope(Dispatchers.Main).async { + throw Error("Some error with a locale message") + } + } + view.drive { + nameInput.text = "Banana" + nameInput.fireEvent(ActionEvent()) + } + + assertEquals("Some error with a locale message", view.access().errorMessage!!.text) + assertFalse(view.access().nameInput.isDisable) + } + + @Test + fun `should no longer show error message when opposition is created`() { + val currentHandler = addOppositionToValueWeb.onAddOpposition + addOppositionToValueWeb.onAddOpposition = { a, b, c -> + currentHandler(a, b, c) + CoroutineScope(Dispatchers.Main).async { + OppositionAddedToValueWeb(UUID.randomUUID(), valueWebId.uuid, UUID.randomUUID(), b!!.value, false) + } + } + view.drive { + nameInput.text = "Banana" + nameInput.fireEvent(ActionEvent()) + } + + assertNull(view.access().errorMessage) + } + + } + +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/common/NodeAccess.kt b/desktop/views/src/testFixtures/kotlin/common/NodeAccess.kt index 49cc120f7..10296675c 100644 --- a/desktop/views/src/testFixtures/kotlin/common/NodeAccess.kt +++ b/desktop/views/src/testFixtures/kotlin/common/NodeAccess.kt @@ -17,8 +17,10 @@ open class NodeAccess(protected val node: N) : FxRobot() { protected fun Node.mandatoryChild(rule: Rendered, secondaryMatch: (Child) -> Boolean = { true }): ReadOnlyProperty, Child> = object : ReadOnlyProperty, Child> { override fun getValue(thisRef: NodeAccess, property: KProperty<*>): Child { val matches = from(this@mandatoryChild).lookup(rule.render()).queryAll() - return matches.filter(secondaryMatch).singleOrNull() ?: - error("Multiple children matching [${rule.render()} $secondaryMatch] query: $matches") + val secondaryMatches = matches.filter(secondaryMatch) + if (secondaryMatches.size > 1) error("Multiple children matching [${rule.render()} $secondaryMatch] query: $secondaryMatches") + else if (secondaryMatches.isEmpty()) error("No children matching [${rule.render()} $secondaryMatch]") + return secondaryMatches.single() } } diff --git a/desktop/views/src/testFixtures/kotlin/theme/characterComparison/Character Card View Access.kt b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/Character Card View Access.kt index e9af1244b..bacb10011 100644 --- a/desktop/views/src/testFixtures/kotlin/theme/characterComparison/Character Card View Access.kt +++ b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/Character Card View Access.kt @@ -5,6 +5,7 @@ import com.soyle.stories.desktop.view.common.NodeAccess import com.soyle.stories.domain.theme.oppositionValue.OppositionValue import com.soyle.stories.domain.theme.valueWeb.ValueWeb import com.soyle.stories.theme.characterValueComparison.components.CharacterCard +import com.soyle.stories.theme.characterValueComparison.components.addValueButton.AddValueButton import javafx.scene.Parent import javafx.scene.control.MenuButton import javafx.scene.control.MenuItem @@ -16,22 +17,7 @@ class `Character Card View Access`(val card: CharacterCard) : NodeAccess fun CharacterCard.access(op: `Character Card View Access`.() -> Unit) = `Character Card View Access`(this).op() } - val addValueButton: MenuButton by mandatoryChild(CssRule.id("add-value-button")) - - fun MenuButton.getCreateValueWebItem(): MenuItem? - { - return items.find { it.id == "create-value-web" } - } - - fun MenuButton.getCreateOppositionValueItem(valueWebId: ValueWeb.Id): MenuItem? - { - return items.find { it.id == "create-opposition-value-${valueWebId.uuid.toString()}" } - } - - fun MenuButton.getOppositionValueItem(oppositionValueId: OppositionValue.Id): MenuItem? - { - return items.find { it.id == oppositionValueId.uuid.toString() } - } + val addValueButton: AddValueButton by mandatoryChild(AddValueButton.Styles.addValueButton) val values: List get() = from(node).lookup(Chip.Styles.chip.render()).queryAll().toList() diff --git a/desktop/views/src/testFixtures/kotlin/theme/characterComparison/addValueButton/Add Value Button Access.kt b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/addValueButton/Add Value Button Access.kt new file mode 100644 index 000000000..32da980cb --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/addValueButton/Add Value Button Access.kt @@ -0,0 +1,40 @@ +package com.soyle.stories.desktop.view.theme.characterComparison.addValueButton + +import com.soyle.stories.desktop.view.common.NodeAccess +import com.soyle.stories.domain.theme.oppositionValue.OppositionValue +import com.soyle.stories.domain.theme.valueWeb.ValueWeb +import com.soyle.stories.theme.characterValueComparison.components.addValueButton.AddValueButton +import javafx.scene.control.Menu +import javafx.scene.control.MenuItem +import tornadofx.hasClass + +class `Add Value Button Access`(val button: AddValueButton) : NodeAccess(button) { + companion object : + NodeAccess.Factory(::`Add Value Button Access`) + + val loadingItem: MenuItem? + get() = button.items.find { it.id == "loading" } + + val createValueWebItem: MenuItem? + get() = button.items.find { it.id == "create-value-web" } + + val noAvailableValueWebsItem: MenuItem? + get() = button.items.find { it.id == "no-available-value-webs" } + + val valueWebItems: List + get() = button.items.asSequence() + .filterIsInstance() + .filter { it.hasClass(AddValueButton.Styles.availableValueWebItem) } + .toList() + + fun getValueWebItem(valueWebId: ValueWeb.Id): Menu? = valueWebItems.singleOrNull { it.id == valueWebId.toString() } + + val Menu.createOppositionValueItem: MenuItem + get() = items.single { it.id == "create-opposition-value" } + + val Menu.oppositionValueItems: List + get() = items.filter { it.hasClass(AddValueButton.Styles.availableOppositionItem) } + + fun Menu.getOppositionValueItem(oppositionValueId: OppositionValue.Id): MenuItem? = + oppositionValueItems.singleOrNull { it.id == oppositionValueId.toString() } +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/characterComparison/addValueButton/AddValueButtonFactory.kt b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/addValueButton/AddValueButtonFactory.kt new file mode 100644 index 000000000..086b42adc --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/addValueButton/AddValueButtonFactory.kt @@ -0,0 +1,26 @@ +package com.soyle.stories.desktop.view.theme.characterComparison.addValueButton + +import com.soyle.stories.desktop.view.theme.characterComparison.doubles.AddSymbolicItemToOppositionControllerDouble +import com.soyle.stories.desktop.view.theme.characterComparison.doubles.ListAvailableOppositionValuesForCharacterInThemeControllerDouble +import com.soyle.stories.desktop.view.theme.valueWeb.create.CreateValueWebFormFactory +import com.soyle.stories.desktop.view.theme.valueWeb.opposition.create.CreateOppositionValueFormFactory +import com.soyle.stories.domain.character.Character +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.theme.characterValueComparison.components.addValueButton.AddValueButton +import com.soyle.stories.theme.valueWeb.opposition.list.ListAvailableOppositionValuesForCharacterInThemeController + +class AddValueButtonFactory( + var listAvailableOppositionValuesForCharacterInThemeController: ListAvailableOppositionValuesForCharacterInThemeControllerDouble = ListAvailableOppositionValuesForCharacterInThemeControllerDouble() +) : AddValueButton.Factory { + + override fun invoke(themeId: Theme.Id, characterId: Character.Id): AddValueButton = + AddValueButton( + themeId, + characterId, + AddValueButtonLocaleMock(), + listAvailableOppositionValuesForCharacterInThemeController, + AddSymbolicItemToOppositionControllerDouble(), + CreateValueWebFormFactory(), + CreateOppositionValueFormFactory() + ) +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/characterComparison/addValueButton/AddValueButtonLocaleMock.kt b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/addValueButton/AddValueButtonLocaleMock.kt new file mode 100644 index 000000000..d9751d302 --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/addValueButton/AddValueButtonLocaleMock.kt @@ -0,0 +1,15 @@ +package com.soyle.stories.desktop.view.theme.characterComparison.addValueButton + +import com.soyle.stories.theme.characterValueComparison.components.addValueButton.AddValueButtonLocale +import javafx.beans.property.StringProperty +import javafx.beans.value.ObservableValue +import tornadofx.stringProperty + +class AddValueButtonLocaleMock( + override val addValue: StringProperty = stringProperty("Add Value"), + override val loading: StringProperty = stringProperty("Loading..."), + override val createNewValueWeb: StringProperty = stringProperty("Create New Value Web"), + override val themeHasNoValueWebs: StringProperty = stringProperty("Theme Has No Value Webs"), + override val createOppositionValue: StringProperty = stringProperty("Create Opposition Value"), +) : AddValueButtonLocale { +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/characterComparison/doubles/AddSymbolicItemToOppositionControllerDouble.kt b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/doubles/AddSymbolicItemToOppositionControllerDouble.kt new file mode 100644 index 000000000..6cf360485 --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/doubles/AddSymbolicItemToOppositionControllerDouble.kt @@ -0,0 +1,22 @@ +package com.soyle.stories.desktop.view.theme.characterComparison.doubles + +import com.soyle.stories.theme.addSymbolicItemToOpposition.AddSymbolicItemToOppositionController + +class AddSymbolicItemToOppositionControllerDouble( + val onAddCharacterToOpposition: (String, String) -> Unit = { _, _ -> }, + val onAddLocationToOpposition: (String, String) -> Unit = { _, _ -> }, + val onAddSymbolToOpposition: (String, String) -> Unit = { _, _ -> } +) : AddSymbolicItemToOppositionController { + + override fun addCharacterToOpposition(oppositionId: String, characterId: String) { + onAddCharacterToOpposition(oppositionId, characterId) + } + + override fun addLocationToOpposition(oppositionId: String, locationId: String) { + onAddLocationToOpposition(oppositionId, locationId) + } + + override fun addSymbolToOpposition(oppositionId: String, symbolId: String) { + onAddSymbolToOpposition(oppositionId, symbolId) + } +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/characterComparison/doubles/ListAvailableOppositionValuesForCharacterInThemeControllerDouble.kt b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/doubles/ListAvailableOppositionValuesForCharacterInThemeControllerDouble.kt new file mode 100644 index 000000000..cd38faa7f --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/characterComparison/doubles/ListAvailableOppositionValuesForCharacterInThemeControllerDouble.kt @@ -0,0 +1,23 @@ +package com.soyle.stories.desktop.view.theme.characterComparison.doubles + +import com.soyle.stories.domain.character.Character +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.theme.valueWeb.opposition.list.ListAvailableOppositionValuesForCharacterInThemeController +import com.soyle.stories.usecase.theme.listAvailableOppositionValuesForCharacterInTheme.ListAvailableOppositionValuesForCharacterInTheme +import kotlinx.coroutines.CompletableJob +import kotlinx.coroutines.Job + +class ListAvailableOppositionValuesForCharacterInThemeControllerDouble( + val onInvoke: (Theme.Id, Character.Id, ListAvailableOppositionValuesForCharacterInTheme.OutputPort) -> Unit = { _, _, _ -> }, + var job: CompletableJob = Job() +) : ListAvailableOppositionValuesForCharacterInThemeController { + + override fun listAvailableOppositionValuesForCharacter( + themeId: Theme.Id, + characterId: Character.Id, + output: ListAvailableOppositionValuesForCharacterInTheme.OutputPort + ): Job { + onInvoke(themeId, characterId, output) + return job + } +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/valueWeb/AddValueWebToThemeControllerDouble.kt b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/AddValueWebToThemeControllerDouble.kt new file mode 100644 index 000000000..a3331a2cf --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/AddValueWebToThemeControllerDouble.kt @@ -0,0 +1,25 @@ +package com.soyle.stories.desktop.view.theme.valueWeb + +import com.soyle.stories.domain.validation.NonBlankString +import com.soyle.stories.theme.addValueWebToTheme.AddValueWebToThemeController +import com.soyle.stories.usecase.theme.addValueWebToTheme.ValueWebAddedToTheme +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred + +class AddValueWebToThemeControllerDouble( + var onAddValueWebToTheme: (String, NonBlankString) -> Deferred = {_,_-> CompletableDeferred() } +) : AddValueWebToThemeController { + + override fun addValueWebToTheme(themeId: String, name: NonBlankString, onError: (Throwable) -> Unit): Deferred { + return onAddValueWebToTheme(themeId, name) + } + + override fun addValueWebToThemeWithCharacter( + themeId: String, + name: NonBlankString, + characterId: String, + onError: (Throwable) -> Unit + ): Deferred { + return onAddValueWebToTheme(themeId, name) + } +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/Create Value Web Form Access.kt b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/Create Value Web Form Access.kt new file mode 100644 index 000000000..9f7e9dbab --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/Create Value Web Form Access.kt @@ -0,0 +1,18 @@ +package com.soyle.stories.desktop.view.theme.valueWeb.create + +import com.soyle.stories.common.exists +import com.soyle.stories.desktop.view.common.NodeAccess +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import javafx.scene.control.Label +import javafx.scene.control.TextInputControl +import tornadofx.Stylesheet +import tornadofx.hasClass + +class `Create Value Web Form Access`(val form: CreateValueWebForm) : NodeAccess(form) { + companion object : + NodeAccess.Factory(::`Create Value Web Form Access`) + + val nameLabel: Label by mandatoryChild(Stylesheet.label) { !it.hasClass(Stylesheet.error) } + val nameInput: TextInputControl by mandatoryChild(Stylesheet.textInput) + val errorMessage: Label? by temporaryChild(Stylesheet.error) { it.exists } +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/CreateValueWebFormFactory.kt b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/CreateValueWebFormFactory.kt new file mode 100644 index 000000000..9f5baf48f --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/CreateValueWebFormFactory.kt @@ -0,0 +1,24 @@ +package com.soyle.stories.desktop.view.theme.valueWeb.create + +import com.soyle.stories.desktop.view.theme.valueWeb.AddValueWebToThemeControllerDouble +import com.soyle.stories.domain.theme.Theme +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import com.soyle.stories.usecase.theme.addValueWebToTheme.ValueWebAddedToTheme + +class CreateValueWebFormFactory( + val onInvoke: (Theme.Id, suspend (ValueWebAddedToTheme) -> Unit) -> Unit = {_,_ ->} +) : CreateValueWebForm.Factory { + + override fun invoke( + themeId: Theme.Id, + onCreateValueWeb: suspend (ValueWebAddedToTheme) -> Unit + ): CreateValueWebForm { + onInvoke(themeId, onCreateValueWeb) + return CreateValueWebForm( + themeId, + onCreateValueWeb, + CreateValueWebFormLocaleMock(), + AddValueWebToThemeControllerDouble() + ) + } +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/CreateValueWebFormLocaleMock.kt b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/CreateValueWebFormLocaleMock.kt new file mode 100644 index 000000000..30fc7475b --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/CreateValueWebFormLocaleMock.kt @@ -0,0 +1,11 @@ +package com.soyle.stories.desktop.view.theme.valueWeb.create + +import com.soyle.stories.theme.valueWeb.create.CreateValueWebFormLocale +import javafx.beans.property.StringProperty +import javafx.beans.value.ObservableValue +import tornadofx.stringProperty + +class CreateValueWebFormLocaleMock( + override val name: StringProperty = stringProperty("Name"), + override val nameCannotBeBlank: StringProperty = stringProperty("Name Cannot Be Blank") +) : CreateValueWebFormLocale \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/getOpenCreateValueWebDialog.kt b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/getOpenCreateValueWebDialog.kt new file mode 100644 index 000000000..85c473140 --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/create/getOpenCreateValueWebDialog.kt @@ -0,0 +1,9 @@ +package com.soyle.stories.desktop.view.theme.valueWeb.create + +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import org.testfx.api.FxRobot + +fun FxRobot.getOpenCreateValueWebDialog(): CreateValueWebForm? = + listWindows().asSequence() + .mapNotNull { it.scene.root as? CreateValueWebForm } + .firstOrNull { it.scene.window?.isShowing == true } \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/AddOppositionToValueWebControllerDouble.kt b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/AddOppositionToValueWebControllerDouble.kt new file mode 100644 index 000000000..077b2a9dd --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/AddOppositionToValueWebControllerDouble.kt @@ -0,0 +1,27 @@ +package com.soyle.stories.desktop.view.theme.valueWeb.opposition.create + +import com.soyle.stories.domain.validation.NonBlankString +import com.soyle.stories.theme.addOppositionToValueWeb.AddOppositionToValueWebController +import com.soyle.stories.usecase.theme.addOppositionToValueWeb.OppositionAddedToValueWeb +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CompletableJob +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job + +class AddOppositionToValueWebControllerDouble( + var onAddOpposition: (String, NonBlankString?, String?) -> Deferred = {_,_,_ -> CompletableDeferred() }, +) : AddOppositionToValueWebController { + + override fun addOpposition(valueWebId: String): Deferred { + return onAddOpposition(valueWebId, null, null) + } + + override fun addOpposition(valueWebId: String, name: NonBlankString): Deferred { + return onAddOpposition(valueWebId, name, null) + + } + + override fun addOppositionWithCharacter(valueWebId: String, name: NonBlankString, characterId: String): Deferred { + return onAddOpposition(valueWebId, name, characterId) + } +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/Create Opposition Value Dialog Access.kt b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/Create Opposition Value Dialog Access.kt index 11591116e..c7275e47a 100644 --- a/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/Create Opposition Value Dialog Access.kt +++ b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/Create Opposition Value Dialog Access.kt @@ -1,16 +1,29 @@ package com.soyle.stories.desktop.view.theme.valueWeb.opposition.create +import com.soyle.stories.common.exists import com.soyle.stories.desktop.view.common.NodeAccess import com.soyle.stories.theme.createOppositionValueDialog.CreateOppositionValueDialog +import com.soyle.stories.theme.valueWeb.create.CreateValueWebForm +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueForm import javafx.scene.Parent +import javafx.scene.control.Label import javafx.scene.control.TextInputControl +import org.testfx.api.FxRobot import tornadofx.Stylesheet +import tornadofx.hasClass -class `Create Opposition Value Dialog Access`(val dialog: CreateOppositionValueDialog) : - NodeAccess(dialog.root) { +class `Create Opposition Value Form Access`(val dialog: CreateOppositionValueForm) : + NodeAccess(dialog) { companion object : - NodeAccess.Factory(::`Create Opposition Value Dialog Access`) + NodeAccess.Factory(::`Create Opposition Value Form Access`) + val nameLabel: Label by mandatoryChild(Stylesheet.label) { ! it.hasClass(Stylesheet.error) } val nameInput: TextInputControl by mandatoryChild(Stylesheet.textInput) -} \ No newline at end of file + val errorMessage: Label? by temporaryChild(Stylesheet.error) { it.isVisible } +} + +fun FxRobot.getOpenCreateOppositionValueDialog(): CreateOppositionValueForm? = + listWindows().asSequence() + .mapNotNull { it.scene.root as? CreateOppositionValueForm } + .firstOrNull { it.scene.window?.isShowing == true } \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/CreateOppositionValueFormFactory.kt b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/CreateOppositionValueFormFactory.kt new file mode 100644 index 000000000..176235bb7 --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/CreateOppositionValueFormFactory.kt @@ -0,0 +1,24 @@ +package com.soyle.stories.desktop.view.theme.valueWeb.opposition.create + +import com.soyle.stories.domain.theme.valueWeb.ValueWeb +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueForm +import com.soyle.stories.usecase.theme.addOppositionToValueWeb.OppositionAddedToValueWeb +import com.soyle.stories.usecase.theme.addValueWebToTheme.ValueWebAddedToTheme + +class CreateOppositionValueFormFactory( + val onInvoke: (ValueWeb.Id, suspend (OppositionAddedToValueWeb) -> Unit) -> Unit = {_,_ ->} +) : CreateOppositionValueForm.Factory { + + override fun invoke( + valueWebId: ValueWeb.Id, + onCreateOppositionValue: suspend (OppositionAddedToValueWeb) -> Unit + ): CreateOppositionValueForm { + onInvoke(valueWebId, onCreateOppositionValue) + return CreateOppositionValueForm( + valueWebId, + onCreateOppositionValue, + CreateOppositionValueFormLocaleMock(), + AddOppositionToValueWebControllerDouble() + ) + } +} \ No newline at end of file diff --git a/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/CreateOppositionValueFormLocaleMock.kt b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/CreateOppositionValueFormLocaleMock.kt new file mode 100644 index 000000000..0deb777c0 --- /dev/null +++ b/desktop/views/src/testFixtures/kotlin/theme/valueWeb/opposition/create/CreateOppositionValueFormLocaleMock.kt @@ -0,0 +1,11 @@ +package com.soyle.stories.desktop.view.theme.valueWeb.opposition.create + +import com.soyle.stories.theme.valueWeb.opposition.create.CreateOppositionValueFormLocale +import javafx.beans.property.StringProperty +import javafx.beans.value.ObservableValue +import tornadofx.stringProperty + +class CreateOppositionValueFormLocaleMock( + override val name: StringProperty = stringProperty("Name"), + override val nameCannotBeBlank: StringProperty = stringProperty("Name Cannot Be Blank"), +) : CreateOppositionValueFormLocale \ No newline at end of file