webbhuset / elm-actor-model / Webbhuset.Component.Sandbox

Sandbox

The sandbox module is helpful when developing components. It lets you run the component using elm reactor outside the system and define several test cases.

If you want to run your sandbox on a CI you can add #markdown to the URL. This will output the test results as a markdown string inside a pre element:

<pre id="markdown-output"> Results here </pre>

This way you can complile the sandbox, run it in a headless Chrome and dump the DOM. From the DOM you can extract the results.


type alias SandboxProgram model msgIn msgOut =
Platform.Program () (Model model msgOut) (Msg msgIn msgOut)

The Program type of your main function.

Create Sandbox

Wrap a component in a sandbox application.

This will render each test case and log all messages. Create a test file with a main function where you declare all test cases.

A sandbox module example for YourComponent:

import YourComponent exposing (Model, MsgIn, MsgOut)

main : SandboxProgram Model MsgIn MsgOut
main =
    Sandbox.ui
        { title = "Title of your component"
        , component = YourComponent.component
        , cases =
            [ testCase1
            , testCase2
            ]
        , stringifyMsgIn = Debug.toString -- Or roll your own if you want prettier messages.
        , stringifyMsgOut = Debug.toString
        , wrapView = identity
        }

ui : { title : String, component : Webbhuset.Component.UI model msgIn msgOut, cases : List (TestCase msgIn msgOut), stringifyMsgIn : msgIn -> String, stringifyMsgOut : msgOut -> String, wrapView : Html msgIn -> Html msgIn } -> SandboxProgram model msgIn msgOut

Sandbox a UI Component

layout : { title : String, component : Webbhuset.Component.Layout model msgIn msgOut (Msg msgIn msgOut), cases : List (TestCase msgIn msgOut), stringifyMsgIn : msgIn -> String, stringifyMsgOut : msgOut -> String, wrapView : (msgIn -> Msg msgIn msgOut) -> Html (Msg msgIn msgOut) -> Html (Msg msgIn msgOut) } -> SandboxProgram model msgIn msgOut

Sandbox a Layout Component

service : { title : String, component : Webbhuset.Component.Service model msgIn msgOut, cases : List (TestCase msgIn msgOut), view : model -> Html msgIn, stringifyMsgIn : msgIn -> String, stringifyMsgOut : msgOut -> String } -> SandboxProgram model msgIn msgOut

Sandbox a Service Component

You need to provied a view function which renders the model of your service component.

Create a Test Case

Test cases defines scenarios for the requirements of your component.

A Test Case is just a record with a title and description together with a list of Actions you want to perform on your sandboxed component. You can also map the component's out messages to actions to simulate the outside system.


type alias TestCase msgIn msgOut =
{ title : String
, desc : String
, init : List (Action msgIn)
, onMsgOut : msgOut -> List (Action msgIn) 
}

A test case for the Component

Actions


type alias Action msgIn =
Layout.Action msgIn

An action to perform on your sandboxed component.

sendMsg : msgIn -> Action msgIn

Send a message to you sandboxed component

Sandbox.sendMsg YourComponent.SomeMessage

spawnChild : String -> (Webbhuset.Internal.PID.PID -> msgIn) -> Action msgIn

Spawn a child component and send the PID to your component.

You can provide a String which will be displayed when the child component is rendered (using renderPID in your layout component).

Sandbox.spawnChild "Hello child" YourComponent.ReceiveChild

delay : Basics.Float -> Action msgIn -> Action msgIn

Perform a delayed action on your sandboxed component. Delay in milliseconds.

Sandbox.sendMsg YourComponent.SomeMessage
    |> Sandbox.delay 1000

Assertions

Sometimes it is useful to test your expectations or requirements on a component.

You can express them using assertions. Assertions have three states: waiting, pass or fail. The state of a test case is visible in the sandbox UI.

In this example we expect that GoodMsg is sent by the component within 1s.

testGoodMsg : Sandbox.TestCase MsgIn MsgOut
testGoodMsg =
    { title = "Good messages are good"
    , desc = "`GoodMsg` must be sent within 1 second. No other messages are allowed."
    , init =
        [ Sandbox.timeout 1000
        , Sandbox.sendMsg YourComponent.SomeInput
        ]
    , onMsgOut = \msgOut ->
        case msgOut of
            YourComponent.GoodMsg ->
                [ Sandbox.pass
                ]

            YourComponent.BadMsg ->
                [ Sandbox.fail "I don't like bad messages"
                ]

pass : Action msgIn

Flag test case as passed.

Sandbox.pass

fail : String -> Action msgIn

Flag test case as failed. You can supply a message explaining what went wrong.

Sandbox.fail "Didn't receive some important out msg"

timeout : Basics.Float -> Action msgIn

Set a timeout in milliseconds. This will cause the test to automatically fail after the timeout if the test havn't been flagged as passed by then.

Sandbox.timeout 1000

Permutate the init order

Sometimes it is useful to test if the order of your init messages would affect the test result. One way to do so is by permuting all possible orders and test them.

permuteInitOrder : TestCase msgIn msgOut -> List (TestCase msgIn msgOut)

Take one test case and permute all possible orders of init messages.

Assert PIDs

mockPID : String -> Webbhuset.Internal.PID.PID

Create a mock PID for testing purposes.

mockPID "form-component"

checkPID : String -> Webbhuset.Internal.PID.PID -> Basics.Bool

Check that a mock pid matches an expected label.

This will return True

mockPID "form-component"
    |> checkPID "form-component"

assertPID : String -> Webbhuset.Internal.PID.PID -> Action msgIn

Assert that a PID matches a label.

This will result in action pass

mockPID "form-component"
    |> assertPID "form-component"

This will result in the action fail "PID form-component does not match expectation other-component"

mockPID "form-component"
    |> assertPID "other-component"


type alias Msg msgIn msgOut =
Webbhuset.ActorSystem.SysMsg ActorName (AppMsg msgIn msgOut)

Sadbox Msg