Gren Effectful Test Runner

Write integration, end-to-end, and browser tests in Gren.

This package lets you write:

Quick Start

Create a directory for your tests and initialize a gren program targeting node:

mkdir tests
cd tests
gren init --platform=node

Install the package and dependencies:

gren package install gren-lang/test
gren package install gren-lang/test-runner-node
gren package install blaix/gren-effectful-tests

Create a src/Main.gren with your tests:

module Main exposing (main)

import Expect
import Node
import Task
import Test.Runner.Effectful as Effectful exposing (..)

tests : Effectful.Test
tests =
    describe "My tests"
        [ await "my task" (Task.succeed "ok") <| \result ->
            test "is ok" <| \_ ->
                Expect.equal "ok" result
        , awaitError "my error" (Task.fail "oops") <| \error ->
            test "is oops" <| \_ ->
                Expect.equal "oops" error
        ]
        
main : Node.SimpleProgram a
main =
    Node.defineSimpleProgram <| \env ->
        Effectful.initialize env <| \config ->
            Effectful.run config tests
                |> Node.endSimpleProgram

Then compile and run with:

gren run Main

Usage

This package provides wrappers for the normal gren-lang/test functions, plus additional functions that let you await tasks, drive a browser, and more.

Effectful.initialize (and Brwoser.initialize) gather permissions and default configuration. If you want total control over configuration and what permissions the runner has access to, see the Subsystems and Permissions section below.

Effectful.run turns your test suite into a Task that you can run however you want. In the example above, we're using it as the task for a Node.SimpleProgram, but you can run it in any type of program just like you would any other task.

Browser.withSession lets you call tasks that can drive a real browser. See the Browser tests section below for details.

See the package docs for the full API. Or read on for specific usage examples.

Tasks

Your test can await the result of a task:

await "the current time" Time.now <| \now ->
    test "is not Jan 1, 1970" <| \_ ->
        Expect.notEqual (Time.millisToPosix 0) now

Errors

If you try to await a task that fails, the test will fail.

If you want to explicitly test the failure condition, use awaitError:

awaitError "expected failure" (Task.fail "oopsy") <| \error ->
    test "is an oopsy" <| \_ ->
        Expect.equal "oopsy" error

Nested awaits

You can nest awaits as deep as you need:

await "task a" (Task.succeed "a") <| \a ->
await "task b" (Task.succeed "b") <| \b ->
awaitError "failed task" (Task.fail "failure") <| \error ->
    test "nested tasks" <| \_ ->
        Expect.equalArrays
            [ "a", "b", "failure" ]
            [ a, b, error ]

Browser tests

You can also test an app running in a browser using Test.Runner.Browser to load pages, click buttons, fill out and submit forms, verify content, etc.

It requires chromedriver to be installed and available on your PATH. You can get it via most package managers (e.g. npm install -g chromedriver) or download it directly.

See the Subsystems and Permissions section below for details on getting the permissions.

myBrowserTests : Browser.Permissions -> Effectful.Test
myBrowserTests browserPerms =
    Browser.withSession browserPerms Browser.options <| \session ->
        await "navigate to form" (Browser.navigateTo "http://localhost:3000/form" session) <| \_ ->
        await "find name input" (Browser.findElement "#name-input" session) <| \input ->
        await "type name" (Browser.sendKeys "Gren" input session) <| \_ ->
        await "find submit button" (Browser.findElement "#submit-btn" session) <| \submitBtn ->
        await "click submit" (Browser.click submitBtn session) <| \_ ->
        await "find greeting" (Browser.findElement "#greeting" session) <| \greeting ->
        await "get greeting text" (Browser.getText greeting session) <| \greetingText ->
            test "message shows expected text" <| \_ ->
                Expect.equal "Hello, Gren!" greetingText

As part of a long-running program

Since Effectful.run returns a Task, you can use it anywhere you can use a task, including in an update function of a long-running program:

update msg model =
    when msg is
        RunTests ->
            { model = model
            , command = 
                Task.perform 
                    (\_ -> TestsDone)
                    (Effectful.run model.testConfig myTests)
            }

Subsystems and Permissions

The package needs a number of subsystem permissions. It provides some convenience initialize functions to gather those in a single call:

import Node
import Test.Runner.Browser as Browser
import Test.Runner.Effectful as Effectful


main : Node.SimpleProgram a
main =
    Node.defineSimpleProgram <| \env ->
        
        -- Gather permissions and default configuration:
        Effectful.initialize env <| \runnerConfig ->
        Browser.initialize <| \browserPerms ->
        
            Node.endSimpleProgram <|
                Effectful.run runnerConfig <|
                    Browser.withSession browserPerms Browser.options <| \session ->
                        myBrowserTests session

Note: Since these initialize functions return an Init.Task, using them essentially grants the package unlimited access to subsystems (filesystem, child processes, etc).

If you want explicit control over the config or what the package has access to, you can pass the permissions in yourself:

import Init
import Node
import ChildProcess
import FileSystem
import HttpClient
import Terminal
import Test.Runner.Browser as Browser
import Test.Runner.Effectful as Effectful


main : Node.SimpleProgram a
main =
    Node.defineSimpleProgram <| \env ->
        
        -- Gather permissions:
        Init.await Terminal.initialize <| \termConfig ->
        Init.await ChildProcess.initialize <| \cpPerm ->
        Init.await FileSystem.initialize <| \fsPerm ->
        Init.await HttpClient.initialize <| \httpPerm ->
        
            let
                runnerConfig =
                    { termConfig = termConfig
                    , stdout = env.stdout
                    , stderr = env.stderr
                    }
                
                browserPerms =
                    { cpPerm = cpPerm
                    , fsPerm = fsPerm
                    , httpPerm = httpPerm
                    }
            in
            Node.endSimpleProgram <|
                Effectful.run runnerConfig <|
                    Browser.withSession browserPerms Browser.options <| \session ->
                        myBrowserTests session

Working on this package

This project uses devbox for local development. See devbox.json for the scripts you can run and lists of dependencies it will install (or that you can install manually if you don't want to use devbox).

Ensure any changes are exercised in this project's tests.

Contact and Support

Found a problem? Need some help? You can open an issue, find me on mastodon, or on the Gren Discord.