Module for Scenario-Driven Development.
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
.
none : Scenario flags m
A Scenario that does nothing.
sequence : List (Scenario flags m) -> Scenario flags m
Return a new Scenario that evaluates given Scenarios sequentially.
toTest : { props : Tepa.ApplicationProps flags memory, sections : List (Section flags memory), origin : { secure : Basics.Bool, hostname : String, port_ : Maybe Basics.Int } } -> Test
Generate scenario tests.
toMarkdown : { title : String, sections : List (Section flags m), config : RenderConfig } -> Result InvalidMarkup String
Generate scenario document markdown text.
stringifyInvalidMarkup : InvalidMarkup -> String
{ title : String
, content : List (Scenario flags memory)
, dependency : Dependency
}
Titled sequence of Scenarios.
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 Memory
commonScenario =
{ title = "Common scenario"
, content =
[ Debug.todo "Common scenarios"
, Debug.todo "..."
]
, dependency =
EntryPoint
(TimeZone.asia__tokyo ())
(Time.millisToPosix 1672531200000)
}
caseA : Section Flags Memory
caseA =
{ title = "Case A"
, content =
-- After common scenario,
[ Debug.todo "User clicks button A"
, Debug.todo "..."
]
, dependency = RunAfter commonScenario.title
}
caseB : Section Flags Memory
caseB =
{ title = "Case B"
, content =
-- After common scenario,
[ Debug.todo "User clicks button B"
, Debug.todo "..."
]
, dependency = RunAfter commonScenario.title
}
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
.An application user.
defineUser : { name : String } -> User
Define a user for your Scenario.
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"
}
defineSession : { uniqueName : String, user : User } -> Session
Define a session for your Scenario.
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 : Basics.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
}
```
userComment : User -> String -> Scenario flags 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 flags m
System comment.
This Scenario only affects document generation, and is ignored for scenario test generation.
comment : Markup -> Scenario flags m
Lower level function to add detailed comments.
todo : Session -> Markup -> Scenario flags m
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.
userTodo : Session -> Markup -> Scenario flags m
Similar todo
, but for user actions.
It prepends the username to the markup.
expectMemory : Session -> Markup -> { layer : Tepa.Layer m -> Maybe (Tepa.Layer m1), expectation : m1 -> Expectation } -> Scenario flags 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.
During the initialization phase of the application, or if no Layers are found for the query, it fails the test.
expectAppView : Session -> Markup -> { expectation : Tepa.Document -> Expectation } -> Scenario flags 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 "..."
]
expectValues : Session -> Markup -> { layer : Tepa.Layer m -> Maybe (Tepa.Layer m1), expectation : Dict String String -> Expectation } -> Scenario flags m
Describe your expectations for the user input values in the specified Layer at the point.
You use Expect module to describe your expectation.
During the initialization phase of the application, or if no Layers are found for the query, it fails the test.
expectCurrentTime : Markup -> { expectation : Tepa.Time.Posix -> Expectation } -> Scenario flags 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 Flags Memory
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.
expectHttpRequest : Session -> Markup -> { layer : Tepa.Layer m -> Maybe (Tepa.Layer m1), expectation : List HttpRequest -> Expectation } -> Scenario flags m
Describe your expectations for the unresolved HTTP requests at the time.
You use Expect module to describe your expectation.
During the initialization phase of the application, the layer
parameter is ignored, so it is common to set \_ -> Nothing
in such case.
After initialization, if no Layers are found for the query, it fails the test.
expectPortRequest : Session -> Markup -> { layer : Tepa.Layer m -> Maybe (Tepa.Layer m1), expectation : List PortRequest -> Expectation } -> Scenario flags m
Describe your expectations for the unresolved Port requests at the time.
You use Expect module to describe your expectation.
During the initialization phase of the application, the layer
parameter is ignored, so it is common to set \_ -> Nothing
in such case.
After initialization, if no Layers are found for the query, it fails the test.
expectRandomRequest : Session -> Markup -> { layer : Tepa.Layer m -> Maybe (Tepa.Layer m1), spec : Tepa.Random.Spec a } -> Scenario flags m
Describe your expectations for the unresolved Random requests at the time.
You pass Tepa.Random.Spec
to specify your expected request.
During the initialization phase of the application, the layer
parameter is ignored, so it is common to set \_ -> Nothing
in such case.
After initialization, if no Layers are found for the query, it fails the test.
appLayer : Tepa.Layer m -> Maybe (Tepa.Layer m)
Specifies the application root layer, which is just an alias for Just
.
childLayer : (m1 -> Maybe (Tepa.Layer m2)) -> (Tepa.Layer m -> Maybe (Tepa.Layer m1)) -> Tepa.Layer m -> Maybe (Tepa.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
mapLayer : (m1 -> m2) -> (Tepa.Layer m -> Maybe (Tepa.Layer m1)) -> Tepa.Layer m -> Maybe (Tepa.Layer m2)
loadApp : Session -> Markup -> { path : AppUrl, flags : Json.Encode.Value } -> Scenario flags 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 "..."
]
closeApp : Session -> Markup -> Scenario flags m
Close the app.
userOperation : Session -> Markup -> { query : Test.Html.Query.Single Tepa.Msg -> Test.Html.Query.Single Tepa.Msg, operation : ( String, Json.Encode.Value ) } -> Scenario flags 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 -> Basics.Int -> Scenario flags m
Wait for given micro seconds.
It only affects Promises defined in Tepa.Time, so you should not use Time
module and Process.sleep
with TEPA._
httpResponse : Session -> Markup -> { layer : Tepa.Layer m -> Maybe (Tepa.Layer m1), response : HttpRequest -> Maybe ( Http.Metadata, String ) } -> Scenario flags m
Simulate response to the Tepa.Http.request
and Tepa.Http.bytesRequest
.
During the initialization phase of the application, the layer
parameter is ignored, so it is common to set \_ -> Nothing
in such case.
After initialization, if no Layers are found for the query, it does nothing and just passes the test.
httpBytesResponse : Session -> Markup -> { layer : Tepa.Layer m -> Maybe (Tepa.Layer m1), response : HttpRequest -> Maybe ( Http.Metadata, Bytes ) } -> Scenario flags m
Similar to httpResponse
, but responds with Bytes
.
{ 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 caseheaders
like accept
and cookie
, all names in lower case, and all values as it isurl
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.
portResponse : Session -> Markup -> { layer : Tepa.Layer m -> Maybe (Tepa.Layer m1), response : PortRequest -> Maybe Json.Encode.Value } -> Scenario flags 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 "..."
]
During the initialization phase of the application, the layer
parameter is ignored, so it is common to set \_ -> Nothing
in such case.
After initialization, if no Layers are found for the query, it does nothing and just passes the test.
{ name : String
, requestBody : Json.Encode.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
.randomResponse : Session -> Markup -> { layer : Tepa.Layer m -> Maybe (Tepa.Layer m1), spec : Tepa.Random.Spec a, response : a } -> Scenario flags 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 flags 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.
During the initialization phase of the application, the layer
parameter is ignored, so it is common to set \_ -> Nothing
in such case.
After initialization, if no Layers are found for the query, it does nothing and just passes the test.
forward : Session -> Markup -> Scenario flags m
Simulate borwser forward event.
If there are no pages to forward, the test fails.
back : Session -> Markup -> Scenario flags m
Simulate borwser back event.
If there are no pages to back, the test fails.
pushPath : Session -> AppUrl -> Markup -> Scenario flags m
Simulate page transition.
fromJust : String -> Maybe a -> (a -> List (Scenario flags m)) -> Scenario flags 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 flags m)) -> Scenario flags m
Similar to fromJust
, but extract Ok
valur from Result
.
{ entryPointFirstListItem : Tepa.Time.Zone -> Tepa.Time.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.
EntryPoint
scenario.RunAfter
scenario.href
and name
for its dependency.en_US : RenderConfig
Standard configuration for en_US.
ja_JP : RenderConfig
Standard configuration for ja_JP.