All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squareup.workflow.testing.RenderTester.kt Maven / Gradle / Ivy

There is a newer version: 1.0.0-alpha.1
Show newest version
/*
 * Copyright 2019 Square Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.workflow.testing

import com.squareup.workflow.StatefulWorkflow
import com.squareup.workflow.Worker
import com.squareup.workflow.Workflow
import com.squareup.workflow.WorkflowAction
import kotlin.reflect.KClass

/**
 * Create a [RenderTester] to unit test an individual render pass of this workflow, using the
 * workflow's [initial state][StatefulWorkflow.initialState].
 *
 * See [RenderTester] for usage documentation.
 */
@Suppress("UNCHECKED_CAST")
fun  Workflow.renderTester(
  props: PropsT
): RenderTester {
  val statefulWorkflow = asStatefulWorkflow() as StatefulWorkflow
  return statefulWorkflow.renderTester(
      props = props,
      initialState = statefulWorkflow.initialState(props, null)
  ) as RenderTester
}

/**
 * Create a [RenderTester] to unit test an individual render pass of this workflow.
 *
 * See [RenderTester] for usage documentation.
 */
/* ktlint-disable parameter-list-wrapping */
fun 
    StatefulWorkflow.renderTester(
  props: PropsT,
  initialState: StateT
): RenderTester =
/* ktlint-enable parameter-list-wrapping */
  RealRenderTester(this, props, initialState)

/**
 * The props must be specified, the initial state may be specified, and then all child workflows
 * and workers that are expected to run, and any outputs from them, must be specified with
 * [expectWorkflow] and [expectWorker] calls. Then call [render] and perform any assertions on the
 * rendering. An event may also be sent to the rendering if no workflows or workers emitted an
 * output. Lastly, the [RenderTestResult] returned by `render` may be used to assert on the
 * [WorkflowAction]s processed to handle events or outputs by calling
 * [verifyAction][RenderTestResult.verifyAction] or
 * [verifyActionResult][RenderTestResult.verifyActionResult].
 *
 * - All workflows and workers that are rendered/ran by this workflow must be specified.
 * - It is an error if more than one workflow or worker specifies an output.
 * - It is a test failure if any workflows or workers that were expected were not ran.
 * - It is a test failure if the workflow tried to run any workflows or workers that were not
 *   expected.
 * - It is a test failure if no workflow or workflow emitted an output, no rendering event was
 *   invoked, and any of the action verification methods on [RenderTestResult] is called.
 *
 * ## Examples
 *
 * ### Worker output
 *
 * The following example tests a render pass that runs one worker, `SubmitLoginWorker`, which
 * is configured to have "emitted" an output, and one workflow, `ChildWorkflow`, which expects a
 * props containing "[email protected]" and returning a `ChildRendering` as its rendering.
 *
 * It checks that the rendering properties are expected and that the output handler for the
 * `SubmitLoginWorker` returned the `CompleteLogin` action.
 *
 * ```
 * workflow
 *   .renderTester(
 *     props = MyProps(…),
 *     initialState = MyState(…)
 *   )
 *   .expectWorker(
 *     matchesWhen = { it is SubmitLoginWorker },
 *     key = "signin",
 *     output = EmittedOutput(LoginResponse(success = true))
 *   )
 *   .expectWorkflow(
 *     workflowType = ChildWorkflow::class,
 *     key = "child",
 *     assertProps = { assertThat(it.email).isEqualTo("[email protected]") },
 *     rendering = ChildRendering("message")
 *   )
 *   .render { rendering ->
 *     assertThat(rendering.text).isEqualTo("foo")
 *   }
 *   .verifyAction { action ->
 *     assertThat(action).isEqualTo(Action.CompleteLogin(success = true))
 *   }
 * ```
 *
 * ### Rendering event
 *
 * This is similar to the example above, but will test an event sent to the rendering instead.
 *
 * ```
 * workflow
 *   .renderTester(
 *     props = MyProps(…),
 *     initialState = MyState(…)
 *   )
 *   .expectWorker(
 *     matchesWhen = { it is SubmitLoginWorker },
 *     key = "signin"
 *   )
 *   .expectWorkflow(
 *     workflowType = ChildWorkflow::class,
 *     key = "child",
 *     assertProps = { assertThat(it.email).isEqualTo("[email protected]") },
 *     rendering = ChildRendering("message")
 *   )
 *   .render { rendering ->
 *     rendering.onCancelClicked()
 *     assertThat(rendering.text).isEqualTo("foo")
 *   }
 *   .verifyAction { action ->
 *     assertThat(action).isEqualTo(Action.CancelLogin)
 *   }
 * ```
 *
 * ### Verify action result
 *
 * This test verifies the action _result_ instead of the action itself. This technique is useful
 * if the [WorkflowAction] is anonymous or inline.
 *
 * ```
 * val currentState = …
 * val previousState = …
 *
 * workflow
 *   .renderTester(
 *     props = MyProps(…),
 *     initialState = currentState
 *   )
 *   .render { rendering ->
 *     rendering.onCancelClicked()
 *   }
 *   .verifyActionResult { newState, output ->
 *     // Check that the workflow navigated back correctly.
 *     assertThat(newState).isEqualTo(previousState)
 *
 *     // Check that the workflow didn't emit any output from the button click.
 *     assertThat(output).isNull()
 *   }
 * ```
 *
 * ### Too many outputs
 *
 * This is an example of what **not** to do – this test will error out because a worker is emitting
 * and output _and_ a rendering event is sent.
 *
 * ```
 * workflow
 *   .renderTester(
 *     props = MyProps(…),
 *     initialState = MyState(…)
 *   )
 *   .expectWorker(
 *     matchesWhen = { it is SubmitLoginWorker },
 *     key = "signin",
 *     output = EmittedOutput(LoginResponse(success = true))
 *   )
 *   .expectWorkflow(
 *     workflowType = ChildWorkflow::class,
 *     key = "child",
 *     assertProps = { assertThat(it.email).isEqualTo("[email protected]") },
 *     rendering = ChildRendering("message")
 *   )
 *   .render { rendering ->
 *     // This will throw and fail the test because the SubmitLoginWorker is also configured to emit
 *     // an output.
 *     rendering.onCancelClicked()
 * ```
 */
interface RenderTester {

  /**
   * Specifies that this render pass is expected to render a particular child workflow.
   *
   * @param workflowType The [KClass] of the expected workflow. May also be any of the supertypes
   * of the expected workflow, e.g. if the workflow type is an interface and the workflow-under-test
   * injects a mock.
   * @param rendering The rendering to return from
   * [renderChild][com.squareup.workflow.RenderContext.renderChild] when this workflow is rendered.
   * @param key The key passed to [renderChild][com.squareup.workflow.RenderContext.renderChild]
   * when rendering this workflow.
   * @param assertProps A function that performs assertions on the props passed to
   * [renderChild][com.squareup.workflow.RenderContext.renderChild].
   * @param output If non-null, [EmittedOutput.output] will be "emitted" when this workflow is
   * rendered. The [WorkflowAction] used to handle this output can be verified using methods on
   * [RenderTestResult].
   */
  fun  expectWorkflow(
    workflowType: KClass>,
    rendering: ChildRenderingT,
    key: String = "",
    assertProps: (props: ChildPropsT) -> Unit = {},
    output: EmittedOutput? = null
  ): RenderTester

  /**
   * Specifies that this render pass is expected to run a particular worker.
   *
   * @param matchesWhen Predicate used to determine if this matches the worker being ran.
   * @param key The key passed to [runningWorker][com.squareup.workflow.RenderContext.runningWorker]
   * when rendering this workflow.
   * @param output If non-null, [EmittedOutput.output] will be emitted when this worker is ran.
   * The [WorkflowAction] used to handle this output can be verified using methods on
   * [RenderTestResult].
   */
  fun expectWorker(
    matchesWhen: (otherWorker: Worker<*>) -> Boolean,
    key: String = "",
    output: EmittedOutput? = null
  ): RenderTester

  /**
   * Execute the workflow's `render` method and run [block] to perform assertions on and send events
   * to the resulting rendering.
   *
   * All workflows and workers rendered/ran by the workflow must be specified before calling this
   * method.
   *
   * @param block Passed the result of the render pass to perform assertions on.
   * If no child workflow or worker was configured to emit an output, may also invoke one of the
   * rendering's event handlers. It is an error to invoke an event handler if a child emitted an
   * output.
   * @return A [RenderTestResult] that can be used to verify the [WorkflowAction] that was used to
   * handle a workflow or worker output or a rendering event.
   */
  fun render(block: (rendering: RenderingT) -> Unit = {}): RenderTestResult
}

/**
 * Specifies that this render pass is expected to run a particular worker.
 *
 * @param doesSameWorkAs Worker passed to the actual worker's
 * [doesSameWorkAs][Worker.doesSameWorkAs] method to identify the worker. Note that the actual
 * method is called on the worker instance given by the workflow-under-test, and the value of this
 * argument is passed to that method – if you need custom comparison logic for some reason, use
 * the overload of this method that takes a `matchesWhen` parameter.
 * @param key The key passed to [runningWorker][com.squareup.workflow.RenderContext.runningWorker]
 * when rendering this workflow.
 * @param output If non-null, [EmittedOutput.output] will be emitted when this worker is ran.
 * The [WorkflowAction] used to handle this output can be verified using methods on
 * [RenderTestResult].
 */
/* ktlint-disable parameter-list-wrapping */
fun 
    RenderTester.expectWorker(
  doesSameWorkAs: Worker<*>,
  key: String = "",
  output: EmittedOutput? = null
): RenderTester = expectWorker(
/* ktlint-enable parameter-list-wrapping */
    matchesWhen = { it.doesSameWorkAs(doesSameWorkAs) },
    key = key,
    output = output
)

/**
 * Wrapper around a potentially-nullable [OutputT] value.
 */
data class EmittedOutput(val output: OutputT)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy