lamdera/program-test/1.0.0

NOTE: the latest version is 4.0.0

A Lamdera package. It is not on package.elm-lang.org: the Lamdera compiler provides it, from static.lamdera.com, so lamdera install works and elm install does not.

Effect.Test

Setting up the simulation

Config
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
-> String
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel

Setup a test.

import Backend
import Effect.Test
import Frontend
import Test exposing (Test)


config =
    { frontendApp = Frontend.appFunctions
    , backendApp = Backend.appFunctions
    , handleHttpRequest = always NetworkError_
    , handlePortToJs = always Nothing
    , handleFileRequest = always Nothing
    , domain = unsafeUrl "https://my-app.lamdera.app"
    }


test : Test
test =
    Effect.Test.start "myButton is clickable"
        |> Effect.Test.connectFrontend
            sessionId0
            myDomain
            { width = 1920, height = 1080 }
            (\( state, frontendActions ) ->
                state
                    |> frontendActions.clickButton { htmlId = "myButton" }
            )
        |> Effect.Test.toTest
type alias Config toBackend frontendMsg frontendModel toFrontend backendMsg backendModel =
{ frontendApp :
FrontendApp toBackend frontendMsg frontendModel toFrontend
, backendApp :
BackendApp toBackend toFrontend backendMsg backendModel
, handleHttpRequest :
{ currentRequest : HttpRequest
, pastRequests : List HttpRequest
}
-> Response Bytes
, handlePortToJs :
{ currentRequest : PortToJs
, pastRequests : List PortToJs
}
-> Maybe ( String, Value )
, handleFileRequest :
{ mimeTypes : List String }
-> Maybe
{ name : String
, mimeType : String
, content : String
, lastModified : Posix
}
, domain : Url
}

Configure the simulation before starting it

import Backend
import Effect.Test
import Frontend
import Test exposing (Test)


config =
    { frontendApp = Frontend.appFunctions
    , backendApp = Backend.appFunctions
    , handleHttpRequest = always NetworkError_
    , handlePortToJs = always Nothing
    , handleFileRequest = always Nothing
    , domain = unsafeUrl "https://my-app.lamdera.app"
    }


test : Test
test =
    Effect.Test.start "myButton is clickable"
        |> Effect.Test.connectFrontend
            sessionId0
            myDomain
            { width = 1920, height = 1080 }
            (\( state, frontendActions ) ->
                state
                    |> frontendActions.clickButton { htmlId = "myButton" }
            )
        |> Effect.Test.toTest
-> Url
-> { width : Int
, height : Int
}
-> ( ( Instructions
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
, FrontendActions
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel

Add a frontend client to the simulation!

import Effect.Test
import Test exposing (Test)


testApp =
    Effect.Test.testApp
        Frontend.appFunctions
        Backend.appFunctions
        (always NetworkError_)
        (always Nothing)
        (always Nothing)
        (unsafeUrl "https://my-app.lamdera.app")


test : Test
test =
    testApp "myButton is clickable"
        |> Effect.Test.connectFrontend
            sessionId0
            myDomain
            { width = 1920, height = 1080 }
            (\( state, frontendActions ) ->
                state
                    |> frontendActions.clickButton { htmlId = "myButton" }
            )
        |> Effect.Test.toTest
type alias FrontendApp toBackend frontendMsg frontendModel toFrontend =
{ init :
Url
-> Key
-> ( frontendModel, Command FrontendOnly toBackend frontendMsg )
, onUrlRequest : UrlRequest -> frontendMsg
, onUrlChange : Url -> frontendMsg
, update :
frontendMsg
-> frontendModel
-> ( frontendModel, Command FrontendOnly toBackend frontendMsg )
, updateFromBackend :
toFrontend
-> frontendModel
-> ( frontendModel, Command FrontendOnly toBackend frontendMsg )
, view : frontendModel -> Document frontendMsg
, subscriptions :
frontendModel -> Subscription FrontendOnly frontendMsg
}
type alias BackendApp toBackend toFrontend backendMsg backendModel =
{ init :
( backendModel, Command BackendOnly toFrontend backendMsg )
, update :
backendMsg
-> backendModel
-> ( backendModel, Command BackendOnly toFrontend backendMsg )
, updateFromFrontend :
-> toBackend
-> backendModel
-> ( backendModel, Command BackendOnly toFrontend backendMsg )
, subscriptions :
backendModel -> Subscription BackendOnly backendMsg
}
type alias HttpRequest =
{ requestedBy : RequestedBy
, method : String
, url : String
, body : HttpBody
, headers : List ( String, String )
}
= RequestedByFrontend ClientId
| RequestedByBackend

Who made this http request?

type alias PortToJs =
{ clientId : ClientId
, portName : String
, value : Value
}

Control the simulation

type alias FrontendActions toBackend frontendMsg frontendModel toFrontend backendMsg backendModel =
{ clientId : ClientId
, keyDownEvent :
-> { keyCode : Int }
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
, clickButton :
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
, inputText :
-> String
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
, clickLink :
{ href : String }
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
, checkView :
(Single frontendMsg -> Expectation)
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
, snapshotView :
{ name : String }
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
}

FrontendActions contains the possible functions we can call on the client we just connected.

import Effect.Test
import Test exposing (Test)


testApp =
    Effect.Test.testApp
        Frontend.appFunctions
        Backend.appFunctions
        (always NetworkError_)
        (always Nothing)
        (always Nothing)
        (unsafeUrl "https://my-app.lamdera.app")


test : Test
test =
    testApp "myButton is clickable"
        |> Effect.Test.connectFrontend
            sessionId0
            myDomain
            { width = 1920, height = 1080 }
            (\( state, frontendActions ) ->
                -- frontendActions is a record we can use on this specific frontend we just connected
                state
                    |> frontendActions.clickButton { htmlId = "myButton" }
            )
        |> Effect.Test.toTest
-> toBackend
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel

Normally you won't send data directly to the backend and instead use connectFrontend followed by things like clickButton or inputText to cause the frontend to send data to the backend. If you do need to send data directly, then you can use this though.

Duration
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel

Simulate the passage of time. This will trigger any subscriptions like Browser.onAnimationFrame or Time.every along the way.

If you need to simulate a large passage of time and are finding that it's taking too long to run, try fastForward instead.

Duration
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel

Similar to simulateTime but this will not trigger any Browser.onAnimationFrame or Time.every subscriptions.

This is useful if you need to move the clock forward a week and it would take too long to simulate it perfectly.

( State
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel

Sometimes you need to decide what should happen next based on some simulation state. In order to do that you can write something like this:

state
    |> TF.andThen
        (\state2 ->
            case List.filterMap isLoginEmail state2.httpRequests |> List.head of
                Just loginEmail ->
                    TF.continueWith state2
                            |> testApp.connectFrontend
                                sessionIdFromEmail
                                (loginEmail.loginUrl)
                                (\( state3, clientIdFromEmail ) ->
                                    ...
                                )


                Nothing ->
                    TF.continueWith state2 |> TF.checkState (\_ -> Err "Should have gotten a login email")
        )
State
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel

Sometimes you need to decide what should happen next based on some simulation state. In order to do that you can write something like this:

state
    |> TF.andThen
        (\state2 ->
            case List.filterMap isLoginEmail state2.httpRequests |> List.head of
                Just loginEmail ->
                    TF.continueWith state2
                            |> testApp.connectFrontend
                                sessionIdFromEmail
                                (loginEmail.loginUrl)
                                (\( state3, clientIdFromEmail ) ->
                                    ...
                                )


                Nothing ->
                    TF.continueWith state2 |> TF.checkState (\_ -> Err "Should have gotten a login email")
        )
type Instructions toBackend frontendMsg frontendModel toFrontend backendMsg backendModel
type alias State toBackend frontendMsg frontendModel toFrontend backendMsg backendModel =
{ testName : String
, frontendApp :
FrontendApp toBackend frontendMsg frontendModel toFrontend
, backendApp :
BackendApp toBackend toFrontend backendMsg backendModel
, backend : backendModel
, pendingEffects : Command BackendOnly toFrontend backendMsg
, frontends :
Dict
(FrontendState toBackend frontendMsg frontendModel toFrontend)
, counter : Int
, elapsedTime : Duration
, toBackend : List ( SessionId, ClientId, toBackend )
, timers :
Dict
Duration
{ msg : Posix -> backendMsg
, startTime : Posix
}
, testErrors : List TestError
, httpRequests : List HttpRequest
, handleHttpRequest :
{ currentRequest : HttpRequest
, pastRequests : List HttpRequest
}
-> Response Bytes
, handlePortToJs :
{ currentRequest : PortToJs
, pastRequests : List PortToJs
}
-> Maybe ( String, Value )
, portRequests : List PortToJs
, handleFileRequest : { mimeTypes : List String } -> Maybe File
, domain : Url
, snapshots :
List
{ name : String
, body : List (Html frontendMsg)
, width : Int
, height : Int
}
}
startTime : Posix
= EmptyBody
| StringBody
{ contentType : String
, content : String
}
| JsonBody Value
| MultipartBody (List HttpPart)
| BytesBody String Bytes
| FileBody File
= StringPart String String
| FilePart String File
| BytesPart
{ key : String
, mimeType : String
, content : Bytes
}

Test the simulation

( State
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
-> Result String ()
)
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
(backendModel -> Result String ())
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
-> Test
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
-> List (Snapshot frontendMsg)

Get all snapshots from a test. This can be used with Effect.Snapshot.uploadSnapshots to perform visual regression testing.

Miscellaneous

Only use this for tests!

List
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)
-> Program () (Model frontendModel) Msg

View your end-to-end tests in a elm reactor style app.

type Msg
type alias Model frontendModel =
{ navigationKey : Key
, currentTest : Maybe (TestView frontendModel)
, testResults : List (Result TestError ())
}