Module for Scenario-Driven Development.
Return a new Scenario that evaluates given Scenarios sequentially.
Generate scenario tests.
Generate scenario document markdown text.
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.
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"
}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 "..."
]Constructor for Markup.
Markup:
markup "No detail"Rendered to:
- No detailMarkup:
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"
}
```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
}
```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.
System comment.
This Scenario only affects document generation, and is ignored for scenario test generation.
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.
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.
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 "..."
]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.
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.
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.
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.
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.
Specifies the application root layer, which is just an alias for Just.
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 .childLoad 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 "..."
]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.
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.
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.
Similar to httpResponse, but responds with Bytes.
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 isurlrequestBodyNote: 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.
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.
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.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.
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.
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 "..."
]Similar to fromJust, but extract Ok valur from Result.
Configuration for rendering scenario.
EntryPoint scenario.RunAfter scenario.href and name for its dependency.Standard configuration for en_US.
Standard configuration for ja_JP.
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 withtoMarkdown.