add elm-program-test example

This commit is contained in:
Ryan Haskell-Glatz 2021-04-24 15:00:41 -05:00
parent 4c15be1291
commit 2b2643da59
14 changed files with 250 additions and 373 deletions

5
examples/06-testing/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
.elm-spa
elm-stuff
node_modules
dist

View File

@ -0,0 +1,29 @@
# my new project
> 🌳 built with [elm-spa](https://elm-spa.dev)
## dependencies
This project requires the latest LTS version of [Node.js](https://nodejs.org/)
```bash
npm install -g elm elm-spa
```
## running locally
```bash
elm-spa server # starts this app at http:/localhost:1234
```
### other commands
```bash
elm-spa add # add a new page to the application
elm-spa build # production build
elm-spa watch # runs build as you code (without the server)
elm-test # run unit tests
```
## learn more
You can learn more at [elm-spa.dev](https://elm-spa.dev)

View File

@ -0,0 +1,36 @@
{
"type": "application",
"source-directories": [
"src",
".elm-spa/defaults",
".elm-spa/generated",
"../../src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm/url": "1.0.0"
},
"indirect": {
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {
"elm-explorations/test": "1.2.2",
"avh4/elm-program-test": "3.4.0"
},
"indirect": {
"avh4/elm-fifo": "1.0.4",
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/http": "2.0.0",
"elm/random": "1.0.0"
}
}
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<script src="/dist/elm.js"></script>
<script> Elm.Main.init() </script>
</body>
</html>

View File

@ -0,0 +1,76 @@
module Pages.Home_ exposing (Model, Msg, init, page, update, view)
import Gen.Params.Home_ exposing (Params)
import Html exposing (Html)
import Html.Events
import Page
import Request
import Shared
import View exposing (View)
page : Shared.Model -> Request.With Params -> Page.With Model Msg
page shared req =
Page.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
-- INIT
type alias Model =
{ counter : Int
}
init : ( Model, Cmd Msg )
init =
( { counter = 0 }, Cmd.none )
-- UPDATE
type Msg
= Increment
| Decrement
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Increment ->
( { model | counter = model.counter + 1 }, Cmd.none )
Decrement ->
( { model | counter = model.counter - 1 }, Cmd.none )
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> View Msg
view model =
{ title = "Homepage"
, body =
[ Html.button [ Html.Events.onClick Increment ] [ Html.text "+" ]
, Html.p [] [ Html.text ("Count: " ++ String.fromInt model.counter) ]
, Html.button [ Html.Events.onClick Decrement ] [ Html.text "-" ]
]
}

View File

@ -0,0 +1,6 @@
module Utils.String exposing (capitalizeFirstLetter)
capitalizeFirstLetter : String -> String
capitalizeFirstLetter str =
String.toUpper (String.left 1 str) ++ String.dropLeft 1 str

View File

@ -0,0 +1,46 @@
module ProgramTests.Homepage exposing (all)
import Pages.Home_
import ProgramTest exposing (ProgramTest, clickButton, expectViewHas)
import Test exposing (Test, describe, test)
import Test.Html.Selector exposing (text)
start : ProgramTest Pages.Home_.Model Pages.Home_.Msg (Cmd Pages.Home_.Msg)
start =
ProgramTest.createDocument
{ init = \_ -> Pages.Home_.init
, update = Pages.Home_.update
, view = Pages.Home_.view
}
|> ProgramTest.start ()
all : Test
all =
describe "Pages.Homepage_"
[ test "Counter increment works" <|
\() ->
start
|> clickButton "+"
|> expectViewHas
[ text "Count: 1"
]
, test "Counter decrement works" <|
\() ->
start
|> clickButton "-"
|> expectViewHas
[ text "Count: -1"
]
, test "Clicking multiple buttons works too" <|
\() ->
start
|> clickButton "-"
|> clickButton "+"
|> clickButton "-"
|> clickButton "-"
|> expectViewHas
[ text "Count: -2"
]
]

View File

@ -0,0 +1,28 @@
module UnitTests.Utils.StringTest exposing (suite)
import Expect
import Test exposing (Test, describe, test)
import Utils.String
suite : Test
suite =
describe "Utils.String"
[ describe "capitalizeFirstLetter"
[ test "works with a single word"
(\_ ->
Utils.String.capitalizeFirstLetter "ryan"
|> Expect.equal "Ryan"
)
, test "doesn't affect already capitalized words"
(\_ ->
Utils.String.capitalizeFirstLetter "Ryan"
|> Expect.equal "Ryan"
)
, test "only capitalizes first word in sentence"
(\_ ->
Utils.String.capitalizeFirstLetter "ryan loves writing unit tests"
|> Expect.equal "Ryan loves writing unit tests"
)
]
]

View File

@ -1,8 +1,7 @@
module ElmSpa.Internals.Page exposing
( Page, static, sandbox, element, advanced
, Protected(..), protected3
, Protected(..), protected
, Bundle, bundle
, protected, protected2
)
{-|
@ -15,20 +14,13 @@ module ElmSpa.Internals.Page exposing
# **User Authentication**
@docs Protected, protected3
@docs Protected, protected
# For generated code
@docs Bundle, bundle
# Deprecated
This will be removed before release, included to prevent bumping to 6.0.0 during beta!
@docs protected, protected2
-}
import Browser.Navigation exposing (Key)
@ -189,9 +181,7 @@ type Protected user route
| RedirectTo route
{-| **This will become `protected`, and [protected](#protected) will be removed in the v6 release.** Keeping them both to prevent a version bump!
Prefixing any of the four functions above with `protected` will guarantee that the page has access to a user. Here's an example with `sandbox`:
{-| Prefixing any of the four functions above with `protected` will guarantee that the page has access to a user. Here's an example with `sandbox`:
-- before
Page.sandbox
@ -215,7 +205,7 @@ Prefixing any of the four functions above with `protected` will guarantee that t
view : Model -> View Msg
-}
protected3 :
protected :
{ effectNone : effect
, fromCmd : Cmd msg -> effect
, beforeInit : shared -> Request route () -> Protected user route
@ -258,7 +248,7 @@ protected3 :
)
-> Page shared route effect view model msg
}
protected3 options =
protected options =
let
protect toPage toRecord =
Page
@ -460,178 +450,3 @@ adapters =
, subscriptions = page.subscriptions
}
}
-- DEPRECATED - will be removed in v6
{-| Deprecated! Will be replaced by [protected3](#protected3)
-}
protected :
{ effectNone : effect
, fromCmd : Cmd msg -> effect
, user : shared -> Request route () -> Maybe user
, route : route
}
->
{ static :
{ view : user -> view
}
-> Page shared route effect view () msg
, sandbox :
{ init : user -> model
, update : user -> msg -> model -> model
, view : user -> model -> view
}
-> Page shared route effect view model msg
, element :
{ init : user -> ( model, Cmd msg )
, update : user -> msg -> model -> ( model, Cmd msg )
, view : user -> model -> view
, subscriptions : user -> model -> Sub msg
}
-> Page shared route effect view model msg
, advanced :
{ init : user -> ( model, effect )
, update : user -> msg -> model -> ( model, effect )
, view : user -> model -> view
, subscriptions : user -> model -> Sub msg
}
-> Page shared route effect view model msg
}
protected options =
let
protect pageWithUser page =
Page
(\shared req ->
case options.user shared req of
Just user ->
Ok (pageWithUser user page)
Nothing ->
Err options.route
)
in
{ static =
protect
(\user page ->
{ init = \_ -> ( (), options.effectNone )
, update = \_ model -> ( model, options.effectNone )
, view = \_ -> page.view user
, subscriptions = \_ -> Sub.none
}
)
, sandbox =
protect
(\user page ->
{ init = \_ -> ( page.init user, options.effectNone )
, update = \msg model -> ( page.update user msg model, options.effectNone )
, view = page.view user
, subscriptions = \_ -> Sub.none
}
)
, element =
protect
(\user page ->
{ init = \_ -> page.init user |> Tuple.mapSecond options.fromCmd
, update = \msg model -> page.update user msg model |> Tuple.mapSecond options.fromCmd
, view = page.view user
, subscriptions = page.subscriptions user
}
)
, advanced =
protect
(\user page ->
{ init = \_ -> page.init user
, update = page.update user
, view = page.view user
, subscriptions = page.subscriptions user
}
)
}
{-| Deprecated! Will be replaced by [protected3](#protected3)
-}
protected2 :
{ effectNone : effect
, fromCmd : Cmd msg -> effect
, beforeInit : shared -> Request route () -> Protected user route
}
->
{ static :
{ view : user -> view
}
-> Page shared route effect view () msg
, sandbox :
{ init : user -> model
, update : user -> msg -> model -> model
, view : user -> model -> view
}
-> Page shared route effect view model msg
, element :
{ init : user -> ( model, Cmd msg )
, update : user -> msg -> model -> ( model, Cmd msg )
, view : user -> model -> view
, subscriptions : user -> model -> Sub msg
}
-> Page shared route effect view model msg
, advanced :
{ init : user -> ( model, effect )
, update : user -> msg -> model -> ( model, effect )
, view : user -> model -> view
, subscriptions : user -> model -> Sub msg
}
-> Page shared route effect view model msg
}
protected2 options =
let
protect pageWithUser page =
Page
(\shared req ->
case options.beforeInit shared req of
Provide user ->
Ok (pageWithUser user page)
RedirectTo route ->
Err route
)
in
{ static =
protect
(\user page ->
{ init = \_ -> ( (), options.effectNone )
, update = \_ model -> ( model, options.effectNone )
, view = \_ -> page.view user
, subscriptions = \_ -> Sub.none
}
)
, sandbox =
protect
(\user page ->
{ init = \_ -> ( page.init user, options.effectNone )
, update = \msg model -> ( page.update user msg model, options.effectNone )
, view = page.view user
, subscriptions = \_ -> Sub.none
}
)
, element =
protect
(\user page ->
{ init = \_ -> page.init user |> Tuple.mapSecond options.fromCmd
, update = \msg model -> page.update user msg model |> Tuple.mapSecond options.fromCmd
, view = page.view user
, subscriptions = page.subscriptions user
}
)
, advanced =
protect
(\user page ->
{ init = \_ -> page.init user
, update = page.update user
, view = page.view user
, subscriptions = page.subscriptions user
}
)
}

View File

@ -1,177 +0,0 @@
module ElmSpa.Page exposing
( Page
, static, sandbox, element, shared
)
{-|
# **( These docs are for CLI contributors )**
### If you are using **elm-spa**, check out [the official guide](https://elm-spa.dev/guide) instead!
---
Every page in **elm-spa** ultimately becomes one data type: `Page`
This makes it easy to wire them up in the generated code, because they
all have the same type signature.
**Note:** You won't use `ElmSpa.Page` directly, instead you'll be using `Page`,
which removes some of the generic type parameters
type alias Page model msg =
{ init : ( model, Cmd msg, List Shared.Msg )
, update : msg -> model -> ( model, Cmd msg, List Shared.Msg )
, view : model -> View msg
, subscriptions : model -> Sub msg
}
## Pages
@docs Page
@docs static, sandbox, element, shared
-}
import Browser.Navigation exposing (Key)
import ElmSpa.Request as Request exposing (Request)
import Url exposing (Url)
{-| The common `Page` type that each of the functions below creates
Page.static { ... } -- Page () Never
Page.sandbox { ... } -- Page Model Msg
Page.element { ... } -- Page Model Msg
Page.shared { ... } -- Page Model Msg
-}
type alias Page sharedMsg view model msg =
{ init : () -> ( model, Cmd msg, List sharedMsg )
, update : msg -> model -> ( model, Cmd msg, List sharedMsg )
, view : model -> view
, subscriptions : model -> Sub msg
}
{-| A page that can track state, and respond to user input.
Page.static
{ view = view
}
view : View Never
-}
static :
{ view : view
}
-> Page sharedMsg view () Never
static options =
{ init = \_ -> ( (), Cmd.none, [] )
, update = \_ _ -> ( (), Cmd.none, [] )
, view = \_ -> options.view
, subscriptions = \_ -> Sub.none
}
{-| A page that can track state, and respond to user input.
Page.sandbox
{ init = init
, update = update
, view = view
}
init : Model
update : Msg -> Model -> Model
view : Model -> View Msg
-}
sandbox :
{ init : model
, update : msg -> model -> model
, view : model -> view
}
-> Page sharedMsg view model msg
sandbox options =
{ init = \_ -> ( options.init, Cmd.none, [] )
, update = \msg model -> ( options.update msg model, Cmd.none, [] )
, view = options.view
, subscriptions = \_ -> Sub.none
}
{-| A page that can send side-effects and subscribe to events.
Page.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
init : (Model, Cmd Msg)
update : Msg -> Model -> (Model, Cmd Msg)
view : Model -> View Msg
subscriptions : Model -> Sub Msg
-}
element :
{ init : ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, view : model -> view
, subscriptions : model -> Sub msg
}
-> Page sharedMsg view model msg
element options =
{ init =
\_ ->
let
( model, cmd ) =
options.init
in
( model, cmd, [] )
, update =
\msg_ model_ ->
let
( model, cmd ) =
options.update msg_ model_
in
( model, cmd, [] )
, view = options.view
, subscriptions = options.subscriptions
}
{-| A page that allows messages to be sent to the `Shared` module.
Page.shared
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
init : (Model, Cmd Msg, List Shared.Msg )
update : Msg -> Model -> (Model, Cmd Msg, List Shared.Msg )
view : Model -> View Msg
subscriptions : Model -> Sub Msg
-}
shared :
{ init : ( model, Cmd msg, List sharedMsg )
, update : msg -> model -> ( model, Cmd msg, List sharedMsg )
, view : model -> view
, subscriptions : model -> Sub msg
}
-> Page sharedMsg view model msg
shared options =
{ init = \_ -> options.init
, update = options.update
, view = options.view
, subscriptions = options.subscriptions
}

View File

@ -23,7 +23,7 @@ export const build = ({ env, runElmMake } : { env : Environment, runElmMake: boo
createMissingAddTemplates()
])
.then(createGeneratedFiles)
.then(runElmMake ? compileMainElm(env): identity)
.then(runElmMake ? compileMainElm(env): _ => ` ${check} ${bold}elm-spa${reset} generated new files.`)
const createMissingDefaultFiles = async () => {
type Action

View File

@ -70,7 +70,7 @@ const start = async () => new Promise((resolve, reject) => {
export default {
run: async () => {
const output = await watch()
const output = await watch(true)
return start().then(serverOutput => [ serverOutput, output ])
}
}

View File

@ -2,8 +2,8 @@ import { build } from './build'
import chokidar from 'chokidar'
import config from '../config'
export const watch = () => {
const runBuild = build({ env: 'development', runElmMake: false })
export const watch = (runElmMake : boolean) => {
const runBuild = build({ env: 'development', runElmMake })
chokidar
.watch(config.folders.src, { ignoreInitial: true })
@ -12,10 +12,12 @@ export const watch = () => {
.then(output => {
console.info('')
console.info(output)
console.info('')
})
.catch(reason => {
console.info('')
console.error(reason)
console.info('')
})
)
@ -23,5 +25,5 @@ export const watch = () => {
}
export default {
run: watch
run: () => watch(false)
}

View File

@ -117,7 +117,7 @@ protected :
-> With model msg
}
protected =
ElmSpa.protected3
ElmSpa.protected
{ effectNone = Effect.none
, fromCmd = Effect.fromCmd
, beforeInit = Auth.beforeProtectedInit