diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/RenderWorkflow.kt b/workflow-runtime/src/main/java/com/squareup/workflow/RenderWorkflow.kt index 98425aef84..179dae4c51 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/RenderWorkflow.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/RenderWorkflow.kt @@ -158,12 +158,10 @@ fun renderWorkflowIn( // It might look weird to start by consuming the output before getting the rendering below, // but remember the first render pass already occurred above, before this coroutine was even // launched. - val output = runner.nextOutput() + runner.nextOutput() + ?.let { onOutput(it.value) } - // After receiving an output, the next render pass must be done before emitting that output, - // so that the workflow states appear consistent to observers of the outputs and renderings. renderingsAndSnapshots.value = runner.nextRendering() - output?.let { onOutput(it.value) } } } diff --git a/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt index 1c402b498a..cb06bdd98d 100644 --- a/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt +++ b/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt @@ -26,6 +26,8 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine @@ -363,5 +365,38 @@ class RenderWorkflowInTest { assertFalse(scope.isActive) } + @Test fun `output is emitted before next render pass`() { + val outputTrigger = CompletableDeferred() + // A workflow whose state and rendering is the last output that it emitted. + val workflow = Workflow.stateful( + initialState = { "{no output}" }, + render = { _, state -> + runningWorker(Worker.from { outputTrigger.await() }) { output -> + action { + setOutput(output) + nextState = output + } + } + return@stateful state + } + ) + val scope = TestCoroutineScope() + val events = mutableListOf() + renderWorkflowIn(workflow, scope, MutableStateFlow(Unit)) { events += "output($it)" } + .onEach { events += "rendering(${it.rendering})" } + .launchIn(scope) + assertEquals(listOf("rendering({no output})"), events) + + outputTrigger.complete("output") + assertEquals( + listOf( + "rendering({no output})", + "output(output)", + "rendering(output)" + ), + events + ) + } + private class ExpectedException : RuntimeException() }