arowM/tepa/1.2.0

NOTE: the latest version is 6.0.0

Tepa.Scenario

Module for Scenario-Driven Development.

Core

type Scenario memory

Scenario describes how the application reacts to the user operations along the time line.

The Scenario you built can be converted to tests with toTest, and to documents with toMarkdown.

A Scenario that does nothing.

sequence : List (Scenario m) -> Scenario m

Return a new Scenario that evaluates given Scenarios sequentially.

{ props : ApplicationProps memory
, sections : List (Section memory)
, origin :
{ secure : Bool
, hostname : String
, port_ : Maybe Int
}
}
-> Test

Generate scenario tests.

{ title : String
, sections : List (Section m)
, config : RenderConfig
}
-> Result InvalidMarkup String

Generate scenario document markdown text.

= InvalidFromJust String
| InvalidFromOk String
| NoDependentSection String
| DuplicatedSection String
| ParameterNotFound String String
type alias Section memory =
{ title : String
, content : List (Scenario memory)
, dependency : Dependency
}

Titled sequence of Scenarios.

  • title: Title for the Section, which must be unique string
  • content: Sequence of Scenarios for the Section
  • dependency: Dependency of the Section

You may want to branch out in the middle of your scenario. In such case, you can declare common section, and refer to the title in dependency parameter:

import TimeZone




-- justinmimbs/timezone-data
myTest : Test
myTest =
    toTest
        { props = Debug.todo "props"
        , sections =
            [ commonScenario
            , caseA
            , caseB
            ]
        }


commonScenario : Section Flags Command Memory Event
commonScenario =
    { title = "Common scenario"
    , content =
        [ Debug.todo "Common scenarios"
        , Debug.todo "..."
        ]
    , dependency =
        EntryPoint
            (TimeZone.asia__tokyo ())
            (Time.millisToPosix 1672531200000)
    }


caseA : Section Flags Command Memory Event
caseA =
    { title = "Case A"
    , content =
        -- After common scenario,
        [ Debug.todo "User clicks button A"
        , Debug.todo "..."
        ]
    , dependency = RunAfter commonScenario.title
    }


caseB : Section Flags Command Memory Event
caseB =
    { title = "Case B"
    , content =
        -- After common scenario,
        [ Debug.todo "User clicks button B"
        , Debug.todo "..."
        ]
    , dependency = RunAfter commonScenario.title
    }
= EntryPoint Zone Posix
| RunAfter String

Dependency of a Section.

  • EntryPoint zone time: Indicates that the Section has no dependencies and starts at the specified time in zone.
  • RunAfter sectionTitle: Indicates that the Section is after another Section specified by the sectionTitle.

User

type User

An application user.

defineUser : { name : String } -> User

Define a user for your Scenario.

Session

type Session

Session is a unit that connects one application instance and its user. Basically, it corresponds to a tab in a browser.

So, for example, if you want to create a scenario where a user opens and operates two tabs, you need to define two separate sessions for the same user:

sakuraChan : User
sakuraChan =
    defineUser
        { name = "Sakura-chan"
        }


sakuraChanMainSession : Session
sakuraChanMainSession =
    defineSession
        { user = sakuraChan
        , name = "Main tab on the Sakura-chan's machine"
        }


sakuraChanSecondSession : Session
sakuraChanSecondSession =
    defineSession
        { user = sakuraChan
        , name = "Second tab on the Sakura-chan's machine"
        }
{ uniqueName : String
, user : User
}

Define a session for your Scenario.

Markup

type Markup

Represents markup for a scenario.

Suppose you have the following Markup, which uses arowM/elm-markdown-ast:

sample : Markup
sample =
    markup """
    `content` for the list item


    `detail` for the list item


    ```json
    {
      "code": "Next `detail` for the list item"
    }
    ```
    """

The sample represents the bellow markdown list item:

- `content` for the list item


    `detail` for the list item


    ```json
    {
      "code": "Next `detail` for the list item"
    }
    ```

You can use hide to skip the item from appearing up in the document, which allows you to generate documents for various targets:

type DocTarget
    = Developer
    | Manager
    | Customer


docLevelDev : DocTarget -> Markup -> Markup
docLevelDev target =
    hide <|
        case target of
            Developer ->
                False


            Manager ->
                True


            Customer ->
                True


myScenario : DocTarget -> Scenario Flags Command Memory Event
myScenario target =
    [ Debug.todo "After some operations..."
    , expectEvents sakuraChanMainSession
        (markup """
          Requests user profile to the server.
          """
            |> docLevelDev
        )
        (Debug.todo "Expectation Here")
    , Debug.todo "..."
    ]
markup : String -> Markup

Constructor for Markup.

Markup:

markup "No detail"

Rendered to:

- No detail

Markup:

markup """
This is **content**.
You can also provide detail informations.


```json
{
  "foo": 3
}
```
"""

Rendered:

- This is **content**.


    You can also provide detail informations.


    ```json
    {
      "foo": 3
    }
    ```

Markup:

markup """
You can interpolate value by `\\{{key}}` syntax.
Here is the sample: {{message}}
By appending `|raw`, you can avoid sanitizing: {{message|raw}}


The `\\{{key|json}}` syntax can embed the JSON structure from a string generated by `Debug.toString`.
Sample:


{{sampleStructure|json}}
"""
|> setParam "message" "Hello!"
|> setParam "sampleStructure"
    ( Debug.toString <|
        { "foo": 3
        , "bar": "baz"
        }
    )

Rendered:

- You can interpolate value by `{{key}}` syntax.


    Here is the sample: Hello\!
    By appending `|raw`, you can avoid sanitizing: Hello!


    The `{{key|json}}` syntax can embed the JSON structure from a string generated by `Debug.toString`.
    Sample:


    ```json
    {
        "foo": 3,
        "bar": "baz"
    }
    ```
hide : Bool -> Markup -> Markup

Hide the Markup from rendered scenario document.

setParam : String -> String -> Markup -> Markup

Specify values for embedded variables.

modifyContent : (String -> String) -> Markup -> Markup

Modify Markup content part.

Markup:

markup """
This is **content**.
You can also provide detail informations.


```json
{
  "foo": 3
}
```
"""
    |> modifyContent (String.prepend "(notation) ")

Rendered to:

- (notation) This is **content**.


    You can also provide detail informations.


    ```json
    {
      "foo": 3
    }
    ```

Primitives

Comments

userComment : User -> String -> Scenario m

User comment.

myScenario =
    [ userComment sakuraChan
        "Hi. I'm Sakura-chan, the cutest goat girl in the world."
    , userComment sakuraChan
        "Today I'll try a goat management service."
    , Debug.todo "..."
    ]

This Scenario only affects document generation, and is ignored for scenario test generation.

You can start with userComment and systemComment to build the skeleton of your scenario, and gradually replace userComment with Event Simulator and systemComment with Expectation.

systemComment : Session -> String -> Scenario m

System comment.

This Scenario only affects document generation, and is ignored for scenario test generation.

Lower level function to add detailed comments.

Stubs

Generates documentation, but the test always fails.

You can create a scenario first with todo and later replace that todo with an actual test, which is the scenario driven development.

Similar todo, but for user actions.

It prepends the username to the markup.

Expectations

-> Markup
-> { layer : Layer m -> Maybe (Layer m1)
, expectation : m1 -> Expectation
}
-> Scenario m

Describe your expectations for the application memory state at the point.

Suppose your application has a counter:

import Expect


myScenario =
    [ Debug.todo "After some operations..."
    , expectMemory sakuraChanMainSession
        (markup "The counter must be less than four.")
        { layer =
            pageHomeLayer
        , expectation =
            \pageHomeMemory ->
                pageHomeMemory.counter
                    |> Expect.lessThan 4
        }
    , Debug.todo "..."
    ]

You use Expect module to describe your expectation.

-> Markup
-> { expectation : Document -> Expectation }
-> Scenario m

Describe your expectations for the application's view at the point.

Suppose your application has a popup:

import Html.Attribute exposing (attribute)
import Test.Html.Query as Query
import Test.Html.Selector as Selector


myScenario =
    [ Debug.todo "After some operations..."
    , expectAppView sakuraChanMainSession
        (markup "Show popup message.")
        { expectation =
            \{ body } ->
                Query.fromHtml (Html.div [] body)
                    |> Query.find [ Selector.id "popup" ]
                    |> Query.has
                        [ Selector.attribute
                            (attribute "aria-hidden" "false")
                        ]
        }
    , Debug.todo "..."
    ]

You use Expect module to describe your expectation.

Note that the expectation field takes whole page view even if you use it in onLayer function.

onLayer popup
    [ expectAppView sakuraChanMainSession
        (markup "expectation about the whole application view")
        { expectation =
            \html ->
                Debug.todo
                    "the argument is not the partial view for the Layer, but for the whole page."
        }
    , Debug.todo "..."
    ]
-> Markup
-> { layer : Layer m -> Maybe (Layer m1)
, expectation : Dict String String -> Expectation
}
-> Scenario m

Describe your expectations for the user input values in the specified Layer at the point.

You use Expect module to describe your expectation.

expectCurrentTime : Markup -> { expectation : Posix -> Expectation } -> Scenario m

Describe your expectations for the emulated current time of the application.

Suppose you want to check current time after sleep.

import Expect
import Tepa.Scenario as Scenario
import Time
import TimeZone -- justinmimbs/timezone-data




sample : Section
sample =
    { title = "Sample Scenario"
    , dependency =
        Scenario.EntryPoint
            (TimeZone.asia__tokyo ())
            (Time.millisToPosix 1672531200000)
    , content =
        [ userComment sakuraChan "I'm trying to access the Goat SNS."
        , Scenario.sleep (Scenario.markup "Passing one minutes.")
        , userComment sakuraChan "Oops, I've slept a little."
        , let
            curr = Time.millisToPosix <| 1672531200000 + 1 * 60 * 1000
          in
          Scenario.expectCurrentTime
            (Scenario.markup <| "Current time is: " ++ formatPosix curr ++ ".")
            { expectation =
                Expect.equal curr
            }
        ]

You use Expect module to describe your expectation.

-> Markup
-> { layer : Layer m -> Maybe (Layer m1)
, expectation : List HttpRequest -> Expectation
}
-> Scenario m

Describe your expectations for the unresolved HTTP requests at the time.

You use Expect module to describe your expectation.

-> Markup
-> { layer : Layer m -> Maybe (Layer m1)
, expectation : List PortRequest -> Expectation
}
-> Scenario m

Describe your expectations for the unresolved Port requests at the time.

You use Expect module to describe your expectation.

-> Markup
-> { layer : Layer m -> Maybe (Layer m1)
, spec : Spec a
}
-> Scenario m

Describe your expectations for the unresolved Random requests at the time.

You pass Tepa.Random.Spec to specify your expected request.

Helper functions to specify Layer

appLayer : Layer m -> Maybe (Layer m)

Specifies the application root layer, which is just an alias for Just.

(m1 -> Maybe (Layer m2))
-> (Layer m -> Maybe (Layer m1))
-> Layer m
-> Maybe (Layer m2)

Helper function to specify child layer.

type alias Parent =
    { child : Maybe (Layer Child)
    }


myParentLayer : m -> Maybe (Layer Parent)
myParentLayer =
    Debug.todo "parent Layer"


myChildLayer : m -> Maybe (Layer Child)
myChildLayer =
    myParentLayer
        |> childLayer .child
(m1 -> m2)
-> (Layer m -> Maybe (Layer m1))
-> Layer m
-> Maybe (Layer m2)

Event Simulators

-> Markup
-> { path : AppUrl
, flags : Value
}
-> Scenario m

Load the app. You can also reload the app by calling loadApp.

import AppUrl exposing (AppUrl)
import Dict
import Json.Encode as JE


myScenario =
    [ userComment sakuraChan
        "Hi. I'm Sakura-chan, the cutest goat girl in the world."
    , userComment sakuraChan
        "I'll open the home page..."
    , loadApp sakuraChanMainSession
        (markup "Load the home page.")
        { path =
            { path = []
            , query = Dict.empty
            , fragment = Nothing
            }
        , flags =
            JE.object []
        }
    , systemComment sakuraChanMainSession
        "Show home page."
    , userComment sakuraChan
        "Oops, I accidentally hit the F5 button..."
    , loadApp sakuraChanMainSession
        (markup "Reload the page.")
        { path =
            { path = []
            , query = Dict.empty
            , fragment = Nothing
            }
        , flags =
            JE.object []
        }
    , Debug.todo "..."
    ]

Close the app.

-> Markup
-> { query : Single Msg -> Single Msg
, operation : ( String, Value )
}
-> Scenario m

About options:

  • layer: Query to specify the event target element from your current page HTML.

    Use querying functions that Test.Html.Query module exports.

  • operation: Simulated event caused by user operation.

    Simulate a custom event. The String is the event name, and the Value is the event object the browser would send to the event listener callback.

Note that current version does not trigger page transition on clicking anchor elements. Alternatively, you can check the anchor has expected href value with expectAppView, and pushPath to the path.

sleep : Markup -> Int -> Scenario m

Wait for given micro seconds.

For TEA users: It only affects Promises defined in Tepa.Time, so you should not use Time module and Process.sleep with TEPA.

Http response Simulators

-> Markup
-> { layer : Layer m -> Maybe (Layer m1)
, response : HttpRequest -> Maybe ( Metadata, String )
}
-> Scenario m

Simulate response to the Tepa.Http.request and Tepa.Http.bytesRequest.

If no Layers found for the query, it does nothing and just passes the test.

-> Markup
-> { layer : Layer m -> Maybe (Layer m1)
, response : HttpRequest -> Maybe ( Metadata, Bytes )
}
-> Scenario m

Similar to httpResponse, but responds with Bytes.

type alias HttpRequest =
{ method : String
, headers : Dict String String
, url : String
, requestBody : HttpRequestBody
}

Http request that your web API server will receive:

  • method like GET and PUT, all in upper case
  • headers like accept and cookie, all names in lower case, and all values as it is
  • url
  • requestBody

Note: It is possible for a request to have the same header multiple times. In that case, all the values end up in a single entry in the headers dictionary. The values are separated by commas.

= EmptyHttpRequestBody
| StringHttpRequestBody String String
| JsonHttpRequestBody Value
| FileHttpRequestBody File
| BytesHttpRequestBody String Bytes

Port response Simulators

-> Markup
-> { layer : Layer m -> Maybe (Layer m1)
, response : PortRequest -> Maybe Value
}
-> Scenario m

Simulate response to the Tepa.portRequest or Tepa.listenPortStream.

Suppose your application requests to access localStorage via port request named "Port to get page.account.bio":

import Json.Encode as JE


myScenario =
    [ Debug.todo "After request to the port..."
    , portResponse sakuraChanMainSession
        (markup "Received response.")
        { layer = "Port to get page.account.bio"
        , response =
            \_ ->
                JE.string "I'm Sakura-chan."
        }
    , Debug.todo "..."
    ]

If no Layers found for the query, it does nothing and just passes the test.

type alias PortRequest =
{ name : String
, requestBody : Value
}

Port request:

  • name to identify the port, which you passed as portName field of portRequest or portStream.
  • requestBody, which you passed as requestBody field of portRequest or portStream.

Random response Simulators

-> Markup
-> { layer : Layer m -> Maybe (Layer m1)
, spec : Spec a
, response : a
}
-> Scenario m

Simulate response to the Tepa.Random.request.

Suppose your application requests random integer:

import Tepa.Random as Random
import Tepa.Scenario as Scenario


oneToTen : Random.Spec Int
oneToTen =
    Random.int 1 10


respondToOneToTenInt : Scenario m
respondToOneToTenInt =
    Scenario.randomResponse
        mySession
        (Scenario.markup "Respond `1` to the first unresolved `oneToTen` request.")
        { layer = myLayer
        , spec = oneToTen
        , value = 1
        }


-- If there is no unresolved `oneToTen` request at the time, the test fails.

If no Layers found for the query, it does nothing and just passes the test.

Browser Simulators

Simulate borwser forward event.

If there are no pages to forward, the test fails.

Simulate borwser back event.

If there are no pages to back, the test fails.

pushPath : Session -> AppUrl -> Markup -> Scenario m

Simulate page transition.

Conditions

fromJust : String -> Maybe a -> (a -> List (Scenario m)) -> Scenario m

Extract Just value.

If the given value is Nothing, document generation and tests fails.

import Url


myScenario =
    [ Debug.todo "After some operations..."
    , fromJust "Make URL"
        (Url.fromString "https://example.com/foo/")
      <|
        \url ->
            [ Debug.todo "Scenarios that use `url`"
            ]
    , Debug.todo "..."
    ]
fromOk : String -> Result err a -> (a -> List (Scenario m)) -> Scenario m

Similar to fromJust, but extract Ok valur from Result.

RenderConfig

type alias RenderConfig =
{ entryPointFirstListItem : Zone -> Posix -> Markup
, dependentScenarioFirstListItem :
{ href : String
, name : String
}
-> Markup
, processSessionScenario :
{ uniqueSessionName : String } -> Markup -> Markup
, processSystemScenario :
{ uniqueSessionName : String } -> Markup -> Markup
, processUserScenario :
{ uniqueSessionName : String
, userName : String
}
-> Markup
-> Markup
}

Configuration for rendering scenario.

  • entryPointFirstListItem: First item on the list for an EntryPoint scenario.
    • argument: The time when the scenario starts and the time zone
  • dependentScenarioFirstListItem: First item on the list for a RunAfter scenario.
    • argument: href and name for its dependency.
  • processSessionScenario: Processor for markups associated with a session.
  • processSystemScenario: Processor for markups associated with the specific system in a session.
  • processUserScenario: Processor for markups associated with the specific user in a session.
en_US : RenderConfig

Standard configuration for en_US.

ja_JP : RenderConfig

Standard configuration for ja_JP.