lamdera/program-test/2.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 end to end tests

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

Start a end-to-end test

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


config =
    { frontendApp = Frontend.appFunctions
    , backendApp = Backend.appFunctions
    , handleHttpRequest = always Effect.Test.NetworkErrorResponse
    , handlePortToJs = always Nothing
    , handleFileUpload = always Effect.Test.UnhandledFileUpload
    , handleMultipleFilesUpload = always Effect.Test.UnhandledMultiFileUpload
    , 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


unsafeUrl : String -> Url
unsafeUrl urlText =
    case Url.fromString urlText of
        Just url ->
            url


        Nothing ->
            unsafeUrl urlText
type alias Config toBackend frontendMsg frontendModel toFrontend backendMsg backendModel =
{ frontendApp :
FrontendApp toBackend frontendMsg frontendModel toFrontend
, backendApp :
BackendApp toBackend toFrontend backendMsg backendModel
, handleHttpRequest :
{ data : Data frontendModel backendModel
, currentRequest : HttpRequest
}
, handlePortToJs :
{ data : Data frontendModel backendModel
, currentRequest : PortToJs
}
-> Maybe ( String, Value )
, handleFileUpload :
{ data : Data frontendModel backendModel
, mimeTypes : List String
}
, handleMultipleFilesUpload :
{ data : Data frontendModel backendModel
, mimeTypes : List String
}
, domain : Url
}

Configure the end to end test before starting it

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


config =
    { frontendApp = Frontend.appFunctions
    , backendApp = Backend.appFunctions
    , handleHttpRequest = always Effect.Test.UnhandledHttpRequest
    , handlePortToJs = always Nothing
    , handleFileUpload = always Effect.Test.UnhandledFileUpload
    , handleMultipleFilesUpload = always Effect.Test.UnhandledMultiFileUpload
    , 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


unsafeUrl : String -> Url
unsafeUrl urlText =
    case Url.fromString urlText of
        Just url ->
            url


        Nothing ->
            unsafeUrl urlText
-> 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 end to end test

import Effect.Test
import Test exposing (Test)
import Url


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


unsafeUrl : String -> Url
unsafeUrl urlText =
    case Url.fromString urlText of
        Just url ->
            url


        Nothing ->
            unsafeUrl urlText
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 )
, sentAt : Posix
}
= BadUrlResponse String
| TimeoutResponse
| NetworkErrorResponse
| BadStatusResponse Metadata String
| BytesHttpResponse Metadata Bytes
| StringHttpResponse Metadata String
| JsonHttpResponse Metadata Value
| TextureHttpResponse Metadata Texture
| UnhandledHttpRequest

The response for an http request. Note that if the http request was expecting one form of data (for example json) and the response contains a different type of data (for example a String) then the data will automatically be converted. The exception to this is Texture. If the request is expecting a Texture (this will only happen with Effect.WebGL.Texture.load and Effect.WebGL.Texture.loadWith) then the response has to contain a Texture if you want the request to succeed. In other words, sending the Bytes that represent that texture won't work.

= RequestedByFrontend ClientId
| RequestedByBackend

Who made this http request?

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

File data for when simulating a user uploading a file via Effect.File.Select.file or Effect.File.Select.files

= CancelFileUpload
| UploadFile FileData
| UnhandledFileUpload

Possible simulated user actions for when Effect.File.Select.file is triggered.

= CancelMultipleFilesUpload
| UploadMultipleFiles FileData (List FileData)
| UnhandledMultiFileUpload

Possible simulated user actions for when Effect.File.Select.files is triggered.

uploadBytesFile : String -> String -> Bytes -> Posix -> FileData

Create a file upload containing binary data

uploadStringFile : String -> String -> String -> Posix -> FileData

Create a file upload containing text data

type alias Data frontendModel backendModel =
{ httpRequests : List HttpRequest
, portRequests : List PortToJs
, time : Posix
, backend : backendModel
, frontends : Dict ClientId frontendModel
, downloads :
List
{ filename : String
, mimeType : String
, content : FileContents
, downloadedAt : Posix
}
}
= StringFile String
| BytesFile Bytes

Control the tests

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
, update :
frontendMsg
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)
import Url


config =
    { frontendApp = Frontend.appFunctions
    , backendApp = Backend.appFunctions
    , handleHttpRequest = always Effect.Test.UnhandledHttpRequest
    , handlePortToJs = always Nothing
    , handleFileUpload = always Effect.Test.UnhandledFileUpload
    , handleMultipleFilesUpload = always Effect.Test.UnhandledMultiFileUpload
    , 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 ) ->
                -- frontendActions is a record we can use on this specific frontend we just connected
                state
                    |> frontendActions.clickButton { htmlId = "myButton" }
            )
        |> Effect.Test.toTest


unsafeUrl : String -> Url
unsafeUrl urlText =
    case Url.fromString urlText of
        Just url ->
            url


        Nothing ->
            unsafeUrl urlText
-> 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.

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

Directly trigger an update on the backend. Normally you shouldn't need to do this as things like Time.every and completed tasks will generate updates automatically.

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.

( Data frontendModel backendModel
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 current state. In order to do that you can write something like this:

import Effect.Test


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


                Nothing ->
                    Effect.Test.continueWith state2 |> Effect.Test.checkState (\_ -> Err "Should have gotten a login email")
        )
type Instructions toBackend frontendMsg frontendModel toFrontend backendMsg backendModel
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
}

Check the current state

(Data frontendModel backendModel -> Result String ())
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel

Make the test fail if it doesn't seem some condition.

(backendModel -> Result String ())
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel

Make the test fail if the backend model doesn't seem some condition.

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.

Test viewer

Sometimes it's hard to tell what's going on in an end to end test. One way to make this easier to use the viewer function. It's like a test runner for your browser that also lets you see the frontend of an app as simulated inputs are being triggered.

Only use this for tests!

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

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

    main =
        viewer [{- End to end tests go here -}]
type Msg toBackend frontendMsg frontendModel toFrontend backendMsg backendModel
type alias Model toBackend frontendMsg frontendModel toFrontend backendMsg backendModel =
{ navigationKey : Key
, currentTest :
Maybe
(TestView
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)
, testResults : List (Result TestError ())
, tests :
Maybe
(Result
FileLoadError
(List
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)
)
)
, windowSize : ( Int, Int )
}
viewerWith : a -> ViewerWith a

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

import Effect.Test


main =
    Effect.Test.viewerWith
        (\image jsonData ->
            [{- End to end tests go here -}]
        )
        |> Effect.Test.addBytesFile "/test.png"
        |> Effect.Test.addBytesFile "/data.json"
        |> Effect.Test.startViewer
type alias ViewerWith a =
{ cmds : Task FileLoadError a }
ViewerWith
(List
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)
)
-> Program
()
(Model
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)
(Msg
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)

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

import Effect.Test


main =
    Effect.Test.viewerWith
        (\image jsonData ->
            [{- End to end tests go here -}]
        )
        |> Effect.Test.addBytesFile "/test.png"
        |> Effect.Test.addBytesFile "/data.json"
        |> Effect.Test.startViewer
addStringFile : String -> ViewerWith (String -> b) -> ViewerWith b

Add a file containing text data to your tests.

import Effect.Test


main =
    Effect.Test.viewerWith
        (\text jsonData ->
            [{- End to end tests go here -}]
        )
        |> Effect.Test.addStringFile "/test.txt"
        |> Effect.Test.addStringFile "/data.json"
        |> Effect.Test.startViewer
addBytesFile : String -> ViewerWith (Bytes -> b) -> ViewerWith b

Add a file containing binary data to your tests.

import Effect.Test


main =
    Effect.Test.viewerWith
        (\image jsonData ->
            [{- End to end tests go here -}]
        )
        |> Effect.Test.addBytesFile "/test.png"
        |> Effect.Test.addBytesFile "/data.json"
        |> Effect.Test.startViewer
addTexture : String -> ViewerWith (Texture -> b) -> ViewerWith b

Add a file containing data for a Effect.WebGL.Texture.Texture to your tests. Right now this is performed with HTTP get requests which means you can only access files in /public (or make get requests to other websites though this isn't recommended since this API might change in the future)

import Effect.Test


main =
    Effect.Test.viewerWith
        (\texture ->
            [{- End to end tests go here -}]
        )
        |> Effect.Test.addTextureFile "/texture.png"
        |> Effect.Test.startViewer
addTextureWithOptions : Options -> String -> ViewerWith (Texture -> b) -> ViewerWith b

Add a file containing data for a Effect.WebGL.Texture.Texture to your tests. Right now this is performed with HTTP get requests which means you can only access files in /public (or make get requests to other websites though this isn't recommended since this API might change in the future)

import Effect.Test


main =
    Effect.Test.viewerWith
        (\texture ->
            [{- End to end tests go here -}]
        )
        |> Effect.Test.addTextureFileWithOptions
            -- WebGL texture options go here
            "/texture.png"
        |> Effect.Test.startViewer

Test runner

If you want to just run the end to end tests to make sure they work, or automatically check that they pass in your CI pipeline then you can setup a headless test runner with this function:

( Value
-> Cmd
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)
)
-> ViewerWith
(List
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)
)
-> Program
()
()
toBackend
frontendMsg
frontendModel
toFrontend
backendMsg
backendModel
)

Create a headless test runner.

import Effect.Test


main =
    Effect.Test.viewerWith
        (\image jsonData ->
            [{- End to end tests go here -}]
        )
        |> Effect.Test.addBytesFile "/test.png"
        |> Effect.Test.addBytesFile "/data.json"
        |> Effect.Test.startHeadless
type HeadlessMsg toBackend frontendMsg frontendModel toFrontend backendMsg backendModel

Msg type for a headless end to end test runner.