From 63b1f70173bb458fee849898fabd5e1a31d891bf Mon Sep 17 00:00:00 2001 From: Laimonas Turauskas Date: Thu, 22 Aug 2024 15:01:21 +0300 Subject: [PATCH] Add modifyOutput function to TestFormula. --- .../com/instacart/formula/test/TestFormula.kt | 34 +++-- .../instacart/formula/test/SimpleFormula.kt | 23 +++ .../formula/test/TestFormulaRobot.kt | 72 --------- .../instacart/formula/test/TestFormulaTest.kt | 141 +++++++++++------- 4 files changed, 131 insertions(+), 139 deletions(-) create mode 100644 formula-test/src/test/java/com/instacart/formula/test/SimpleFormula.kt delete mode 100644 formula-test/src/test/java/com/instacart/formula/test/TestFormulaRobot.kt diff --git a/formula-test/src/main/java/com/instacart/formula/test/TestFormula.kt b/formula-test/src/main/java/com/instacart/formula/test/TestFormula.kt index 8a7014d8..a517441c 100644 --- a/formula-test/src/main/java/com/instacart/formula/test/TestFormula.kt +++ b/formula-test/src/main/java/com/instacart/formula/test/TestFormula.kt @@ -38,6 +38,7 @@ abstract class TestFormula : data class Value( val key: Any?, val input: Input, + val output: Output, val onNewOutput: (Output) -> Unit ) @@ -62,6 +63,7 @@ abstract class TestFormula : stateMap[state.uniqueIdentifier] = Value( key = state.key, input = input, + output = state.output, onNewOutput = context.onEvent { transition(state.copy(output = it)) } @@ -82,24 +84,32 @@ abstract class TestFormula : * Emits a new [Output]. */ fun output(output: Output) { - val update = requireNotNull(stateMap.values.lastOrNull()?.onNewOutput) { - "Formula is not running" - } + val update = getMostRecentRunningFormula().onNewOutput update(output) } fun output(key: Any?, output: Output) { - val instance = getByKey(key) + val instance = getRunningFormulaByKey(key) instance.onNewOutput(output) } + fun updateOutput(modify: Output.() -> Output) { + val formulaValue = getMostRecentRunningFormula() + val newOutput = formulaValue.output.modify() + formulaValue.onNewOutput(newOutput) + } + + fun updateOutput(key: Any?, modify: Output.() -> Output) { + val formulaValue = getRunningFormulaByKey(key) + val newOutput = formulaValue.output.modify() + formulaValue.onNewOutput(newOutput) + } + /** * Performs an interaction on the current [Input] passed by the parent. */ fun input(interact: Input.() -> Unit) { - val input = requireNotNull(stateMap.values.lastOrNull()?.input) { - "Formula is not running" - } + val input = getMostRecentRunningFormula().input interact(input) } @@ -107,7 +117,7 @@ abstract class TestFormula : * Performs an interaction on the current [Input] passed by the parent. */ fun input(key: Any?, interact: Input.() -> Unit) { - val instance = getByKey(key) + val instance = getRunningFormulaByKey(key) instance.input.interact() } @@ -118,7 +128,13 @@ abstract class TestFormula : } } - private fun getByKey(key: Any?): Value { + private fun getMostRecentRunningFormula(): Value { + return requireNotNull(stateMap.values.lastOrNull()) { + "Formula is not running" + } + } + + private fun getRunningFormulaByKey(key: Any?): Value { return requireNotNull(stateMap.entries.firstOrNull { it.value.key == key }?.value) { val existingKeys = stateMap.entries.map { it.value.key } "Formula for $key is not running, there are $existingKeys running" diff --git a/formula-test/src/test/java/com/instacart/formula/test/SimpleFormula.kt b/formula-test/src/test/java/com/instacart/formula/test/SimpleFormula.kt new file mode 100644 index 00000000..da24fde2 --- /dev/null +++ b/formula-test/src/test/java/com/instacart/formula/test/SimpleFormula.kt @@ -0,0 +1,23 @@ +package com.instacart.formula.test + +import com.instacart.formula.IFormula +import com.instacart.formula.test.SimpleFormula.Input +import com.instacart.formula.test.SimpleFormula.Output + +interface SimpleFormula : IFormula { + data class Input(val inputId: String = "inputId") + data class Output(val outputId: Int, val text: String) + + override fun key(input: Input): Any? = "simple-formula-key" +} + +class TestSimpleFormula( + private val initialOutput: Output = Output( + outputId = 0, + text = "", + ) +) : SimpleFormula { + override val implementation = testFormula( + initialOutput = initialOutput, + ) +} \ No newline at end of file diff --git a/formula-test/src/test/java/com/instacart/formula/test/TestFormulaRobot.kt b/formula-test/src/test/java/com/instacart/formula/test/TestFormulaRobot.kt deleted file mode 100644 index 047b9d19..00000000 --- a/formula-test/src/test/java/com/instacart/formula/test/TestFormulaRobot.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.instacart.formula.test - -import com.instacart.formula.Evaluation -import com.instacart.formula.Formula -import com.instacart.formula.IFormula -import com.instacart.formula.Snapshot - -class TestFormulaRobot { - - private val childFormula: FakeChildFormula = FakeChildFormula() - private val parentFormula: ParentFormula = ParentFormula(childFormula) - private val observer = parentFormula.test() - - - fun start() = apply { - observer.input(Unit) - } - - fun withTestFormula(assertion: TestFormula.() -> Unit) = apply { - childFormula.implementation.assertion() - } - - fun assertOutput(on: ParentFormula.Output.() -> Unit) = apply { - observer.output(on) - } - - class ParentFormula( - private val childFormula: ChildFormula - ) : Formula() { - - data class State(val name: String) - - data class Output( - val name: String, - val button: ChildFormula.Button - ) - - override fun initialState(input: Unit): State = State(name = "") - - override fun Snapshot.evaluate(): Evaluation { - return Evaluation( - output = Output( - name = state.name, - button = context.child(childFormula, ChildFormula.Input( - name = state.name, - onChangeName = context.onEvent { - transition(state.copy(name = it)) - } - )) - ) - ) - } - } - - interface ChildFormula : IFormula { - - data class Input( - val name: String, - val onChangeName: (newName: String) -> Unit - ) - - class Button(val onNameChanged: (String) -> Unit) - - override fun key(input: Input): Any? = "child-key" - } - - class FakeChildFormula : ChildFormula { - override val implementation = testFormula( - initialOutput = ChildFormula.Button {} - ) - } -} \ No newline at end of file diff --git a/formula-test/src/test/java/com/instacart/formula/test/TestFormulaTest.kt b/formula-test/src/test/java/com/instacart/formula/test/TestFormulaTest.kt index 4321d973..8040aad8 100644 --- a/formula-test/src/test/java/com/instacart/formula/test/TestFormulaTest.kt +++ b/formula-test/src/test/java/com/instacart/formula/test/TestFormulaTest.kt @@ -6,17 +6,23 @@ import org.junit.Test class TestFormulaTest { @Test fun `assert running count is zero when formula is not running`() { - TestFormulaRobot() - .withTestFormula { assertRunningCount(0) } - .start() - .withTestFormula { assertRunningCount(1) } + val formula = TestSimpleFormula() + formula.implementation.assertRunningCount(0) + formula.test().input(SimpleFormula.Input()) + formula.implementation.assertRunningCount(1) + } + + @Test fun `emits initial output when subscribed`() { + val formula = TestSimpleFormula() + formula.test().input(SimpleFormula.Input()).output { + assertThat(this).isEqualTo(formula.implementation.initialOutput()) + } } @Test fun `output throws an exception when no test formula is running`() { try { - TestFormulaRobot() - .withTestFormula { output(TestFormulaRobot.ChildFormula.Button(onNameChanged = {})) } - + val formula = TestSimpleFormula() + formula.implementation.output(SimpleFormula.Output(0, "")) fail("Should not happen") } catch (e: Exception) { assertThat(e).hasMessageThat().startsWith("Formula is not running") @@ -24,42 +30,69 @@ class TestFormulaTest { } @Test fun `output emits new output to the parent`() { - val newOutput = TestFormulaRobot.ChildFormula.Button(onNameChanged = {}) - TestFormulaRobot() - .start() - .withTestFormula { output(newOutput) } - .assertOutput { assertThat(this.button).isEqualTo(newOutput) } + val formula = TestSimpleFormula() + val observer = formula.test().input(SimpleFormula.Input()) + + val newOutput = SimpleFormula.Output(5, "random") + formula.implementation.output(newOutput) + observer.output { assertThat(this).isEqualTo(newOutput) } } @Test fun `output with key throws an exception when test formula matching key is not running`() { - val newOutput = TestFormulaRobot.ChildFormula.Button(onNameChanged = {}) try { - TestFormulaRobot() - .start() - .withTestFormula { - output(key = "random-key", newOutput) - } + val newOutput = SimpleFormula.Output(5, "random") + val formula = TestSimpleFormula() + val observer = formula.test().input(SimpleFormula.Input()) + formula.implementation.output("random-key", newOutput) fail("Should not happen") } catch (e: Exception) { - assertThat(e).hasMessageThat().startsWith("Formula for random-key is not running, there are [child-key] running") + assertThat(e).hasMessageThat().startsWith("Formula for random-key is not running, there are [simple-formula-key] running") } } @Test fun `output with key emits new output to the parent when key matches`() { - val newOutput = TestFormulaRobot.ChildFormula.Button(onNameChanged = {}) - TestFormulaRobot() - .start() - .withTestFormula { output(key = "child-key", newOutput) } - .assertOutput { assertThat(this.button).isEqualTo(newOutput) } + val newOutput = SimpleFormula.Output(5, "random") + + val formula = TestSimpleFormula() + val observer = formula.test().input(SimpleFormula.Input()) + formula.implementation.output("simple-formula-key", newOutput) + observer.output { assertThat(this).isEqualTo(newOutput) } + } + + @Test fun `updateOutput uses previous output and emits new one to the parent`() { + val formula = TestSimpleFormula() + val observer = formula.test().input(SimpleFormula.Input()) + + formula.implementation.updateOutput { copy(outputId = outputId.inc()) } + observer.output { assertThat(this).isEqualTo(SimpleFormula.Output(1, "")) } + } + + @Test fun `updateOutput with key emits a modified output when key matches a running formula`() { + val formula = TestSimpleFormula() + val observer = formula.test().input(SimpleFormula.Input()) + + formula.implementation.updateOutput("simple-formula-key") { copy(outputId = outputId.inc()) } + observer.output { assertThat(this).isEqualTo(SimpleFormula.Output(1, "")) } + } + + @Test fun `updateOutput with key throws an error when there is no running formula matching the key`() { + try { + val formula = TestSimpleFormula() + val observer = formula.test().input(SimpleFormula.Input()) + + formula.implementation.updateOutput("random-key") { copy(outputId = outputId.inc()) } + observer.output { assertThat(this).isEqualTo(SimpleFormula.Output(1, "")) } + fail() + } catch (e: Exception) { + assertThat(e).hasMessageThat().startsWith("Formula for random-key is not running, there are [simple-formula-key] running") + } } @Test fun `input() throw an exception when no formulas are running`() { try { - TestFormulaRobot() - .withTestFormula { - input { onChangeName("my name") } - } + val formula = TestSimpleFormula() + formula.implementation.input { } fail("Should not happen") } catch (e: Exception) { @@ -67,24 +100,26 @@ class TestFormulaTest { } } - @Test fun `input() works as expected when formula is running`() { - TestFormulaRobot() - .start() - .withTestFormula { - input { onChangeName("my name") } - } - .assertOutput { - assertThat(name).isEqualTo("my name") - } + @Test fun `input() emits the last input provided by the parent`() { + val myInput = SimpleFormula.Input("my-input-id") + val formula = TestSimpleFormula() + val observer = formula.test() + + // Initial input + observer.input(myInput) + formula.implementation.input { assertThat(this).isEqualTo(myInput) } + + // Next input + val nextInput = SimpleFormula.Input("next-input-id") + observer.input(nextInput) + formula.implementation.input { assertThat(this).isEqualTo(nextInput) } } @Test fun `input() throws an error when NO formula that matches the key provided is running`() { try { - TestFormulaRobot() - .start() - .withTestFormula { - input(key = "random-key") { onChangeName("my name") } - } + val formula = TestSimpleFormula() + formula.test().input(SimpleFormula.Input()) + formula.implementation.input(key = "random-key") {} fail("Should not happen") } catch (e: Exception) { @@ -93,21 +128,11 @@ class TestFormulaTest { } @Test fun `input() works as expected when formula that matches the key provided is running`() { - TestFormulaRobot() - .start() - .withTestFormula { - input(key = "child-key") { onChangeName("my name") } - } - .assertOutput { - assertThat(name).isEqualTo("my name") - } - } - - @Test fun `input passed to formula`() { - TestFormulaRobot() - .start() - .withTestFormula { - input { assertThat(name).isEqualTo("") } - } + val myInput = SimpleFormula.Input() + val formula = TestSimpleFormula() + formula.test().input(myInput) + formula.implementation.input(key = "simple-formula-key") { + assertThat(this).isEqualTo(myInput) + } } }